Skip to content

IM Chat

This guide covers instant messaging features including sending messages, receiving messages, history, revocation, and chat mode control.

Sending Messages

Text Messages

typescript
const result = await classroom.sendTextMessage('Hello everyone!');
if (!result.ok) {
  console.error('Send failed:', result.message);
}

Image Messages

typescript
// 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

typescript
const file = fileInput.files[0];
const result = await classroom.sendFileMessage(file);
if (!result.ok) {
  console.error('File send failed:', result.message);
}

Custom Messages

typescript
// Send custom message (content is serialized to a JSON string)
const result = await classroom.sendCustomMessage({
  type: 'emoji',
  emojiId: 'thumbs_up',
});

Direct Messages (Private)

typescript
// 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):

typescript
classroom.state.messageList$.subscribe(messages => {
  renderMessageList(messages);
});

Listen for New Messages via Events

Listen for new messages to trigger notifications, scrolling, etc.:

typescript
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):

typescript
interface MessageExt {
  IsPrivateMsg?: boolean;      // Whether this is a private message
  PrivateInfo?: {
    From: { ID: string };      // Sender userId
    To: { ID: string };        // Recipient userId
  };
}

Parsing example:

typescript
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.

APIImplementationPurpose
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 headstate.hasOlderMessages$=true
  • prepend mode (loadOlderMessages) trims from the tailstate.hasNewerMessages$=true (enters "history-browsing" mode)

Bidirectional Sliding Window State

StateTypeMeaning
state.hasOlderMessages$booleanWhether older messages are still loadable (upward pagination anchor)
state.hasNewerMessages$booleanWhether the list is in "history-browsing" mode (real-time messages are silently buffered)
state.newerMessagesCount$numberNumber 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)

typescript
// 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)

typescript
// 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)

typescript
// 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

typescript
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

typescript
// 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:

typescript
classroom.on(TEvent.MESSAGE_REVOKED, ({ msgId }) => {
  console.log('Message revoked:', msgId);
});

Unread Management

typescript
// 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:

typescript
// 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

typescript
// Check whether the current user can send messages
if (classroom.canSendChatMessage()) {
  // Can send
} else {
  showToast('You are currently muted');
}