MessageList Store

最近更新时间:2026-07-01 16:34:31

我的收藏

概述

MessageListStore 是消息列表的数据访问与数据结构层,为上层 UI 提供统一的消息列表能力。它主要管理当前会话的消息列表数据、历史消息加载、消息收发后的列表更新、消息状态变化、消息撤回、消息已读回执、消息定位、清空历史消息后的同步等消息级能力。
我们将 MessageListStore 的创建、订阅、销毁以及当前会话切换后的消息列表更新等生命周期管理封装到了 useChatContext 中。
因此,在绝大多数业务场景下,推荐直接使用 useChatContext 获取 MessageListStore 的属性和方法,而不是手动创建或管理 MessageListStore 实例。只有在需要独立消息列表实例、特殊会话消息面板、浮窗隔离、多面板隔离、或自定义消息加载流程等场景时,才需要直接接触 MessageListStore 的能力。

MessageListStore

属性

属性名
类型
说明
messageList
MessageInfo[]
当前会话的消息列表数据。
hasOlderMessages
boolean
是否还有更早的历史消息可加载。
hasNewerMessages
boolean
是否还有更新方向的消息可加载,常用于消息定位后继续向后加载。
pinnedMessageList
MessageInfo[]
当前会话的置顶消息列表。

方法

方法名
类型
说明
loadMessages
(option?: MessageLoadOption) => Promise<any>
加载当前会话的消息列表,支持按方向、游标、数量、消息类型等参数加载。适用于初始化消息列表以及跳转到一个久远的消息片段。
loadOlderMessages
() => Promise<any>
加载历史消息。
loadNewerMessages
() => Promise<any>
加载时间上新的消息,常用于消息定位后的后续加载。
sendMessageReadReceipts
(messages: MessageInfo[]) => Promise<any>
发送指定消息的已读回执。
deleteMessages
(messages: MessageInfo[]) => Promise<any>
删除指定消息,并同步更新本地消息列表状态。
forwardMessages
(messages: MessageInfo[], option: ForwardMessageOption, conversationID: string) => Promise<any>
将一组消息转发到指定会话,支持逐条转发或合并转发。

使用示例

以下是一个完整的 MessageList 代码示例,展示了如何使用 MessageListStore 来构建一个具有分页加载功能的文本消息列表:
支持按会话 ID 加载消息列表:输入 C2Cxxx 或 GROUPxxx 后,通过 MessageListStore.create(conversationID) 获取当前会话消息(推荐通过 useChatContext 获取)。
支持历史消息分页加载:滚动到顶部时触发 loadOlderMessages()。
支持加载更多后保持滚动位置:加载历史消息后,不会把用户当前位置顶走。
支持新消息自动滚动:用户在列表底部时,新消息到达会自动滚到底部。
支持历史阅读状态识别:用户离开底部时标记为 Viewing history。
支持新消息非打断提醒:用户正在看历史消息时,新消息到达会用原生非阻塞 <dialog> 提示。
支持一键回到底部:点击 Go to latest 后关闭提示并滚动到最新消息。
支持只渲染文本消息:过滤掉非文本消息,布局更接近论坛式消息流。
支持基础消息元信息展示:展示发送者、时间和正文。
index.tsx
styles.css
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
import {
useChatContext,
MessageType,
MessageListStore,
type MessageInfo,
type TextMessagePayload,
} from '@tencentcloud/chat-uikit-react';
import './styles.css';

const NEAR_BOTTOM = 40;

function getMessageText(message: MessageInfo) {
const payload = (message.messagePayload || {}) as TextMessagePayload;
if (
message.messageType === MessageType.Text
&& 'text' in payload
&& typeof payload.text === 'string'
) {
return payload.text;
}
return '';
}

function BasicMessageList({ conversationID }: { conversationID: string }) {
const {
hasOlderMessages,
loadMessages,
loadOlderMessages,
messageList,
setActiveConversation,
messageListOnEvent,
} = useChatContext();

const listRef = useRef<HTMLDivElement>(null);
const keepScrollRef = useRef<number | null>(null);
const isNearBottomRef = useRef(true);
const newMessageDialogRef = useRef<HTMLDialogElement>(null);
const [isViewingHistory, setIsViewingHistory] = useState(false);
const [showNewMessageToast, setShowNewMessageToast] = useState(false);
const textMessages = messageList.filter(message => getMessageText(message));
useEffect(() => {
setActiveConversation(conversationID);
}, [conversationID]);

const scrollToBottom = () => {
const list = listRef.current;
if (!list) return;
list.scrollTop = list.scrollHeight;
};

const updateScrollState = () => {
const list = listRef.current;
if (!list) return;
const distanceToBottom = list.scrollHeight - list.scrollTop - list.clientHeight;
const isNearBottom = distanceToBottom < NEAR_BOTTOM;
isNearBottomRef.current = isNearBottom;
setIsViewingHistory(!isNearBottom);
};

useEffect(() => {
loadMessages().then(() => requestAnimationFrame(scrollToBottom));
}, [conversationID, loadMessages]);

useEffect(() => {
return onEvent((event) => {
if (event.type !== 'onReceiveNewMessage') return;
if (isNearBottomRef.current) {
requestAnimationFrame(scrollToBottom);
return;
}
setShowNewMessageToast(true);
});
}, [onEvent]);

useEffect(() => {
const dialog = newMessageDialogRef.current;
if (!dialog) return;
if (showNewMessageToast && !dialog.open) {
dialog.show();
}
if (!showNewMessageToast && dialog.open) {
dialog.close();
}
}, [showNewMessageToast]);

useLayoutEffect(() => {
const list = listRef.current;
if (!list) return;

if (keepScrollRef.current !== null) {
list.scrollTop = list.scrollHeight - keepScrollRef.current;
keepScrollRef.current = null;
return;
}

if (isNearBottomRef.current) {
scrollToBottom();
}
}, [messageList]);

const handleScroll = () => {
const list = listRef.current;
if (!list) return;
updateScrollState();
if (list.scrollTop > 0 || !hasOlderMessages) return;

keepScrollRef.current = list.scrollHeight;
loadOlderMessages();
};

const handleReadNewMessage = () => {
setShowNewMessageToast(false);
scrollToBottom();
};

return (
<div className="message-list-basic-demo__body">
<div className="message-list-basic-demo__status">
{isViewingHistory ? 'Viewing history' : 'At latest messages'}
</div>
<div
className="message-list-basic-demo__list"
onScroll={handleScroll}
ref={listRef}
>
{textMessages.map(message => (
<div
className="message-list-basic-demo__post"
key={message.msgID}
>
<div className="message-list-basic-demo__post-meta">
<strong>{message.from?.nickname || message.from?.userID || (message.isSentBySelf ? 'Me' : 'Unknown')}</strong>
<time>{message.timestamp?.toLocaleString?.() || message.msgID}</time>
</div>
<p>{getMessageText(message)}</p>
</div>
))}
</div>
<dialog className="message-list-basic-demo__dialog" ref={newMessageDialogRef}>
<span>New message received.</span>
<button onClick={handleReadNewMessage} type="button">
Go to latest
</button>
</dialog>
</div>
);
}

