互动消息聊天指南
本文介绍互动消息相关功能的使用方法,包括消息收发、历史消息、消息撤回和私聊。
发送消息
文本消息
typescript
const result = await classroom.sendTextMessage('大家好!');
if (!result.ok) {
console.error('发送失败:', result.message);
}图片消息
typescript
// 选择文件后发送(自动上传 COS)
const fileInput = document.querySelector<HTMLInputElement>('#image-input');
const file = fileInput?.files?.[0];
if (file) {
const result = await classroom.sendImageMessage(file);
if (!result.ok) console.error('图片发送失败:', result.message);
}文件消息
typescript
const file = fileInput.files[0];
const result = await classroom.sendFileMessage(file);
if (!result.ok) {
console.error('文件发送失败:', result.message);
}自定义消息
typescript
// 发送自定义消息(content 会被序列化为 JSON 字符串)
const result = await classroom.sendCustomMessage({
type: 'emoji',
emojiId: 'thumbs_up',
});私聊消息
typescript
// 发送定向私聊消息(仅目标用户可见)
const result = await classroom.sendDirectedMessage('user_002', '你好,有问题想请教');
if (!result.ok) {
console.error('私聊发送失败:', result.message);
}接收消息
通过响应式状态订阅消息列表
订阅完整消息列表(包含历史消息 + 实时新消息)
typescript
classroom.state.messageList$.subscribe(messages => {
renderMessageList(messages);
});通过事件监听新消息
监听新消息(用于触发通知/滚动等 UI 操作)
typescript
import { TEvent } from '@tencent-classroom/sdk';
classroom.on(TEvent.RECV_MESSAGE, (message) => {
console.log('新消息:', message.content);
scrollToBottom();
showNotification(message);
});解析展示私聊消息标识
messageList$ 中包含公聊消息 + 与自己相关的私聊消息。
私聊消息通过 message.ext(即 IM 的 cloudCustomData)中的字段标识:
typescript
interface MessageExt {
IsPrivateMsg?: boolean; // 是否为私聊消息
PrivateInfo?: {
From: { ID: string }; // 发送者 userId
To: { ID: string }; // 接收者 userId
};
}解析示例:
typescript
classroom.state.messageList$.subscribe((messages) => {
for (const msg of messages) {
// 解析 ext 字段
const ext = typeof msg.ext === 'string' ? JSON.parse(msg.ext || '{}') : (msg.ext || {});
if (ext.IsPrivateMsg) {
// 这是一条私聊消息
const fromId = ext.PrivateInfo?.From?.ID;
const toId = ext.PrivateInfo?.To?.ID;
renderPrivateMessage(msg, fromId, toId);
} else {
// 普通公聊消息
renderPublicMessage(msg);
}
}
});messageList$ 的过滤规则
SDK 内部已确保 messageList$ 只包含:
- 所有公聊消息(Text / Image / File 类型)
- 与当前用户相关的私聊消息(自己是发送者 From 或接收者 To)
其他用户之间的私聊消息不会出现在列表中,业务层无需再做可见性判断。
历史消息
SDK 提供三个互补的历史消息 API,覆盖向上分页、向下渐进分页、一键回到最新三种场景。进房后 SDK 会自动加载课堂历史,业务层无需手动触发。
| API | 实现路径 | 用途 |
|---|---|---|
loadOlderMessages(count) | TIM SDK getMessageListHopping(direction=0) | 向上分页加载更早的消息 |
loadNewerMessages(count) | TIM SDK getMessageListHopping(direction=1) | 向下渐进分页加载更新消息(适合滚到底部继续浏览) |
seekToLatestMessages() | TIM SDK getMessageList 循环拉取约 100 条 + 整批 replace messageList$ | 一键回到最新(适合点击"↓ N 条新消息"按钮) |
messageList$内置 500 条上限,触发双向裁剪:
- append(实时 /
loadNewerMessages)超限时从头部裁剪 →state.hasOlderMessages$=true- prepend(
loadOlderMessages)超限时从尾部裁剪 →state.hasNewerMessages$=true(进入"历史浏览"模式)
双向滑动窗口状态
| 状态 | 类型 | 含义 |
|---|---|---|
state.hasOlderMessages$ | boolean | 是否还有更早消息可加载(向上分页锚点) |
state.hasNewerMessages$ | boolean | 是否处于"历史浏览"模式(实时消息会被静默吸收) |
state.newerMessagesCount$ | number | 历史浏览态期间被吸收的新消息数(不含 Tips) |
历史浏览态下的发送行为
hasNewerMessages$=true 时,5 个用户主动发送 API(sendTextMessage / sendImageMessage / sendFileMessage / sendCustomMessage / sendDirectedMessage)会先自动 await seekToLatestMessages() 再插入乐观消息,确保自己的消息追加到与"实时尾"一致的列表。
向上分页(loadOlderMessages)
typescript
// UI 滚动到顶部时拉取更早的消息(基于 messageList$ 头部最老消息的 seq)
const result = await classroom.loadOlderMessages(15);
if (result.ok && result.data.length === 0) {
console.log('已无更早消息');
}调用条件
loadOlderMessages 需要 messageList$ 非空才能取到 seq 锚点;进房后 SDK 已自动加载初始历史,无需业务层干预。
向下渐进分页(loadNewerMessages)
typescript
// UI 滚动到底部时调用(基于 messageList$ 末尾最新消息的 seq),
// 让用户继续向下浏览被裁掉的更新消息,渐进追上实时尾。
const result = await classroom.loadNewerMessages(15);
if (result.ok && result.data.length === 0) {
console.log('已是最新');
}一键回到最新(seekToLatestMessages)
typescript
// 用户点击"↓ N 条新消息"浮动按钮时调用。
// SDK 循环 getMessageList 拉取约 100 条最新消息后整批 replace messageList$,
// 同时清零 hasNewerMessages$ / newerMessagesCount$。
if (classroom.state.hasNewerMessages$.get()) {
const r = await classroom.seekToLatestMessages();
if (r.ok) scrollChatToBottom();
}滚动驱动的完整示例
typescript
const msgListEl = document.querySelector('.chat-list')!;
let loadingOlder = false;
let loadingNewer = false;
msgListEl.addEventListener('scroll', async () => {
// 滚到顶部:分页加载更早消息(仅在仍可能有更早时尝试)
if (msgListEl.scrollTop <= 0 && !loadingOlder && classroom.state.hasOlderMessages$.get()) {
loadingOlder = true;
const prevHeight = msgListEl.scrollHeight;
const res = await classroom.loadOlderMessages(15);
loadingOlder = false;
if (res.ok) {
// 还原 scrollTop,避免视觉跳动
msgListEl.scrollTop = msgListEl.scrollHeight - prevHeight;
}
}
// 滚到底部 + 处于历史浏览态:渐进追上实时尾
const distanceToBottom = msgListEl.scrollHeight - msgListEl.clientHeight - msgListEl.scrollTop;
if (distanceToBottom <= 8 && !loadingNewer && classroom.state.hasNewerMessages$.get()) {
loadingNewer = true;
await classroom.loadNewerMessages(15);
loadingNewer = false;
}
});
// 浮动"↓ N 条新消息"按钮:一键回到最新
classroom.state.hasNewerMessages$.subscribe((show) => {
toggleBackToLatestButton(show, classroom.state.newerMessagesCount$.get());
});
classroom.state.newerMessagesCount$.subscribe((n) => updateBackToLatestBadge(n));
backToLatestButton.addEventListener('click', () => classroom.seekToLatestMessages());消息撤回
typescript
// 撤回自己的消息(所有角色可撤回自己的消息)
const result = await classroom.revokeMessage(message.id);
if (!result.ok) {
console.error('撤回失败:', result.message);
}
// 老师/助教撤回他人消息(按 IM 序列号)
await classroom.revokeClassMessage(message.seq);监听消息撤回事件:
typescript
classroom.on(TEvent.MESSAGE_REVOKED, ({ msgId }) => {
console.log('消息已被撤回:', msgId);
});未读管理
typescript
// 订阅未读消息数
classroom.state.messageUnreadCount$.subscribe(count => {
updateBadge(count);
});
// 标记全部已读
classroom.markAllMessagesAsRead();
// 标记指定序列号以前的消息为已读
classroom.markMessageAsRead(latestSeq);聊天模式控制
老师/助教可控制课堂聊天模式:
typescript
// 设置聊天模式
await classroom.setSilenceMode('freeChat'); // 自由聊天
await classroom.setSilenceMode('publicOnly'); // 仅公开消息
await classroom.setSilenceMode('privateOnly'); // 仅私聊
await classroom.setSilenceMode('muteAll'); // 全员禁言
// 订阅禁言模式变化
classroom.state.silenceMode$.subscribe(mode => {
console.log('当前聊天模式:', mode);
});检查发送权限
typescript
// 检查当前用户是否可发送消息
if (classroom.canSendChatMessage()) {
// 可以发送
} else {
showToast('当前处于禁言状态');
}