ConversationList Store

最近更新时间:2026-06-18 11:26:16

我的收藏

概述

ConversationListStore 是会话列表的数据访问与数据结构层,为上层 UI 提供统一的会话列表能力。它主要管理会话列表数据、未读消息总数、会话置顶、删除、免打扰、标记未读、草稿、清空历史消息等会话级能力。
我们将 ConversationListStore 的创建、订阅、销毁以及当前会话切换等生命周期管理封装到了 useChatContext 中。
因此,在绝大多数业务场景下,推荐直接使用 useChatContext 获取 ConversationListStore 的属性和方法,而不是手动创建或管理 ConversationListStore 实例。只有在需要独立会话列表实例、特殊分组列表、浮窗隔离、多面板隔离等场景时,才需要直接接触 ConversationListStore 的能力。

基本使用

index.tsx
style.css
import { useEffect } from 'react';
import {
ConversationMarkType,
useChatContext,
} from '@tencentcloud/chat-uikit-react';
import './styles.css';

function ConversationListBasicDemo() {
const {
activeConversation,
clearConversationUnreadCount,
conversationList,
loadConversations,
markConversation,
setActiveConversation,
totalUnreadCount,
} = useChatContext();

useEffect(() => {
loadConversations();
}, [loadConversations]);

const handleSelectConversation = async (conversationID: string) => {
setActiveConversation(conversationID);
await clearConversationUnreadCount(conversationID);
await markConversation([conversationID], ConversationMarkType.Unread, false);
};

return (
<div className="conversation-list-basic-demo">
<div className="conversation-list-basic-demo__stat">
<span>Unread Message Count</span>
<strong>{totalUnreadCount}</strong>
</div>
<div className="conversation-list-basic-demo__list">
{conversationList.map(conversation => (
<button
className={
activeConversation?.conversationID === conversation.conversationID
? 'conversation-list-basic-demo__item conversation-list-basic-demo__item--active'
: 'conversation-list-basic-demo__item'
}
key={conversation.conversationID}
onClick={() => handleSelectConversation(conversation.conversationID)}
type="button"
>
<span>{conversation.title || conversation.conversationID}</span>
<small>{conversation.conversationID}</small>
{conversation.unreadCount > 0 && (
<em>{conversation.unreadCount}</em>
)}
</button>
))}
</div>
</div>
);
}
.conversation-list-basic-demo {
height: 100%;
display: flex;
flex-direction: column;
gap: 20px;
padding: 24px;
overflow: auto;
width: 400px;
}

.conversation-list-basic-demo__stat {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 20px;
border: 1px solid #edf0f5;
border-radius: 12px;
background: #f9fafb;
}

.conversation-list-basic-demo__stat span {
color: #6b7280;
}

.conversation-list-basic-demo__stat strong {
color: #111827;
font-size: 28px;
}

.conversation-list-basic-demo__list {
display: flex;
flex-direction: column;
gap: 10px;
}

.conversation-list-basic-demo__item {
position: relative;
display: flex;
flex-direction: column;
gap: 4px;
padding: 14px 48px 14px 16px;
border: 1px solid #edf0f5;
border-radius: 12px;
background: #fff;
color: #111827;
text-align: left;
cursor: pointer;
}

.conversation-list-basic-demo__item:hover,
.conversation-list-basic-demo__item--active {
border-color: #667eea;
background: #eef2ff;
}

.conversation-list-basic-demo__item small {
color: #6b7280;
}

.conversation-list-basic-demo__item em {
position: absolute;
right: 16px;
top: 50%;
min-width: 24px;
padding: 3px 7px;
border-radius: 999px;
background: #ff4d4f;
color: #fff;
font-style: normal;
text-align: center;
transform: translateY(-50%);
}

ConversationListStore

数据

属性名
类型
说明
conversationList
ConversationInfo[] | undefined
会话列表数据。
totalUnreadCount
number
总未读数。

操作方法

