IM Chat
This guide covers instant messaging features including sending messages, receiving messages, history, revocation, and chat mode control.
Sending Messages
Text Messages
const result = await classroom.sendTextMessage('Hello everyone!');
if (!result.ok) {
console.error('Send failed:', result.message);
}Image Messages
// Send after file selection (auto-uploaded to 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('Image send failed:', result.message);
}File Messages
const file = fileInput.files[0];
const result = await classroom.sendFileMessage(file);
if (!result.ok) {
console.error('File send failed:', result.message);
}Custom Messages
// Send custom message (content is serialized to a JSON string)
const result = await classroom.sendCustomMessage({
type: 'emoji',
emojiId: 'thumbs_up',
});Direct Messages (Private)
// Send a directed private message (visible only to the target user)
const result = await classroom.sendDirectedMessage('user_002', 'Hi, I have a question');
if (!result.ok) {
console.error('DM failed:', result.message);
}Receiving Messages
Subscribe to Message List via Reactive State
Subscribe to the full message list (history + real-time messages):
classroom.state.messageList$.subscribe(messages => {
renderMessageList(messages);
});Listen for New Messages via Events
Listen for new messages to trigger notifications, scrolling, etc.:
import { TEvent } from '@tencent-classroom/sdk';
classroom.on(TEvent.RECV_MESSAGE, (message) => {
console.log('New message:', message.content);
scrollToBottom();
showNotification(message);
});Parsing Private Message Indicators
messageList$ contains public messages + private messages relevant to the current user.
Private messages are identified by fields in message.ext (the IM cloudCustomData):
interface MessageExt {
IsPrivateMsg?: boolean; // Whether this is a private message
PrivateInfo?: {
From: { ID: string }; // Sender userId
To: { ID: string }; // Recipient userId
};
}Parsing example:
classroom.state.messageList$.subscribe((messages) => {
for (const msg of messages) {
// Parse ext field
const ext = typeof msg.ext === 'string' ? JSON.parse(msg.ext || '{}') : (msg.ext || {});
if (ext.IsPrivateMsg) {
// This is a private message
const fromId = ext.PrivateInfo?.From?.ID;
const toId = ext.PrivateInfo?.To?.ID;
renderPrivateMessage(msg, fromId, toId);
} else {
// Regular public message
renderPublicMessage(msg);
}
}
});messageList$ Filtering Rules
The SDK ensures messageList$ only contains:
- All public messages (Text / Image / File types)
- Private messages relevant to the current user (where the user is the sender From or recipient To)
Private messages between other users will never appear in the list — no visibility checks are needed at the business layer.
History Messages
The SDK exposes three complementary history APIs covering backward pagination, forward progressive pagination, and one-shot jump-to-latest. Classroom history is auto-loaded by the SDK on join — applications never need to trigger it manually.
| API | Implementation | Purpose |
|---|---|---|
loadOlderMessages(count) | TIM SDK getMessageListHopping (direction=0) | Paginate older messages |
loadNewerMessages(count) | TIM SDK getMessageListHopping (direction=1) | Progressive forward pagination (good for "scroll to bottom") |
seekToLatestMessages() | TIM SDK getMessageList loop (~100 entries) + batch replace of messageList$ | One-shot jump back to the latest tail (good for "↓ N new messages" button) |
messageList$is capped at 500 entries with bidirectional trimming:
- append mode (real-time /
loadNewerMessages) trims from the head →state.hasOlderMessages$=true- prepend mode (
loadOlderMessages) trims from the tail →state.hasNewerMessages$=true(enters "history-browsing" mode)
Bidirectional Sliding Window State
| State | Type | Meaning |
|---|---|---|
state.hasOlderMessages$ | boolean | Whether older messages are still loadable (upward pagination anchor) |
state.hasNewerMessages$ | boolean | Whether the list is in "history-browsing" mode (real-time messages are silently buffered) |
state.newerMessagesCount$ | number | Number of buffered real-time messages while in history-browsing mode (Tips excluded) |
Send behavior in history-browsing mode
While hasNewerMessages$=true, the 5 user-facing send APIs (sendTextMessage / sendImageMessage / sendFileMessage / sendCustomMessage / sendDirectedMessage) automatically await seekToLatestMessages() before inserting an optimistic placeholder, ensuring the sent message lands on a list whose tail equals the realtime tail.
Backward Pagination (loadOlderMessages)
// Call when the chat scrolls to the top (anchored on head.seq of messageList$)
const result = await classroom.loadOlderMessages(15);
if (result.ok && result.data.length === 0) {
console.log('No more older messages');
}Precondition
loadOlderMessages needs messageList$ to be non-empty so it has a seq anchor; the SDK populates the initial history automatically on join, so applications don't need to bootstrap it.
Progressive Forward Pagination (loadNewerMessages)
// Call when the user scrolls to the bottom of the list (anchored on tail.seq) —
// progressively fills the gap of newer messages trimmed by the 500-cap so the
// user can keep reading downward toward the realtime tail.
const result = await classroom.loadNewerMessages(15);
if (result.ok && result.data.length === 0) {
console.log('Already up-to-date');
}One-shot Jump to Latest (seekToLatestMessages)
// Call when the user taps the "↓ N new messages" floating button.
// The SDK loops getMessageList to gather ~100 latest messages then replaces
// messageList$ in one batch, also clearing hasNewerMessages$ / newerMessagesCount$.
if (classroom.state.hasNewerMessages$.get()) {
const r = await classroom.seekToLatestMessages();
if (r.ok) scrollChatToBottom();
}Scroll-driven Example
const msgListEl = document.querySelector('.chat-list')!;
let loadingOlder = false;
let loadingNewer = false;
msgListEl.addEventListener('scroll', async () => {
// Scrolled to top: paginate older messages (only attempt while older may exist)
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) {
// Restore scrollTop to avoid visual jump
msgListEl.scrollTop = msgListEl.scrollHeight - prevHeight;
}
}
// Scrolled to bottom + history-browsing mode: progressively catch up
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 new messages" floating button: one-shot jump to latest
classroom.state.hasNewerMessages$.subscribe((show) => {
toggleBackToLatestButton(show, classroom.state.newerMessagesCount$.get());
});
classroom.state.newerMessagesCount$.subscribe((n) => updateBackToLatestBadge(n));
backToLatestButton.addEventListener('click', () => classroom.seekToLatestMessages());Revoking Messages
// Revoke your own message (all roles can revoke their own)
const result = await classroom.revokeMessage(message.id);
if (!result.ok) {
console.error('Revoke failed:', result.message);
}
// Teacher/assistant revoke any message (by IM sequence number)
await classroom.revokeClassMessage(message.seq);Listen for revocation events:
classroom.on(TEvent.MESSAGE_REVOKED, ({ msgId }) => {
console.log('Message revoked:', msgId);
});Unread Management
// Subscribe to unread message count
classroom.state.messageUnreadCount$.subscribe(count => {
updateBadge(count);
});
// Mark all as read
classroom.markAllMessagesAsRead();
// Mark messages up to a specific seq as read
classroom.markMessageAsRead(latestSeq);Chat Mode Control
Teachers/assistants can control the classroom chat mode:
// Set chat mode
await classroom.setSilenceMode('freeChat'); // Free chat
await classroom.setSilenceMode('publicOnly'); // Public messages only
await classroom.setSilenceMode('privateOnly'); // Private messages only
await classroom.setSilenceMode('muteAll'); // Mute all
// Subscribe to silence mode changes
classroom.state.silenceMode$.subscribe(mode => {
console.log('Current chat mode:', mode);
});Check Send Permission
// Check whether the current user can send messages
if (classroom.canSendChatMessage()) {
// Can send
} else {
showToast('You are currently muted');
}Related APIs
- IM API Reference — Full method list
- Events Reference — IM event details
- State Reference — IM-related signals