チャット会話リストブロック
Chat Conversation Listコンポーネントは、ReactとTypeScriptで構築された完全にカスタマイズ可能でアクセシブルな会話リストインターフェースです。検索機能、無限スクロール、チャットアプリケーションインターフェースのための柔軟なカスタマイズオプションを備えた完全な会話概要を提供します。
🚀 インストール
npm install @nodeblocks/frontend-chat-conversation-list-block@0.2.0
📖 使用法
import {ChatConversationList} from '@nodeblocks/frontend-chat-conversation-list-block';
- 基本的な使用法
- 高度な使用法
function SimpleChatConversationList() { const [conversations] = useState([ { id: 'conv-001', title: 'カスタマーサポート', content: 'お問い合わせありがとうございます。今日はどのようなお手伝いができますか?', dateTime: '2025-02-01T10:00:00Z', titleLines: 1, unreadCount: 3, href: '/chat/conv-001', avatar: { sx: { bgcolor: 'primary.main' }, }, }, { id: 'conv-002', title: 'プロジェクトチーム討議', content: '新機能の実装準備ができました。レビューをお願いします。', dateTime: '2025-02-01T09:30:00Z', titleLines: 2, unreadCount: 1, href: '/chat/conv-002', avatar: { sx: { bgcolor: 'secondary.main' }, }, }, { id: 'conv-003', title: 'マーケティングキャンペーン', content: 'キャンペーン戦略について話し合うミーティングをスケジュールしましょう。', dateTime: '2025-01-31T16:45:00Z', titleLines: 1, href: '/chat/conv-003', avatar: { sx: { bgcolor: 'info.main' }, }, }, ]); return ( <ChatConversationList conversations={conversations} labels={{ heading: '会話', emptyState: 'まだ会話がありません', }} placeholders={{ search: '会話を検索...', }} search={{ defaultValue: '', onChange: (value) => { console.log('検索が変更されました:', value); }, }} onNavigate={(url) => { console.log('ナビゲート:', url); }} onScrollBottom={() => { console.log('さらに会話を読み込み'); }} isLoading={false} > <ChatConversationList.Heading /> <ChatConversationList.SearchBar /> <ChatConversationList.ScrollPanel /> </ChatConversationList> ); }
function AdvancedChatConversationList() { const [conversations, setConversations] = useState([ { id: 'support-001', title: 'カスタマーサポートチーム', content: 'あなたの問題は解決されました。他にお手伝いできることはありますか?', dateTime: '2025-02-01T14:30:00Z', titleLines: 1, unreadCount: 5, href: '/chat/support-001', isSelected: false, avatar: { sx: { bgcolor: 'primary.main' }, src: '/avatars/support-team.jpg', }, }, { id: 'team-002', title: '開発チーム討議', content: 'コードレビューが完了しました。新機能ブランチをマージする準備ができています。', dateTime: '2025-02-01T13:15:00Z', titleLines: 2, unreadCount: 2, href: '/chat/team-002', isSelected: true, avatar: { sx: { bgcolor: 'secondary.main' }, }, }, { id: 'client-003', title: 'クライアントプロジェクト更新', content: 'プロジェクトのタイムラインについて話し合うため、明日午後2時にミーティングを予定しています。', dateTime: '2025-02-01T11:45:00Z', titleLines: 1, href: '/chat/client-003', avatar: { sx: { bgcolor: 'info.main' }, src: '/avatars/client.jpg', }, }, ]); return ( <ChatConversationList conversations={conversations} labels={{ heading: 'チーム会話', emptyState: '検索に一致する会話がありません', }} placeholders={{ search: '名前またはメッセージで検索...', }} search={{ defaultValue: '', onChange: (value) => { console.log('検索が変更されました:', value); }, }} onNavigate={(url) => { console.log('ナビゲート:', url); }} onScrollBottom={() => { console.log('さらに会話を読み込み'); }} isLoading={false} className="custom-conversation-list" sx={{maxHeight: '500px', border: '1px solid #e0e0e0'}} > {({defaultBlocks}) => ({ blocks: { ...defaultBlocks, statsBar: ( <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 16px', backgroundColor: '#f8f9fa', borderBottom: '1px solid #dee2e6', fontSize: '14px', color: '#6c757d', }} > <span>📈 {conversations.length} 会話</span> <span>🔔 {conversations.reduce((acc, conv) => acc + (conv.unreadCount || 0), 0)} 未読</span> </div> ), footer: ( <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', padding: '16px', backgroundColor: '#f8f9fa', borderTop: '1px solid #dee2e6', }} > <button style={{ padding: '8px 16px', backgroundColor: '#007bff', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', }} > ➕ 新しい会話を始める </button> </div> ), }, blockOrder: ['heading', 'searchBar', 'statsBar', 'scrollPanel', 'footer'], })} </ChatConversationList> ); }
🔧 プロパティリファレンス
メインコンポーネントプロパティ
| プロパティ | 型 | デフォルト | 説明 |
|---|---|---|---|
conversations | Conversation[] | 必須 | 表示する会話オブジェクトの配列 |
labels | {heading?: string; emptyState?: string;} | undefined | テキストラベルの設定 |
placeholders | {search?: string;} | undefined | プレースホルダーテキストの設定 |
isLoading | boolean | undefined | ローディング状態を表示 |
onNavigate | (url: string) => void | 必須 | ナビゲーションコールバック(hrefに必要) |
onScrollBottom | () => void | undefined | リストの下部に到達したときのコールバック |
search | {defaultValue?: string; onChange?: (value: string) => void;} | undefined | 検索入力の設定 |
size | 'compact' | 'normal' | undefined | テキストサイズとスペーシングを設定(サブコンポーネントはデフォルトで'compact') |
className | string | undefined | スタイリング用の追加CSSクラス名 |
sx | SxProps<Theme> | undefined | カスタムスタイリング用のMUIシステムプロパティ |
children | BlocksOverride | undefined | デフォルトレンダリングをオーバーライドするカスタムブロックコンポーネント |
注意: このコンポーネントはすべてのMUI Stackコンポーネントプロパティを継承します。
会話オブジェクト
| プロパティ | 型 | デフォルト | 説明 |
|---|---|---|---|
id | string | 必須 | 会話の一意識別子 |
title | string | 必須 | 会話のタイトル |
titleLines | 1 | 2 | 必須 | タイトルが切り取られる前の行数 |
content | string | undefined | 会話本文(一行後に切り取られます) |
dateTime | string | undefined | ISO文字列としての日時(date-fnsで日本語ロケールを使用してフォーマット) |
unreadCount | number | undefined | 未読数(99を超えると「99...」と表示されます) |
href | string | undefined | 会話をリンク可能にし、ホバー状態を追加 |
isSelected | boolean | undefined | trueの場合、会話をハイライト |
avatar | Partial<AvatarProps> & {isLoading?: boolean} | undefined | オプションのローディング状態を持つMUI Avatarプロパティ |
サブコンポーネント
ChatConversationListコンポーネントは複数のサブコンポーネントを提供します。すべてのサブコンポーネントは、メインコンポーネントのコンテキストからデフォルト値を受け取り、プロパティを通じてこれらの値をオーバーライドできます。
ChatConversationList.Heading
MUI Typographyを使用して会話リストのヘッダーをレンダリングします。
| プロパティ | 型 | デフォルト | 説明 |
|---|---|---|---|
labels | {heading?: string;} | コンテキスト値 | ラベル設定(コンテキストをオーバーライド) |
size | 'compact' | 'normal' | 'compact' | サイズバリアント(コンテキストをオーバーライド) |
className | string | undefined | スタイリング用の追加CSSクラス名 |
children | ReactNode | デフォルトヘッダーコンテンツ | デフォルトヘッダーをオーバーライドするカスタムコンテンツ |
component | ElementType | 'h1' | レンダリングするHTML要素 |
variant | TypographyVariant | 'h4' | MUI Typographyバリアント |
sx | SxProps<Theme> | undefined | カスタムスタイリング用のMUIシステムプロパティ |
注意: このコンポーネントはすべてのMUI Typographyコンポーネントプロパティを継承します。
ChatConversationList.SearchBar
デバウンス検索機能付きの検索入力をレンダリングします。
| プロパティ | 型 | デフォルト | 説明 |
|---|---|---|---|
search | {defaultValue?: string; onChange?: (value: string) => void;} | コンテキスト値 | 検索設定(コンテキストをオーバーライド) |
placeholders | {search?: string;} | コンテキスト値 | プレースホルダー設定(コンテキストをオーバーライド) |
conversations | Conversation[] | コンテキスト値 | 表示を決定する会話(コンテキストをオーバーライド) |
size | 'compact' | 'normal' | 'compact' | サイズバリアント(コンテキストをオーバーライド) |
debounceTime | number | 500 | 検索onChangeをトリガーする前の遅延(ミリ秒) |
className | string | undefined | スタイリング用の追加CSSクラス名 |
defaultValue | string | undefined | 検索入力の初期値 |
placeholder | string | undefined | 空のときのプレースホルダーテキスト |
variant | TextFieldVariant | 'outlined' | MUI TextFieldバリアント |
type | string | 'search' | HTML入力タイプ |
fullWidth | boolean | true | 入力が全幅を取るかどうか(size='normal'時は無視) |
sx | SxProps<Theme> | undefined | カスタムスタイリング用のMUIシステムプロパティ |
注意: このコンポーネントはすべてのMUI TextFieldコンポーネントプロパティを継承します(onChangeとsizeを除く)。
ChatConversationList.Header
HeadingとSearchBarをオプションのディバイダー付きで組み合わせる便利なコンポーネント。
| プロパティ | 型 | デフォルト | 説明 |
|---|---|---|---|
size | 'compact' | 'normal' | コンテキスト値 | サイズバリアント(コンテキストをオーバーライド) |
className | string | undefined | スタイリング用の追加CSSクラス名 |
children | ReactNode | デフォルトヘッダーコンテンツ | デフォルトヘッダーをオーバーライドするカスタムコンテンツ |
sx | SxProps<Theme> | undefined | カスタムスタイリング用のMUIシステムプロパティ |
注意: このコンポーネントはすべてのMUI Stackコンポーネントプロパティを継承します(directionを除く)。
ChatConversationList.ScrollPanel
無限スクロールサポート付きのスクロール可能な会話リストをレンダリングします。
| プロパティ | 型 | デフォルト | 説明 |
|---|---|---|---|
conversations | Conversation[] | コンテキスト値 | 会話配列(コンテキストをオーバーライド) |
labels | {heading?: string; emptyState?: string;} | コンテキスト値 | ラベル設定(コンテキストをオーバーライド) |
isLoading | boolean | コンテキスト値 | ローディング状態(コンテキストをオーバーライド) |
onScrollBottom | () => void | コンテキスト値 | スクロールコールバック(コンテキストをオーバーライド) |
size | 'compact' | 'normal' | 'compact' | サイズバリアント(コンテキストをオーバーライド) |
className | string | undefined | スタイリング用の追加CSSクラス名 |
children | ReactNode | デフォルト会話アイテム | デフォルトレンダリングをオーバーライドするカスタムコンテンツ |
sx | SxProps<Theme> | undefined | カスタムスタイリング用のMUIシステムプロパティ |
注意: このコンポーネントはすべてのMUI Stackコンポーネントプロパティを継承します(directionとspacingを除く)。
🎨 設定例
このコンポーネントはアバターのカスタマイズにMUI Avatarプロパティを使用します。アバターの設定方法は以下の通りです:
const conversation = {
id: 'conv-001',
title: 'サポートチーム',
titleLines: 1,
avatar: {
// MUI Avatarプロパティ
src: '/path/to/image.jpg', // 画像ソース
srcSet: '/path/to/image@2x.jpg 2x', // レスポンシブ画像
sx: { bgcolor: 'primary.main' }, // カスタムスタイリング
children: 'ST', // フォールバックコンテンツ(イニシャル)
// 拡張プロパティ
isLoading: false, // ローディングプレースホルダーを表示
},
};
🔧 TypeScriptサポート
包括的な型定義による完全なTypeScriptサポート:
import {ChatConversationList} from '@nodeblocks/frontend-chat-conversation-list-block';
import {AvatarProps} from '@mui/material';
import {useState} from 'react';
interface LabelsConfig {
heading?: string;
emptyState?: string;
}
interface SearchConfig {
defaultValue?: string;
onChange?: (value: string) => void;
}
interface PlaceholdersConfig {
search?: string;
}
interface Conversation {
/** 会話の一意識別子 */
id: string;
/** 会話のタイトル */
title: string;
/** タイトルが切り取られる前の行数 */
titleLines: 1 | 2;
/** 会話本文(一行後に切り取られます) */
content?: string;
/** ISO文字列としての日時(date-fnsでフォーマット) */
dateTime?: string;
/** 未読数(99を超えると99で切り取られます) */
unreadCount?: number;
/** 会話をリンク可能にし、ホバー状態を追加 */
href?: string;
/** trueの場合、会話をハイライト */
isSelected?: boolean;
/** オプションのローディング状態を持つMUI Avatarプロパティ */
avatar?: Partial<AvatarProps> & {
/** 一時的なローディング状態を表示 */
isLoading?: boolean;
};
}
// 検索とページネーション付きの完全な型付き例
function TypedChatConversationList() {
const [conversations, setConversations] = useState<Conversation[]>([
{
id: 'support-001',
title: 'カスタマーサポートチーム',
content: 'あなたの問題は解決されました。他にお手伝いできることはありますか?',
dateTime: '2025-02-01T14:30:00Z',
titleLines: 1,
unreadCount: 2,
href: '/chat/support-001',
isSelected: false,
avatar: {
sx: { bgcolor: 'primary.main' },
src: '/avatars/support-team.jpg',
},
},
{
id: 'team-002',
title: '開発チーム討議',
content: 'コードレビューが完了しました。新機能ブランチをマージする準備ができています。',
dateTime: '2025-02-01T13:15:00Z',
titleLines: 2,
unreadCount: 5,
href: '/chat/team-002',
isSelected: true,
avatar: {
sx: { bgcolor: 'secondary.main' },
},
},
{
id: 'client-003',
title: 'クライアントプロジェクト更新',
content: 'プロジェクトのタイムラインについて話し合うため、明日午後2時にミーティングを予定しています。',
dateTime: '2025-02-01T11:45:00Z',
titleLines: 1,
href: '/chat/client-003',
avatar: {
sx: { bgcolor: 'info.main' },
src: '/avatars/client.jpg',
},
},
]);
const [isLoading, setIsLoading] = useState(false);
const [searchValue, setSearchValue] = useState('');
const [filteredConversations, setFilteredConversations] = useState(conversations);
const handleSearch = (value: string) => {
setSearchValue(value);
if (!value.trim()) {
setFilteredConversations(conversations);
return;
}
const filtered = conversations.filter(
conv =>
conv.title.toLowerCase().includes(value.toLowerCase()) ||
conv.content?.toLowerCase().includes(value.toLowerCase()),
);
setFilteredConversations(filtered);
};
const handleScrollBottom = () => {
console.log('さらに会話を読み込み中...');
setIsLoading(true);
setTimeout(() => {
const newConversations: Conversation[] = [
{
id: `conv-${Date.now()}`,
title: '新しい会話',
content: 'これは新しく読み込まれた会話です。',
dateTime: new Date().toISOString(),
titleLines: 1,
href: `/chat/conv-${Date.now()}`,
avatar: {
sx: { bgcolor: 'warning.main' },
},
},
];
setConversations(prev => [...prev, ...newConversations]);
if (!searchValue.trim()) {
setFilteredConversations(prev => [...prev, ...newConversations]);
}
setIsLoading(false);
}, 1500);
};
const handleNavigate = (url: string) => {
console.log('ナビゲート:', url);
setConversations(prev =>
prev.map(conv => ({
...conv,
isSelected: conv.href === url,
})),
);
};
return (
<ChatConversationList
conversations={filteredConversations}
labels={{
heading: 'チャット会話',
emptyState: searchValue ? '検索に一致する会話がありません' : 'まだ会話がありません',
}}
placeholders={{
search: '名前またはメッセージで検索...',
}}
search={{
defaultValue: searchValue,
onChange: handleSearch,
}}
onNavigate={handleNavigate}
onScrollBottom={handleScrollBottom}
isLoading={isLoading}
className="custom-conversation-list"
sx={{maxHeight: '500px', border: '1px solid #e0e0e0'}}
>
<ChatConversationList.Heading className="custom-heading" />
<ChatConversationList.SearchBar className="custom-search-bar" debounceTime={300} />
<ChatConversationList.ScrollPanel className="custom-scroll-panel" />
</ChatConversationList>
);
}
📝 注意事項
- 日付は
date-fnsライブラリを使用して日本語ロケール(ja)で相対時間表示(例:「5分前」)でフォーマットされます - 検索入力はデフォルトでデバウンスされています(500ms)、過度なコールバックを防ぐため
- 検索入力の特殊文字は自動的にサニタイズされます
- 検索バーは会話がある場合、またはユーザーが検索を操作した場合にのみ表示されます
size='normal'を使用すると、レイアウトが異なるスペーシングでより広い表示に調整されます- 無限スクロールは効率的な検出のためにIntersectionObserverを使用して実装されています
- 選択された会話は水色の背景(
#e0f3fc)でハイライト表示されます
React、TypeScript、MUI、date-fnsを使用して❤️で構築されました。