function MessageListBasicDemo() {
const [inputConversationID, setInputConversationID] = useState('');
const [conversationID, setConversationID] = useState('');

return (
<div className="message-list-basic-demo">
<div className="message-list-basic-demo__header">
<h2>MessageList Basic</h2>
<p>Basic message list with scroll loading and new-message toast.</p>
</div>

<div className="message-list-basic-demo__toolbar">
<input
onChange={event => setInputConversationID(event.target.value)}
placeholder="C2CuserID or GROUPgroupID"
value={inputConversationID}
/>
<button
onClick={() => setConversationID(inputConversationID.trim())}
type="button"
>
Load Messages
</button>
</div>

{conversationID
? <BasicMessageList conversationID={conversationID} />
: <div className="message-list-basic-demo__empty">Enter a conversationID first.</div>}
</div>
);
}

.message-list-basic-demo {
height: 100%;
display: flex;
flex-direction: column;
gap: 16px;
padding: 24px;
overflow: hidden;
}

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

.message-list-basic-demo__header p {
margin: 0;
color: #6b7280;
font-size: 14px;
}

.message-list-basic-demo__toolbar {
display: flex;
gap: 8px;
}

.message-list-basic-demo__toolbar input {
width: 260px;
padding: 8px 10px;
border: 1px solid #d7dce5;
border-radius: 8px;
}

.message-list-basic-demo__toolbar button {
padding: 8px 12px;
border: 1px solid #667eea;
border-radius: 8px;
background: #667eea;
color: #fff;
cursor: pointer;
}

.message-list-basic-demo__body {
position: relative;
flex: 1;
min-height: 0;
width: min(760px, 100%);
display: flex;
flex-direction: column;
border: 1px solid #edf0f5;
border-radius: 12px;
overflow: hidden;
}

.message-list-basic-demo__status {
padding: 8px 12px;
border-bottom: 1px solid #edf0f5;
background: #f9fafb;
color: #6b7280;
font-size: 12px;
}

.message-list-basic-demo__list {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px;
overflow: auto;
}

.message-list-basic-demo__post {
display: flex;
flex-direction: column;
gap: 8px;
padding: 14px 16px;
border: 1px solid #edf0f5;
border-radius: 12px;
background: #fff;
color: #111827;
}

.message-list-basic-demo__post-meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}

.message-list-basic-demo__post-meta strong {
color: #374151;
font-size: 13px;
}

.message-list-basic-demo__post-meta time {
color: #9ca3af;
font-size: 12px;
}

.message-list-basic-demo__post p {
margin: 0;
color: #111827;
font-size: 14px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
}

.message-list-basic-demo__empty {
width: 420px;
padding: 32px;
border: 1px dashed #d7dce5;
border-radius: 12px;
color: #6b7280;
text-align: center;
}

.message-list-basic-demo__dialog {
z-index: 1000;
padding: 14px 16px;
border: 1px solid #d7dce5;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(15, 23, 42, 0.24);
}

.message-list-basic-demo__dialog[open] {
display: flex;
align-items: center;
gap: 12px;
}

.message-list-basic-demo__dialog button {
padding: 6px 10px;
border: 1px solid #667eea;
border-radius: 8px;
background: #667eea;
color: #fff;
cursor: pointer;
}
替换为使用 MessageListStore:
function BasicMessageList({ conversationID }: { conversationID: string }) {
const {
hasOlderMessages,
loadMessages,
loadOlderMessages,
messageList,
onEvent,
} = MessageListStore.create(conversationID);
// useEffect(() => {
// setActiveConversation(conversationID);
// }, [conversationID]);
// 略
}

相关文档

交流与反馈

如遇任何问题,可联系 官网售后 反馈,享有专业工程师的支持,解决您的难题。