方法名
类型
说明
loadConversations
(option?: ConversationLoadOption) => Promise<any>
加载会话列表,支持传入加载参数。
getConversationInfo
(conversationID: string) => Promise<ConversationInfo>
根据会话 ID 获取会话详情。
deleteConversation
(conversationID: string) => Promise<any>
删除指定会话。
pinConversation
(conversationID: string, pin: boolean) => Promise<any>
设置或取消指定会话置顶。
markConversation
(conversationIDList: string[], markType: ConversationMarkType, enable: boolean) => Promise<any>
对一组会话设置或取消指定标记。
setReceiveMessageOpt
(conversationID: string, opt: ReceiveMessageOption) => Promise<any>
设置指定会话的消息接收选项。
setConversationDraft
(conversationID: string, draft: string) => Promise<any>
设置指定会话的草稿内容。
clearConversationMessages
(conversationID: string) => Promise<any>
清空指定会话的消息记录。
clearConversationUnreadCount
(conversationID: string) => Promise<any>
清空指定会话的未读数。

使用示例

这个示例使用 ConversationListStore 的基础会话列表能力:加载会话、展示未读总数、选中会话并清空未读、置顶/取消置顶、发起 C2C 会话。
index.tsx
index.css
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
ConversationListStore,
ConversationMarkType,
type ConversationInfo,
} from '@tencentcloud/chat-uikit-react';
import './styles.css';

function formatTime(conversation: ConversationInfo) {
const timestamp = conversation.lastMessage?.timestamp;
if (!timestamp) {
return '';
}
return timestamp.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
});
}

function getLastMessageText(conversation: ConversationInfo) {
const payload = conversation.lastMessage?.messagePayload;
if (payload && 'text' in payload && typeof payload.text === 'string') {
return payload.text;
}
return conversation.draft ? `[Draft] ${conversation.draft}` : 'No messages yet';
}

