Chat Conversation List Block
The Chat Conversation List Component is a fully customizable and accessible conversation listing interface built with React and TypeScript. It provides a complete conversation overview with search functionality, infinite scrolling, and flexible customization options for chat application interfaces.
π Installationβ
npm install @nodeblocks/frontend-chat-conversation-list-block@0.2.0
π Usageβ
import {ChatConversationList} from '@nodeblocks/frontend-chat-conversation-list-block';
- Basic Usage
- Advanced Usage
function SimpleChatConversationList() { const [conversations] = useState([ { id: 'conv-001', title: 'Customer Support', content: 'Thank you for contacting us. How can we help you today?', dateTime: '2025-02-01T10:00:00Z', titleLines: 1, unreadCount: 3, href: '/chat/conv-001', avatar: { sx: {bgcolor: 'primary.main'}, }, }, { id: 'conv-002', title: 'Project Team Discussion', content: 'The new feature implementation is ready for review.', dateTime: '2025-02-01T09:30:00Z', titleLines: 2, unreadCount: 1, href: '/chat/conv-002', avatar: { sx: {bgcolor: 'secondary.main'}, }, }, { id: 'conv-003', title: 'Marketing Campaign', content: "Let's schedule a meeting to discuss the campaign strategy.", dateTime: '2025-01-31T16:45:00Z', titleLines: 1, href: '/chat/conv-003', avatar: { sx: {bgcolor: 'info.main'}, }, }, ]); return ( <ChatConversationList conversations={conversations} labels={{ heading: 'Conversations', emptyState: 'No conversations yet', }} placeholders={{ search: 'Search conversations...', }} search={{ defaultValue: '', onChange: (value) => { console.log('Search changed:', value); }, }} onNavigate={(url) => { console.log('Navigate to:', url); }} onScrollBottom={() => { console.log('Load more conversations'); }} isLoading={false} > <ChatConversationList.Heading /> <ChatConversationList.SearchBar /> <ChatConversationList.ScrollPanel /> </ChatConversationList> ); }
function AdvancedChatConversationList() { const [conversations] = useState([ { id: 'support-001', title: 'Customer Support Team', content: 'Your issue has been resolved. Is there anything else we can help you with?', 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: 'Development Team Discussion', content: 'The code review is complete. Ready to merge the new feature branch.', 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: 'Client Project Updates', content: 'Meeting scheduled for tomorrow at 2 PM to discuss project timeline.', 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: 'Team Conversations', emptyState: 'No conversations match your search', }} placeholders={{ search: 'Search by name or message...', }} search={{ defaultValue: '', onChange: (value) => { console.log('Search changed:', value); }, }} onNavigate={(url) => { console.log('Navigate to:', url); }} onScrollBottom={() => { console.log('Load more conversations'); }} 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} conversations</span> <span>π {conversations.reduce((acc, conv) => acc + (conv.unreadCount || 0), 0)} unread</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', }} > β Start New Conversation </button> </div> ), }, blockOrder: ['heading', 'searchBar', 'statsBar', 'scrollPanel', 'footer'], })} </ChatConversationList> ); }
π§ Props Referenceβ
Main Component Propsβ
| Prop | Type | Default | Description |
|---|---|---|---|
conversations | Conversation[] | Required | Array of conversation objects to display |
labels | {heading?: string; emptyState?: string;} | undefined | Text labels configuration |
placeholders | {search?: string;} | undefined | Placeholder text configuration |
isLoading | boolean | undefined | Show loading state |
onNavigate | (url: string) => void | Required | Navigation callback (required for href) |
onScrollBottom | () => void | undefined | Callback when the bottom of the list is reached |
search | {defaultValue?: string; onChange?: (value: string) => void;} | undefined | Search input configuration |
size | 'compact' | 'normal' | undefined | Configures text size and spacing (sub-components default to 'compact') |
className | string | undefined | Additional CSS class name for styling |
sx | SxProps<Theme> | undefined | MUI system props for custom styling |
children | BlocksOverride | undefined | Custom block components to override default rendering |
Note: This component inherits all MUI Stack component props.
Conversation Objectβ
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | Required | Unique identifier for the conversation |
title | string | Required | Title of the conversation |
titleLines | 1 | 2 | Required | Number of lines before the title is cut off |
content | string | undefined | Conversation body (will be cut off after one line) |
dateTime | string | undefined | Date time as ISO string (formatted using date-fns with Japanese locale) |
unreadCount | number | undefined | Unread count (>99 will be displayed as "99...") |
href | string | undefined | Makes conversation linkable and adds hover state |
isSelected | boolean | undefined | When true, highlight the conversation |
avatar | Partial<AvatarProps> & {isLoading?: boolean} | undefined | MUI Avatar props with optional loading state |
Sub-Componentsβ
The ChatConversationList component provides several sub-components. All sub-components receive their default values from the main component's context and can override these values through props.
ChatConversationList.Headingβ
Renders the conversation list heading using MUI Typography.
| Prop | Type | Default | Description |
|---|---|---|---|
labels | {heading?: string;} | Context values | Labels configuration (overrides context) |
size | 'compact' | 'normal' | 'compact' | Size variant (overrides context) |
className | string | undefined | Additional CSS class name for styling |
children | ReactNode | Default heading content | Custom content to override default heading |
component | ElementType | 'h1' | HTML element to render |
variant | TypographyVariant | 'h4' | MUI Typography variant |
sx | SxProps<Theme> | undefined | MUI system props for custom styling |
Note: This component inherits all MUI Typography component props.
ChatConversationList.SearchBarβ
Renders the search input with debounced search functionality.
| Prop | Type | Default | Description |
|---|---|---|---|
search | {defaultValue?: string; onChange?: (value: string) => void;} | Context values | Search configuration (overrides context) |
placeholders | {search?: string;} | Context values | Placeholder configuration (overrides context) |
conversations | Conversation[] | Context values | Conversations to determine visibility (overrides context) |
size | 'compact' | 'normal' | 'compact' | Size variant (overrides context) |
debounceTime | number | 500 | Delay in milliseconds before triggering search onChange |
className | string | undefined | Additional CSS class name for styling |
defaultValue | string | undefined | Initial value for the search input |
placeholder | string | undefined | Placeholder text when empty |
variant | TextFieldVariant | 'outlined' | MUI TextField variant |
type | string | 'search' | HTML input type |
fullWidth | boolean | true | Whether input takes full width (ignored when size='normal') |
sx | SxProps<Theme> | undefined | MUI system props for custom styling |
Note: This component inherits all MUI TextField component props (except onChange and size).
ChatConversationList.Headerβ
A convenience component that combines Heading and SearchBar with optional divider.
| Prop | Type | Default | Description |
|---|---|---|---|
size | 'compact' | 'normal' | Context values | Size variant (overrides context) |
className | string | undefined | Additional CSS class name for styling |
children | ReactNode | Default header content | Custom content to override default header |
sx | SxProps<Theme> | undefined | MUI system props for custom styling |
Note: This component inherits all MUI Stack component props (except direction).
ChatConversationList.ScrollPanelβ
Renders the scrollable list of conversations with infinite scroll support.
| Prop | Type | Default | Description |
|---|---|---|---|
conversations | Conversation[] | Context values | Conversations array (overrides context) |
labels | {heading?: string; emptyState?: string;} | Context values | Labels configuration (overrides context) |
isLoading | boolean | Context values | Loading state (overrides context) |
onScrollBottom | () => void | Context values | Scroll callback (overrides context) |
size | 'compact' | 'normal' | 'compact' | Size variant (overrides context) |
className | string | undefined | Additional CSS class name for styling |
children | ReactNode | Default conversation items | Custom content to override default rendering |
sx | SxProps<Theme> | undefined | MUI system props for custom styling |
Note: This component inherits all MUI Stack component props (except direction and spacing).
π¨ Configuration examplesβ
The component uses MUI Avatar props for avatar customization. Here's how to configure avatars:
const conversation = {
id: 'conv-001',
title: 'Support Team',
titleLines: 1,
avatar: {
// MUI Avatar props
src: '/path/to/image.jpg', // Image source
srcSet: '/path/to/image@2x.jpg 2x', // Responsive images
sx: { bgcolor: 'primary.main' }, // Custom styling
children: 'ST', // Fallback content (initials)
// Extended props
isLoading: false, // Show loading placeholder
},
};
π§ TypeScript Supportβ
Full TypeScript support with comprehensive type definitions:
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 {
/** Unique identifier for the conversation */
id: string;
/** Title of the conversation */
title: string;
/** Amount of lines before the title is cut off */
titleLines: 1 | 2;
/** Conversation body (will be cut off after one line) */
content?: string;
/** Date time as ISO string (formatted using date-fns) */
dateTime?: string;
/** Unread count (>99 will be cut off at 99) */
unreadCount?: number;
/** Makes conversation linkable and adds hover state */
href?: string;
/** When true, highlight the conversation */
isSelected?: boolean;
/** MUI Avatar props with optional loading state */
avatar?: Partial<AvatarProps> & {
/** Show a temporary loading state */
isLoading?: boolean;
};
}
// Complete typed example with search and pagination
function TypedChatConversationList() {
const [conversations, setConversations] = useState<Conversation[]>([
{
id: 'support-001',
title: 'Customer Support Team',
content: 'Your issue has been resolved. Is there anything else we can help you with?',
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: 'Development Team Discussion',
content: 'The code review is complete. Ready to merge the new feature branch.',
dateTime: '2025-02-01T13:15:00Z',
titleLines: 2,
unreadCount: 5,
href: '/chat/team-002',
isSelected: true,
avatar: {
sx: {bgcolor: 'secondary.main'},
},
},
]);
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('Loading more conversations...');
setIsLoading(true);
setTimeout(() => {
const newConversations: Conversation[] = [
{
id: `conv-${Date.now()}`,
title: 'New Conversation',
content: 'This is a newly loaded conversation.',
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('Navigating to:', url);
setConversations(prev =>
prev.map(conv => ({
...conv,
isSelected: conv.href === url,
})),
);
};
return (
<ChatConversationList
conversations={filteredConversations}
labels={{
heading: 'Chat Conversations',
emptyState: searchValue ? 'No conversations match your search' : 'No conversations yet',
}}
placeholders={{
search: 'Search by name or message...',
}}
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>
);
}
π Notesβ
- Dates are formatted using the
date-fnslibrary with Japanese locale (ja) showing relative time (e.g., "5εε") - The search input is debounced by default (500ms) to prevent excessive callbacks
- Special characters in search input are automatically sanitized
- The search bar only appears when there are conversations or when the user has interacted with search
- When
size='normal'is used, the layout adjusts for a wider display with different spacing - Infinite scroll is implemented using IntersectionObserver for efficient detection
- Selected conversations are highlighted with a light blue background (
#e0f3fc)
Built with β€οΈ using React, TypeScript, MUI, and date-fns.