チャット会話リストブロック
ChatConversationList は、MUI Typography と Avatar 要素に基づく、レスポンシブで高度にカスタマイズ可能な受信トレイ一覧ビューです。組み込みの検索バー、サブコンポーネント単位のデバウンス制御、未読バッジカウンター、遅延読み込み/最下部スクロール時コールバックフック、レスポンシブな2カラムの分割ペインレイアウトをサポートします。
インストール
- npm
- yarn
- pnpm
- bun
npm install @nodeblocks/frontend-chat-conversation-list-block
yarn add @nodeblocks/frontend-chat-conversation-list-block
pnpm add @nodeblocks/frontend-chat-conversation-list-block
bun add @nodeblocks/frontend-chat-conversation-list-block
必要なもの
| 項目 | 理由 |
|---|---|
conversations | 受信トレイの会話エントリ配列(id、title、content、dateTime、unreadCount など) |
onNavigate | 会話項目をクリックしたときに呼び出されるコールバック |
labels (optional) | 一覧の見出しや空状態のテキストマッピング |
search (optional) | onChange などのコールバックとデフォルト値を含む状態を持つ検索設定 |
renderDesktopPane (optional) | md 以上のビューポートに右側の詳細ペイン(ChatConversation など)を差し込むレンダープロップ |
ChatConversationList は制御された表示専用コンポーネントであり、アクティブなスレッド状態、検索フィルタリング、ページネーション一覧を内部では管理しません。アクティブな conversations 配列と選択中のスレッド index/ID は親コンポーネントの state に保持し、search.onChange に応じて項目をフィルタし、isLoading プロパティでローディング表示を管理してください。
コード例
- クイックスタート
- ラベルとURL
- コンパウンドコンポーネント
- ブロックのオーバーライド
function Example() { const conversations = [ { id: '1', title: 'Customer Support', content: 'Thank you for contacting us. How can we help you?', dateTime: new Date(Date.now() - 3 * 60 * 1000).toISOString(), titleLines: 1, unreadCount: 3, href: '#support', avatar: {children: 'CS', sx: {bgcolor: 'primary.main'}}, }, { id: '2', title: 'Project Discussion Group', content: 'The review slides are ready for feedback.', dateTime: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(), titleLines: 2, unreadCount: 0, href: '#team', avatar: {children: 'PD', sx: {bgcolor: 'secondary.main'}}, }, ]; const [searchValue, setSearchValue] = React.useState(''); const [lastEvent, setLastEvent] = React.useState('Ready'); const visibleConversations = conversations.filter((conversation) => { const haystack = `${conversation.title} ${conversation.content ?? ''}`.toLowerCase(); return !searchValue || haystack.includes(searchValue.toLowerCase()); }); return ( <div style={{height: 400, border: '1px solid #e0e0e0', borderRadius: 8, overflow: 'hidden'}}> <ChatConversationList conversations={visibleConversations} labels={{ heading: 'Conversations', emptyState: 'No chats found', }} placeholders={{ search: 'Search conversations...', }} search={{ defaultValue: '', onChange: val => { setSearchValue(val); setLastEvent(`Search key: ${val}`); }, }} onNavigate={url => setLastEvent(`Routing to: ${url}`)} /> <div style={{marginTop: 8, fontSize: 12, color: '#666'}}>{lastEvent}</div> </div> ); }
見出しテキスト、検索プレースホルダー、項目のナビゲーションルーティング、状態を持つサイドペインの詳細を URL でカスタマイズします。ユーザーが項目を選択すると、クリックハンドラがリンク遷移を捕捉して分割ペインを同期します。
function Example() { const [selectedId, setSelectedId] = React.useState('1'); const [messages, setMessages] = React.useState([ {id: 'm-1', title: 'John', content: 'Did you check the mockups?', createdAt: new Date().toISOString()}, ]); const [searchValue, setSearchValue] = React.useState(''); const [lastEvent, setLastEvent] = React.useState('Ready'); const conversations = [ { id: '1', title: 'Design Review Thread', content: 'The mockups look great! Just minor adjustments needed.', dateTime: new Date(Date.now() - 5 * 60 * 1000).toISOString(), titleLines: 1, unreadCount: 1, href: '#1', isSelected: selectedId === '1', avatar: {children: 'DR', sx: {bgcolor: 'primary.main'}}, }, { id: '2', title: 'Dev Sync Channel', content: 'Sprint planning starts tomorrow morning.', dateTime: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), titleLines: 1, unreadCount: 0, href: '#2', isSelected: selectedId === '2', avatar: {children: 'DS', sx: {bgcolor: 'secondary.main'}}, }, ]; const handleMessageSubmit = text => { setMessages(prev => [ ...prev, { id: String(Date.now()), title: 'You', content: text, createdAt: new Date().toISOString(), isOwnMessage: true, }, ]); }; return ( <div style={{height: 500, border: '1px solid #e0e0e0', borderRadius: 8, overflow: 'hidden'}}> <ChatConversationList conversations={conversations} labels={{heading: 'Support Inbox', emptyState: 'No conversations found'}} placeholders={{search: 'Filter threads...'}} search={{ defaultValue: searchValue, onChange: val => { setSearchValue(val); setLastEvent(`Searching: ${val}`); }, }} onNavigate={url => { setSelectedId(url.replace('#', '')); setLastEvent(`Navigate: ${url}`); }} // Split-pane layout for md and above viewports renderDesktopPane={() => ( <div style={{height: '100%', display: 'flex', flexDirection: 'column'}}> {selectedId ? ( <ChatConversation chatView={{ heading: { avatar: {children: selectedId === '1' ? 'DR' : 'DS'}, buttonHref: '#conversations', }, }} labels={{ chatViewHeadingButtonText: 'Exit Detail', chatViewHeadingText: selectedId === '1' ? 'Design Review' : 'Dev Sync', }} placeholders={{commentInput: 'Send response...'}} messages={messages} onMessageSubmit={handleMessageSubmit} onNavigate={url => { setSelectedId(''); setLastEvent(`Detail navigate: ${url}`); }} /> ) : ( <div style={{padding: 32, textAlign: 'center', color: '#888'}}> Select a conversation thread to view the detail stream. </div> )} </div> )} /> <div style={{marginTop: 8, fontSize: 12, color: '#666'}}>{lastEvent}</div> </div> ); }
アクティブなスレッドの index または ID(たとえば selectedId)を state に保持します。conversations には isSelected: c.id === selectedId を使って選択状態のハイライトを付け、詳細サブコンポーネント(たとえば ChatConversation)は renderDesktopPane の render prop 内にマウントしてください。
細かいコンパウンド子ブロックを使ってレイアウトを構成します。明示的な props 渡しを優先するために、conversations、search、labels のようなデータ設定をサブコンポーネントへ直接渡すこともできます(たとえば ScrollPanel、SearchBar、Heading に直接渡す)。
function Example() { const conversations = [ { id: '1', title: 'Customer Support', content: 'We are processing your ticket.', dateTime: new Date().toISOString(), titleLines: 1, unreadCount: 1, href: '#support', avatar: {children: 'CS'}, }, ]; const [lastEvent, setLastEvent] = React.useState('Ready'); return ( <div style={{height: 400, border: '1px solid #e0e0e0', borderRadius: 8, overflow: 'hidden'}}> <ChatConversationList conversations={conversations} onNavigate={url => setLastEvent(`Navigating: ${url}`)}> <div style={{ padding: '12px 24px', backgroundColor: '#fafafa', borderBottom: '1px solid #e8e8e8', display: 'flex', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }} > <ChatConversationList.Heading labels={{heading: 'Support Center'}} /> <div style={{width: 280}}> <ChatConversationList.SearchBar placeholders={{search: 'Filter tickets...'}} search={{onChange: val => setLastEvent(`Searching: ${val}`)}} /> </div> </div> <ChatConversationList.ScrollPanel conversations={conversations} isLoading={false} labels={{emptyState: 'No tickets in this folder'}} /> </ChatConversationList> <div style={{marginTop: 8, fontSize: 12, color: '#666'}}>{lastEvent}</div> </div> ); }
子関数のオーバーライドマッピングを使って、ブロックを先頭に追加したり、情報パネルを挿入したりできます。
function Example() { const conversations = [ { id: '1', title: 'General Support', content: 'Online status check.', dateTime: new Date().toISOString(), titleLines: 1, href: '#support', }, ]; const [lastEvent, setLastEvent] = React.useState('Ready'); return ( <div style={{height: 400, border: '1px solid #e0e0e0', borderRadius: 8, overflow: 'hidden'}}> <ChatConversationList conversations={conversations} labels={{heading: 'Workspace Chats'}} onNavigate={to => setLastEvent(`To: ${to}`)} > {({defaultBlocks, defaultBlockOrder}) => ({ blocks: { ...defaultBlocks, systemAlert: ( <div style={{ background: '#f6ffed', borderBottom: '1px solid #b7eb8f', padding: '8px 16px', fontSize: '12px', color: '#389e0d', textAlign: 'center', }} > Connected to the secure server. Inbox synchronization is active. </div> ), }, blockOrder: ['systemAlert', 'header', 'scrollPanel'], })} </ChatConversationList> <div style={{marginTop: 8, fontSize: 12, color: '#666'}}>{lastEvent}</div> </div> ); }
ブロックのオーバーライドを使うタイミング
オーバーライドは、受信トレイ通知バナー、オフライン警告、固定アナウンスを先頭に追加するために使います。defaultBlockOrder は ['heading', 'searchBar', 'header', 'scrollPanel', 'item'] で、デフォルトのルート描画では単独の heading、searchBar、item を除外し、その後に header と scrollPanel を描画します。
重要なプロパティ
コアプロパティ
| プロパティ | 型 | 必須 | デフォルト | 説明 |
|---|---|---|---|---|
conversations | Array<{ id: string; title: string; titleLines: 1 | 2; content?: string; dateTime?: string; href?: string; isSelected?: boolean; unreadCount?: number; avatar?: Partial<AvatarProps> & { isLoading?: boolean } }> | はい | - | 一覧内に表示される受信トレイ会話スレッドの配列 |
onNavigate | (url: string) => void | はい | - | href を持つ会話をクリックしたときに呼び出されるナビゲーションコールバック |
isLoading | boolean | いいえ | false | true のときにスクロールパネル内へローディングスピナーを表示します |
onScrollBottom | () => void | いいえ | undefined | 一覧の最下部までスクロールしたときに呼び出されるコールバック(遅延読み込みによる追加に使用) |
会話オブジェクト
conversations 配列内の各会話オブジェクトにプロパティを設定します:
| プロパティ | 型 | 必須 | デフォルト | 説明 |
|---|---|---|---|---|
id | string | はい | - | 会話スレッドの一意な識別子 |
title | string | はい | - | 会話スレッドの見出しラベル |
titleLines | 1 | 2 | はい | - | 切り詰めと省略表示の前に表示するタイトル行数 |
content | string | いいえ | undefined | メインメッセージのプレビュー本文(1行後に切り詰め) |
dateTime | string | いいえ | undefined | formatRelativeDateText で整形される ISO 日時文字列 |
href | string | いいえ | undefined | 行クリック時に onNavigate コールバックへ渡されるルーティング用アンカーリンク |
isSelected | boolean | いいえ | false | true のときにアクティブな選択をハイライト |
unreadCount | number | いいえ | undefined | 未読件数バッジを表示(99を超えると 99... と表示) |
avatar | Partial<AvatarProps> & { isLoading?: boolean } | いいえ | undefined | MUI Avatar の標準 props + ローディング状態のプレースホルダー。リンク付きアバターは component="a" で設定できます |
コンテンツプロパティ
| プロパティ | 型 | 必須 | デフォルト | 説明 |
|---|---|---|---|---|
labels | { heading?: string; emptyState?: string } | いいえ | undefined | 受信トレイ見出しのカスタムテキストと空の受信トレイ表示テキスト |
placeholders | { search?: string } | いいえ | undefined | 検索テキスト入力のプレースホルダー |
search | { defaultValue?: string; onChange?: (value: string) => void } | いいえ | undefined | 初期値と変更コールバックを含む状態を持つ検索制御 |
renderDesktopPane | () => ReactNode | いいえ | undefined | md 以上のビューポートで右側に分割ペインレイアウトを読み込むレンダープロップ |
formatRelativeDateText | (dateTime: string) => string | いいえ | (dateTime) => DateTime.fromISO(dateTime).toRelative() ?? '' | 各行の ISO dateTime プレビュー用のカスタム整形関数 |
レイアウトと構成のプロパティ
| プロパティ | 型 | 必須 | デフォルト | 説明 |
|---|---|---|---|---|
children | BlocksOverride | ReactNode | いいえ | undefined | コンパウンド子要素またはオーバーライド関数の子 |
className | string | いいえ | undefined | 外側コンテナに適用されるスタイルクラス |
sx | SxProps | いいえ | undefined | 外側の Stack に渡される MUI SX スタイル上書き |
ChatConversationList は StackProps(children を除く)をすべて継承します。defaultBlockOrder は ['heading', 'searchBar', 'header', 'scrollPanel', 'item'] で、デフォルトのルート描画では単独の heading、searchBar、item を除外し、その後に header と scrollPanel を描画します。
デフォルト UI ブロック
| ブロック | 基盤 | 備考 |
|---|---|---|
ChatConversationList (root) | Stack | デフォルトのヘッダーとスクロールパネルを縦に積み重ねます |
ChatConversationList.Header | Stack | Heading と SearchBar の子セクションをまとめるラッパー |
ChatConversationList.Heading | Typography | 一覧のメインヘッダーを表示します |
ChatConversationList.SearchBar | TextField | debounceTime が 500 ms の検索入力とアイコン装飾付き |
ChatConversationList.ScrollPanel | Stack | 一覧項目、ローディングスピナー、空状態をマッピングするスクロール可能な表示領域 |
ChatConversationList.ConversationItem | Link + Avatar | タイトル、本文プレビュー、未読バッジ、日付を表示します |
TypeScript
import {ChatConversationList} from '@nodeblocks/frontend-chat-conversation-list-block';
import type {AvatarProps} from '@mui/material';
type Conversation = {
id: string;
title: string;
titleLines: 1 | 2;
content?: string;
dateTime?: string;
href?: string;
isSelected?: boolean;
unreadCount?: number;
avatar?: Partial<AvatarProps> & {
isLoading?: boolean;
};
};
const conversations: Conversation[] = [
{
id: 'thread-1',
title: 'Customer support',
content: 'Ticket resolved successfully.',
dateTime: '2026-05-27T02:00:00Z',
titleLines: 1,
unreadCount: 1,
avatar: {children: 'CS', sx: {bgcolor: 'primary.main'}},
},
];
<ChatConversationList
conversations={conversations}
labels={{
heading: 'Inbox',
emptyState: 'No threads active',
}}
placeholders={{
search: 'Filter threads...',
}}
search={{
onChange: val => void val,
}}
onNavigate={url => void url}
/>;