function ConversationListStoreBasicDemo() {
const {
clearConversationUnreadCount,
conversationList,
getConversationInfo,
hasMoreConversations,
loadConversations,
loadMoreConversations,
markConversation,
pinConversation,
totalUnreadCount,
} = ConversationListStore.create();

const [activeConversationID, setActiveConversationID] = useState('');
const [loading, setLoading] = useState(false);
const [actionError, setActionError] = useState('');

const activeConversation = useMemo(() => {
return conversationList.find(conversation => conversation.conversationID === activeConversationID);
}, [activeConversationID, conversationList]);

const handleLoadConversations = useCallback(async () => {
setLoading(true);
setActionError('');
try {
await loadConversations();
} catch (error) {
setActionError(error instanceof Error ? error.message : 'Failed to load conversations');
} finally {
setLoading(false);
}
}, [loadConversations]);

useEffect(() => {
void handleLoadConversations();
}, [handleLoadConversations]);

const handleSelectConversation = async (conversationID: string) => {
setActiveConversationID(conversationID);
setActionError('');
try {
await clearConversationUnreadCount(conversationID);
await markConversation([conversationID], ConversationMarkType.Unread, false);
} catch (error) {
setActionError(error instanceof Error ? error.message : 'Failed to select conversation');
}
};

const handleTogglePin = async (conversation: ConversationInfo) => {
setActionError('');
try {
await pinConversation(conversation.conversationID, !conversation.isPinned);
} catch (error) {
setActionError(error instanceof Error ? error.message : 'Failed to update pin status');
}
};

const handleLoadMore = async () => {
setActionError('');
try {
await loadMoreConversations();
} catch (error) {
setActionError(error instanceof Error ? error.message : 'Failed to load more conversations');
}
};

const handleStartC2CConversation = async () => {
const normalizedUserID = window.prompt('Enter user ID')?.trim();
if (!normalizedUserID) {
return;
}

setActionError('');
try {
const conversation = await getConversationInfo(`C2C${normalizedUserID}`);
setActiveConversationID(conversation.conversationID);
await loadConversations();
} catch (error) {
setActionError(error instanceof Error ? error.message : 'Failed to start C2C conversation');
}
};

return (
<div className="conversation-list-store-basic-demo">
<div className="conversation-list-store-basic-demo__header">
<div>
<h2>ConversationListStore Basic</h2>
<p>Use ConversationListStore.create() to build a custom conversation list.</p>
</div>
<div className="conversation-list-store-basic-demo__actions">
<button
className="conversation-list-store-basic-demo__refresh"
onClick={handleStartC2CConversation}
type="button"
>
Start C2C
</button>
<button
className="conversation-list-store-basic-demo__refresh"
disabled={loading}
onClick={handleLoadConversations}
type="button"
>
{loading ? 'Loading...' : 'Refresh'}
</button>
</div>
</div>

<div className="conversation-list-store-basic-demo__stats">
<span>Total unread</span>
<strong>{totalUnreadCount}</strong>
</div>

{actionError && (
<div className="conversation-list-store-basic-demo__error">{actionError}</div>
)}

<div className="conversation-list-store-basic-demo__layout">
<div className="conversation-list-store-basic-demo__list">
{conversationList.map(conversation => (
<article
className={
activeConversationID === conversation.conversationID
? 'conversation-list-store-basic-demo__item conversation-list-store-basic-demo__item--active'
: 'conversation-list-store-basic-demo__item'
}
key={conversation.conversationID}
>
<button
className="conversation-list-store-basic-demo__main"
onClick={() => handleSelectConversation(conversation.conversationID)}
type="button"
>
<span className="conversation-list-store-basic-demo__avatar">
{(conversation.title || conversation.conversationID).slice(0, 1).toUpperCase()}
</span>
<span className="conversation-list-store-basic-demo__content">
<span className="conversation-list-store-basic-demo__row">
<strong>{conversation.title || conversation.conversationID}</strong>
<time>{formatTime(conversation)}</time>
</span>
<span className="conversation-list-store-basic-demo__message">
{getLastMessageText(conversation)}
</span>
<small>{conversation.conversationID}</small>
</span>
</button>

<div className="conversation-list-store-basic-demo__meta">
{conversation.unreadCount > 0 && (
<em>{conversation.unreadCount}</em>
)}
<button
className={
conversation.isPinned
? 'conversation-list-store-basic-demo__pin conversation-list-store-basic-demo__pin--active'
: 'conversation-list-store-basic-demo__pin'
}
onClick={() => handleTogglePin(conversation)}
type="button"
>
{conversation.isPinned ? 'Unpin' : 'Pin'}
</button>
</div>
</article>
))}

{conversationList.length === 0 && !loading && (
<div className="conversation-list-store-basic-demo__empty">No conversations</div>
)}

{hasMoreConversations && (
<button
className="conversation-list-store-basic-demo__load-more"
onClick={handleLoadMore}
type="button"
>
Load more
</button>
)}
</div>

<aside className="conversation-list-store-basic-demo__detail">
<span>Selected conversation</span>
<strong>{activeConversation?.title || activeConversationID || 'None'}</strong>
<small>{activeConversation?.conversationID || 'Click a conversation to clear unread count.'}</small>
</aside>
</div>
</div>
);
}
.conversation-list-store-basic-demo {
height: 100%;
display: flex;
flex-direction: column;
gap: 18px;
padding: 24px;
overflow: hidden;
}

.conversation-list-store-basic-demo__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}

.conversation-list-store-basic-demo__header h2 {
margin: 0 0 8px;
color: #111827;
font-size: 22px;
}

.conversation-list-store-basic-demo__header p {
margin: 0;
color: #6b7280;
font-size: 14px;
line-height: 1.5;
}

.conversation-list-store-basic-demo__refresh,
.conversation-list-store-basic-demo__load-more,
.conversation-list-store-basic-demo__pin {
border: 1px solid #d7dce5;
border-radius: 999px;
background: #fff;
color: #374151;
cursor: pointer;
}

.conversation-list-store-basic-demo__refresh {
padding: 8px 14px;
}

.conversation-list-store-basic-demo__refresh:disabled {
cursor: not-allowed;
opacity: 0.6;
}

.conversation-list-store-basic-demo__actions {
display: flex;
gap: 8px;
}

