概述
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 后关闭提示并滚动到最新消息。
支持只渲染文本消息:过滤掉非文本消息,布局更接近论坛式消息流。
支持基础消息元信息展示:展示发送者、时间和正文。
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><divclassName="message-list-basic-demo__list"onScroll={handleScroll}ref={listRef}>{textMessages.map(message => (<divclassName="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"><inputonChange={event => setInputConversationID(event.target.value)}placeholder="C2CuserID or GROUPgroupID"value={inputConversationID}/><buttononClick={() => 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]);// 略}