.conversation-list-store-basic-demo__stats {
display: flex;
align-items: center;
justify-content: space-between;
width: 360px;
padding: 16px 18px;
border: 1px solid #edf0f5;
border-radius: 12px;
background: #f9fafb;
}

.conversation-list-store-basic-demo__stats span {
color: #6b7280;
}

.conversation-list-store-basic-demo__stats strong {
color: #111827;
font-size: 26px;
}

.conversation-list-store-basic-demo__error {
width: 360px;
padding: 10px 12px;
border: 1px solid #fecaca;
border-radius: 10px;
background: #fef2f2;
color: #b91c1c;
font-size: 13px;
}

.conversation-list-store-basic-demo__layout {
flex: 1;
min-height: 0;
display: flex;
gap: 18px;
}

.conversation-list-store-basic-demo__list {
width: 420px;
display: flex;
flex-direction: column;
gap: 10px;
overflow: auto;
}

.conversation-list-store-basic-demo__item {
position: relative;
display: flex;
align-items: center;
gap: 10px;
padding: 12px;
border: 1px solid #edf0f5;
border-radius: 14px;
background: #fff;
}

.conversation-list-store-basic-demo__item:hover,
.conversation-list-store-basic-demo__item--active {
border-color: #667eea;
background: #eef2ff;
}

.conversation-list-store-basic-demo__main {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 12px;
padding: 0;
border: 0;
background: transparent;
color: inherit;
text-align: left;
cursor: pointer;
}

.conversation-list-store-basic-demo__avatar {
width: 40px;
height: 40px;
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 12px;
background: #667eea;
color: #fff;
font-weight: 700;
}

.conversation-list-store-basic-demo__content {
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}

.conversation-list-store-basic-demo__row {
display: flex;
align-items: center;
gap: 10px;
}

.conversation-list-store-basic-demo__row strong,
.conversation-list-store-basic-demo__message,
.conversation-list-store-basic-demo__content small {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.conversation-list-store-basic-demo__row strong {
color: #111827;
font-size: 14px;
}

.conversation-list-store-basic-demo__row time,
.conversation-list-store-basic-demo__content small {
color: #9ca3af;
font-size: 12px;
}

.conversation-list-store-basic-demo__message {
color: #6b7280;
font-size: 13px;
}

.conversation-list-store-basic-demo__meta {
flex: 0 0 auto;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8px;
}

.conversation-list-store-basic-demo__meta em {
min-width: 22px;
padding: 2px 7px;
border-radius: 999px;
background: #ff4d4f;
color: #fff;
font-size: 12px;
font-style: normal;
text-align: center;
}

.conversation-list-store-basic-demo__pin {
padding: 4px 9px;
font-size: 12px;
}

.conversation-list-store-basic-demo__pin--active {
border-color: #667eea;
background: #667eea;
color: #fff;
}

.conversation-list-store-basic-demo__empty,
.conversation-list-store-basic-demo__detail {
border: 1px dashed #d7dce5;
border-radius: 14px;
color: #6b7280;
}

.conversation-list-store-basic-demo__empty {
padding: 28px;
text-align: center;
}

.conversation-list-store-basic-demo__load-more {
padding: 10px 14px;
}

.conversation-list-store-basic-demo__detail {
width: 260px;
height: fit-content;
display: flex;
flex-direction: column;
gap: 8px;
padding: 18px;
background: #f9fafb;
}

.conversation-list-store-basic-demo__detail span {
color: #6b7280;
font-size: 13px;
}

.conversation-list-store-basic-demo__detail strong {
color: #111827;
font-size: 18px;
}

.conversation-list-store-basic-demo__detail small {
color: #6b7280;
line-height: 1.5;
}

@media (max-width: 768px) {
.conversation-list-store-basic-demo {
overflow: auto;
}

.conversation-list-store-basic-demo__header,
.conversation-list-store-basic-demo__layout {
flex-direction: column;
}

.conversation-list-store-basic-demo__stats,
.conversation-list-store-basic-demo__error,
.conversation-list-store-basic-demo__list,
.conversation-list-store-basic-demo__detail {
width: 100%;
}
}