Chat rooms
Chat rooms suit live streams, voice rooms and event chat. They have no fixed membership: users enter when they open the page and leave when they leave it, and one room can hold hundreds of thousands of people at once. Messages go only to users in the room at that moment; they don't appear in the conversation list, don't count as unread and aren't pushed offline. Users who just entered see the most recent few dozen messages.
A typical flow: your server creates a chat room when a stream starts and gives the room ID to the page. The page calls im.chatrooms.enter() to enter and gets a ChatroomHandle, which it uses to read room information, show messages and send messages. When the user leaves the page, call leave().
const room = await im.chatrooms.enter('100310941534519296');
room.subscribe(() => {
const { info, messages } = room.getSnapshot();
render(info.name, info.member_count, messages);
});
await room.send({ type: 'text', body: { text: '主播晚上好' } });For the full chat room rules (member limits, roles, message priority and rate limits, recent messages, attributes), see Chat room management in the server docs. This page only covers how to use them in a web page.
| You want to | See |
|---|---|
| Show a list of rooms and their online counts | Find chat rooms |
| Enter, switch and leave rooms | Enter a chat room, Switch rooms, Leave a chat room |
| Tell the user they were kicked or banned | Removed from a chat room |
| Show and send messages, gift effects | Receive messages, Send messages |
| Likes | Likes |
| Seats in a voice room | Chat room attributes |
| Moderators muting, kicking or muting everyone | Manage a chat room |
| Use it in React or Vue components | React and Vue |
Find chat rooms
im.chatrooms.list() lists the app's chat rooms that are public to clients (created by the server with listed: true and active), optionally searching by name prefix. Rooms that aren't listed can only be entered by ID. im.chatrooms.get() gets one room's information without entering it.
const page = await im.chatrooms.list({ name_prefix: '周末', limit: 20 });
for (const item of page.items) {
console.log(item.room_id, item.name, item.member_count);
}
// 还有下一页时:im.chatrooms.list({ name_prefix: '周末', cursor: page.next_cursor })To show online counts for several rooms on a list page, query them in one call with onlineCounts(), up to 100 rooms:
const counts = await im.chatrooms.onlineCounts(['100310941534519296', '100311027031212032']);
// [{ room_id: '100310941534519296', member_count: 12034 }, ...],不存在和已解散的不在结果中Online counts are per user (several devices of one user count once), cached for up to 5 seconds and approximate, which suits displays like "12K online".
Enter a chat room
enter() resolves once the user has entered and returns the room's handle. You can pass an ext of up to 512 bytes (such as "VIP3"), which is sent to other members with the entry notification.
import { DRError } from '@deeprespond/im-web';
try {
const room = await im.chatrooms.enter('100310941534519296', { ext: 'VIP3' });
console.log(room.getSnapshot().state); // 'in'
} catch (err) {
if (err instanceof DRError && err.reason === 'chatroom_banned') {
showToast('你已被禁止进入该直播间');
} else if (err instanceof DRError && err.code === 'not_found') {
showToast('直播已结束');
} else {
throw err;
}
}To enter, the room must exist and be active, the user must not be banned from it, and it must not be full (owners and admins aren't subject to the member limit).
- Entering several times from one page. Calling
enter()for the same room more than once in a tab returns the same handle. The SDK counts entries and only really leaves onceleave()has been called the same number of times. So several components on a page can enter and leave the same room independently. - How many rooms. A browser (all tabs together) can be in a limited number of rooms at once (10 by default). Beyond that,
enter()rejects withlimit_exceeded(connection_room_limit). - Too many entries at once. When many users enter the same room at the same moment, the server asks some of them to try again shortly. The SDK waits and retries automatically, so
enter()may take a few seconds to resolve. - Not connected yet. Entering goes over the connection. If it isn't ready,
enter()waits up to 15 seconds, then rejects withnot_connected.
Switch rooms
When switching between rooms, pass leave_others: true when entering the new one. This also leaves the other rooms this tab is in (rooms entered by other tabs aren't affected), and the old handles' state becomes left:
const next = await im.chatrooms.enter('100311027031212032', { leave_others: true });Leave a chat room
const room = await im.chatrooms.enter('100310941534519296');
// ……
await room.leave();leave()leaves immediately, and other members get a leave notification (in smaller rooms). If you don't callleave(), the SDK leaves automatically when the page closes or reloads, and the server also treats the user as gone 15 seconds after the network drops.- In single-page apps, call
leave()when the route changes or the component unmounts. - After leaving, the handle is finished: the snapshot's
stateisleftand won't change again, and methods likesend()reject withinvalid_state. To enter again, callenter()again and you get a new handle. im.chatrooms.enteredholds the rooms this tab is currently in (room ID to handle).
Multiple tabs
Tabs in the same browser share one connection, and each tab enters and leaves its own rooms:
- Only the tab that entered a room receives its messages and notifications.
- When several tabs enter the same room, the server sees one user on one connection; the user only really leaves when the last tab leaves.
- Closing a tab leaves only the rooms that tab entered.
leave_othersonly leaves rooms entered by the calling tab.
Automatic re-entry after a disconnect
When the network drops and comes back and the SDK reconnects, it re-enters the rooms the user was in before. The handle stays the same, and you don't need to do anything. While re-entering, the snapshot's state is reentering, so you can show "Reconnecting". On success it returns to in, and the room information, attributes and recent messages are fetched again.
If during the disconnect the user was removed or banned, or the room was dissolved or disabled, re-entry fails and the handle is treated as removed.
Messages sent during the disconnect aren't delivered later: chat room messages only go to connections online at that moment.
Removed from a chat room
When the user is removed or banned by the owner, an admin or your server, or the room is dissolved or disabled, the handle emits removed, the snapshot's state becomes removed, and removed holds the reason. The SDK doesn't re-enter automatically.
const room = await im.chatrooms.enter('100310941534519296');
room.on('removed', ({ reason, expires_at }) => {
const text: Record<string, string> = {
kicked: '你已被移出直播间',
banned: expires_at ? `你已被禁止进入,至 ${new Date(expires_at).toLocaleString()}` : '你已被永久禁止进入',
dismissed: '直播已结束',
disabled: '该直播间已被关闭',
};
showToast(text[reason] ?? '你已离开直播间');
});reason | Description |
|---|---|
kicked | Removed. operator is who did it (null when your server did). The user can enter again |
banned | Banned. expires_at is when the ban ends, null if permanent. The user can't enter until then |
dismissed | The room was dissolved |
disabled | The room was disabled |
Room information and online count
The snapshot's info is the room's information: name, description, cover, announcement, owner, admins, online count, whether everyone is muted and so on; see ChatroomInfo. When it changes (including mute-everyone, owner and admin changes), the SDK updates the snapshot, so just subscribe():
const room = await im.chatrooms.enter('100310941534519296');
const off = room.subscribe(() => {
const { info } = room.getSnapshot();
render({
title: info.name,
cover: info.avatar_url,
online: info.member_count,
muteAll: info.mute_all,
});
});
// 不再需要时 off()info.self is the user's role and status in the room:
| Field | Description |
|---|---|
role | owner, admin or member |
muted, muted_until | Whether the user is muted, and when it ends (null if permanent). The SDK resets it to false when the mute expires |
allowlisted | Whether the user is on the allowlist; allowlisted users can still speak when everyone is muted |
member_count is fetched on entry. In smaller rooms (up to the runtime policy chatroom_member_notify_limit, 100 by default), it updates with entry and leave notifications. Larger rooms don't push those notifications; to refresh the count periodically, call refresh() or im.chatrooms.get():
const room = await im.chatrooms.enter('100310941534519296');
const timer = setInterval(() => void room.refresh().catch(() => {}), 30_000);Receive messages
The snapshot's messages holds the recent messages fetched on entry plus messages received and sent since, in arrival order, keeping at most 500 (older ones are dropped). Usually you just render the message list from messages:
const room = await im.chatrooms.enter('100310941534519296');
room.subscribe(() => {
for (const m of room.getSnapshot().messages) {
if (m.type === 'text') {
render(m.sender_nickname ?? m.sender, (m.body as { text: string }).text, m._local.status);
}
}
});To act on each new message (for example playing a gift animation), listen for the message event:
const room = await im.chatrooms.enter('100310941534519296');
room.on('message', ({ message }) => {
if (message.type === 'custom' && message.body?.gift) {
render('gift', message.sender_nickname, message.body.gift);
}
});- Moderator and official messages.
sender_roleis the sender's role when sending (owner,admin,member), so you can show a moderator badge. Messages with_local.trusted: truewere sent by your server, the console or the system, not by a user from a client, and can be shown as official. Don't rely on the content ofextfor this; senders fill inextthemselves. - System messages. When
sender_typeissystem,sender,sender_nickname,sender_avatar_urlandsender_roleare allnull. - Too many messages. In busy rooms, the server caps how many messages per second it pushes to each connection. Beyond that it drops low-priority messages first, then normal ones (high-priority messages aren't dropped under normal conditions). When messages are dropped, the handle emits
dropped, and you can show "Too many messages, some were skipped":
room.on('dropped', ({ count }) => {
render('hint', `消息过多,已省略 ${count} 条`);
});- Recalls. When a message is recalled, the SDK removes it from
messagesand emitsrecalled. - No delivery or ordering guarantee. Messages may be lost on network problems, and messages from different users may arrive in slightly different orders. For content that must arrive (such as gift settlement), send it with high priority and make sure of it in your own business logic.
Recent messages
After entering, the SDK fetches the recent messages automatically and puts them at the start of messages; you don't need to call anything. Recent messages include only normal and high priority, up to chatroom_history_size (runtime policy, 50 by default), and are kept for 24 hours. If the policy is 0, nothing is kept and messages starts empty. You can't page back through recent messages.
When the server signals that content may have been missed, the SDK refetches the room information, attributes and recent messages, merging and de-duplicating them with the existing messages. You can also call refresh() to do this manually.
Send messages
send() sends a message. Text, image, voice, video, file, location and custom messages are supported, in the same format as one-to-one and group messages; see Message format.
const room = await im.chatrooms.enter('100310941534519296');
const sent = await room.send({ type: 'text', body: { text: '唱得太好了!' } });
console.log(sent.message_id, sent._local.status); // 'sent'Normal- and high-priority messages first appear in messages with status sending, become sent on success, or become failed and reject on failure. The SDK retries network errors and timeouts automatically for up to 1 minute. Rate-limited sends (rate_limited) aren't retried; tell the user they're sending too fast.
Priority
Each chat room message has a priority that decides whether it can be rate-limited or dropped when there are too many messages:
priority | Use | Who can send | When the room's quota is exceeded |
|---|---|---|---|
high | System notices, admin announcements, gift settlement and other messages that mustn't be lost | Owner, admins | Rejects with rate_limited |
normal | Regular chat (the default) | Everyone | Rejects with rate_limited |
low | Likes, bullet-comment effects, entry effects and other high-frequency messages that are fine to lose | Everyone | Silently dropped; the returned message has _local.status dropped, and there's no need to tell the user |
const room = await im.chatrooms.enter('100310941534519296');
// 进场特效:低优先级
await room.send({ type: 'custom', body: { effect: 'enter', level: 3 } }, { priority: 'low' });
// 管理员公告:高优先级,普通用户发送以 permission_denied(priority_denied)拒绝
await room.send({ type: 'text', body: { text: '请文明发言' } }, { priority: 'high' });Low-priority messages don't appear in messages first and aren't retried. They appear only once the server pushes them back; dropped ones never appear.
The number of normal and low-priority messages each room accepts per second is set by the runtime policy chatroom_msg_per_second (40 by default); see Message priority and delivery.
Size limit and images
A chat room message (type, content and ext together) can't exceed the runtime policy max_chatroom_message_bytes (2 KB by default). Larger messages are rejected locally with local_validation (too_large).
To send an image or other attachment, upload the file first and put its URL in the message (chat rooms don't accept files directly):
declare const file: File; // 用户选择的图片
const room = await im.chatrooms.enter('100310941534519296');
const info = await im.files.upload(file, { purpose: 'attachment', kind: 'image' });
await room.send({
type: 'image',
body: { url: info.url, width: info.width ?? undefined, height: info.height ?? undefined, size: info.size },
});For uploading and showing attachments, see Files.
Mutes
If the user is muted (info.self.muted is true), or everyone is muted (info.mute_all is true) and the user isn't the owner, an admin or allowlisted, they can't send messages or set attributes. Sending rejects with chatroom_muted, with details.reason member (the user is muted) or all (everyone is muted). Disable the input box based on the snapshot:
const room = await im.chatrooms.enter('100310941534519296');
room.subscribe(() => {
const { info } = room.getSnapshot();
const self = info.self;
const privileged = self?.role === 'owner' || self?.role === 'admin' || self?.allowlisted === true;
const canSpeak = !self?.muted && (!info.mute_all || privileged);
render('input', { disabled: !canSpeak, placeholder: canSpeak ? '说点什么' : '你已被禁言' });
});Mute, unmute and mute-everyone changes update the snapshot in real time, and the SDK clears an expired mute locally. If the user is globally muted (chat room scope), sending rejects with user_muted; see Users, friends and presence.
Likes
Don't send one message per tap for high-frequency actions like likes. like() merges all likes within one second into a single low-priority message and doesn't wait for a result:
// 用户每点一次
room.like();
// 其他人的点赞(同一个人一秒内的合并为一次)
room.on('like', ({ username, count }) => {
render('hearts', username, count);
});Likes don't appear in messages; they're only delivered through the like event. Likes are low priority and may be dropped when the room is busy.
Recall messages
const room = await im.chatrooms.enter('100310941534519296');
const sent = await room.send({ type: 'text', body: { text: '发错了' } });
if (sent.message_id) {
await room.recall(sent.message_id);
}- Regular users can recall their own messages within the runtime policy
recall_window_seconds(120 seconds by default). - Admins can recall messages whose sender is currently a regular user; the owner can recall anyone's messages.
- Only messages still among the recent messages can be recalled; others reject with
not_found.
After a recall, the message is removed from everyone's messages and recalled is emitted:
room.on('recalled', ({ message_id, recalled_by }) => {
render('hint', recalled_by ? `${recalled_by} 撤回了一条消息` : '一条消息已被撤回');
});Member list
members() returns the users in the room right now, page by page, ordered by entry time from earliest, up to 100 per page. The SDK de-duplicates by username across pages.
const room = await im.chatrooms.enter('100310941534519296');
let page = await room.members({ limit: 50 });
render(page.items.map((m) => `${m.nickname || m.username}(${m.role})`));
if (page.next_cursor) {
page = await room.members({ cursor: page.next_cursor, limit: 50 });
}In smaller rooms (up to the runtime policy chatroom_member_notify_limit, 100 by default), the handle emits memberJoined and memberLeft when people enter and leave, and updates the online count in the snapshot. Larger rooms don't push these events; to show "XX entered the room", have the entering user send a low-priority custom message themselves.
room.on('memberJoined', (p) => {
const { username, nickname, ext } = p as { username: string; nickname: string; ext: string | null };
render('hint', `${nickname || username} 进入了直播间`, ext);
});
room.on('memberLeft', (p) => {
const { username } = p as { username: string; reason: string };
render('hint', `${username} 离开了直播间`);
});| Event | Payload fields |
|---|---|
memberJoined | room_id, username, nickname, avatar_url, role, ext (the ext passed on entry), joined_at, member_count |
memberLeft | room_id, username, reason (left, disconnected, kicked or banned), member_count |
Chat room attributes
Attributes are custom key-value pairs on the room, visible to every member. They're often used for seats in voice rooms (such as seat_1 set to zhangsan) or game state in live rooms. The snapshot's attributes holds all current attributes, and the SDK updates the snapshot when they change.
const room = await im.chatrooms.enter('100310941534519296');
room.subscribe(() => {
const { attributes } = room.getSnapshot();
render('seat1', attributes.seat_1?.value ?? '空位');
});Set and remove attributes
const room = await im.chatrooms.enter('100310941534519296');
// 上麦:离开聊天室(包括断线 15 秒后)时自动删除,麦位不会一直被占着
const result = await room.setAttributes({ seat_1: 'zhangsan' }, { auto_delete: true });
if (result.failed.seat_1) {
showToast('这个麦位已经有人了');
}
// 下麦
await room.removeAttributes(['seat_1']);- Who can change them. Users in the room can set and remove keys nobody has set yet and keys they set themselves. Keys set by others come back in
failedwithpermission_denied(attribute_owned), while the other keys are written as usual. The owner and admins can passforce: trueto overwrite and remove keys set by others (including your server); regular users passingforcegetpermission_denied(role_required). - Mutes. Muted users, and regular users not on the allowlist while everyone is muted, can't set attributes (
chatroom_muted) but can remove their own. - Format. Up to 100 keys per room. Keys are 1 to 128 characters of letters, digits and
_,-,.,:. Values are strings up to 4096 bytes. You can set or remove up to 20 keys per call, with keys and values totalling at most 16 KB per set; larger requests are rejected locally withlocal_validation. - Rate. Up to 20 attribute writes per room per second. State that changes many times a second (such as a live like count) doesn't belong in attributes; use low-priority custom messages instead.
- Values also go through content moderation after being set, and attributes that violate the rules are deleted.
Announcement
The announcement is info.announcement in the snapshot, and announcement_updated_at changes every time it's edited. To show "Announcement updated", remember the announcement time the user has seen and compare:
const room = await im.chatrooms.enter('100310941534519296');
room.subscribe(() => {
const { info } = room.getSnapshot();
const seen = localStorage.getItem(`announcement:${info.room_id}`);
if (info.announcement && info.announcement_updated_at && info.announcement_updated_at !== seen) {
render('announcement', info.announcement);
localStorage.setItem(`announcement:${info.room_id}`, info.announcement_updated_at);
}
});The owner and admins can change the announcement:
await room.manage.update({ announcement: '今晚 8 点开播,请文明发言' });Manage a chat room
The owner and admins can manage the room from the client through the handle's manage (they must enter the room first). Without the required role, calls reject with permission_denied (role_required; details.required_role is the role needed). Hide these options in regular users' UI based on info.self.role.
| Action | Method | Owner | Admin |
|---|---|---|---|
| Change name, description, cover, announcement | manage.update() | Yes | Yes |
| Change member limit, whether listed | manage.update() | Yes | No |
| Turn mute-everyone on or off | manage.setMuteAll() | Yes | Yes |
| Mute, unmute, view mute list | manage.mute(), unmute(), mutes() | Yes | Regular users only |
| Ban, unban, view ban list | manage.ban(), unban(), bans() | Yes | Regular users only |
| Remove | manage.kick() | Yes | Regular users only |
| Manage the allowlist | manage.allowlist | Yes | Yes |
| Set or revoke admins | manage.setAdmin() | Yes | No |
| Transfer ownership | manage.transferOwner() | Yes | No |
| Dissolve the room | manage.dismiss() | Yes | No |
| Send high-priority messages | send() | Yes | Yes |
| Recall others' messages | recall() | Anyone's | Regular users' |
- Nobody can mute, remove or ban the owner (
owner_protected); admins can't act on other admins (admin_protected). - When unmuting and unbanning, admins can only lift mutes and bans set by admins. Those set by the owner or your server can only be lifted by the owner (
set_by_higher_role). - While the room is disabled, all client management calls reject with
chatroom_disabled. - Regular users can enter, send normal- and low-priority messages, view members and recent messages, set attributes and recall their own messages.
Mute, ban and remove
Mute, ban, remove and allowlist are batch operations of up to 100 users at a time. Each user is handled separately, one failure doesn't affect the others, and a result comes back for each user:
const room = await im.chatrooms.enter('100310941534519296');
// 禁言 10 分钟;省略 duration_seconds 为永久禁言
const results = await room.manage.mute(['spammer01', 'spammer02'], { duration_seconds: 600, reason: '刷屏' });
for (const r of results) {
if ('code' in r) showToast(`${r.username}:${r.code}`); // 如 permission_denied(owner_protected)
}
await room.manage.unmute(['spammer02']);
// 封禁:在聊天室里的会被立即移出,到期前不能再进入
await room.manage.ban(['troll01'], { duration_seconds: 24 * 3600 });
// 移出:之后可以再次进入
await room.manage.kick(['troll02']);- Mute: the user can't send messages or set attributes, but stays in the room and keeps receiving messages. From 1 second to 10 years, or permanent; lifted automatically when it expires.
- Ban: the user is removed if in the room and can't enter while banned. Banning an admin also removes their admin role.
- Remove: only makes the user leave; they can enter again. Use a ban to stop them coming back.
- The three lists (mutes, bans, allowlist) are independent of whether the user is in the room, so you can act on users who aren't there right now.
View a list (only unexpired entries):
const mutes = await room.manage.mutes({ limit: 20 });
for (const item of mutes.items) {
const entry = item as { username: string; nickname: string; muted_until: string | null; created_by_role: string };
render(entry.nickname || entry.username, entry.muted_until ?? '永久');
}For the fields of list entries, see mute list entries, ban list entries and allowlist entries in the server docs.
Mute everyone and the allowlist
const room = await im.chatrooms.enter('100310941534519296');
await room.manage.setMuteAll(true);
// 嘉宾在全员禁言时仍然可以发言
await room.manage.allowlist.add(['guest01']);
await room.manage.allowlist.remove(['guest01']);
await room.manage.setMuteAll(false);When everyone is muted, the owner, admins and allowlisted users can still speak. A user who is individually muted can't speak even if allowlisted. The allowlist holds up to 500 people.
Edit room information
manage.update() changes only the fields given and returns the updated room information. You can pass the info.info_version you read (as version) to avoid overwriting someone else's simultaneous change; if it doesn't match, the call rejects with version_conflict.
const room = await im.chatrooms.enter('100310941534519296');
const { info } = room.getSnapshot();
await room.manage.update({ name: '周末音乐会(返场)', description: '每周六晚八点', version: info.info_version });To change the cover, upload the image first with purpose chatroom_avatar:
declare const file: File;
const room = await im.chatrooms.enter('100310941534519296');
const uploaded = await im.files.upload(file, { purpose: 'chatroom_avatar', kind: 'image' });
await room.manage.update({ avatar_url: uploaded.url });The name, description and announcement go through content moderation, and anything that fails rejects with content_rejected (details.field names the field).
Admins, ownership and dissolving
const room = await im.chatrooms.enter('100310941534519296');
await room.manage.setAdmin('lisi', true); // 设置管理员(最多 99 个)
await room.manage.setAdmin('lisi', false); // 取消管理员
await room.manage.transferOwner('wangwu'); // 转让后你成为普通用户The owner can dissolve the room. Everyone (including the owner) is removed with reason dismissed in the removed event. This can't be undone:
await room.manage.dismiss();Create chat rooms from the client
By default only your server or the console can create chat rooms. Once the app turns on client_chatroom_create_enabled in its runtime policies, users can create them from the client and become the owner. Otherwise create() rejects with permission_denied (client_create_disabled), so decide whether to show the option based on the runtime configuration:
if (im.config?.client_chatroom_create_enabled) {
const info = await im.chatrooms.create({ name: '我的直播间', description: '欢迎来玩', listed: true });
const room = await im.chatrooms.enter(info.room_id);
}The app has a limit on the number of chat rooms; once reached, the call rejects with limit_exceeded (chatroom_limit).
React and Vue
The React bindings provide useChatroom: it enters when the component mounts, leaves when it unmounts or the room ID changes, and returns the handle and a snapshot that updates with changes.
import { useChatroom } from '@deeprespond/im-web-react';
function LiveRoom({ roomId }: { roomId: string }) {
const { handle, snapshot, error } = useChatroom(roomId);
if (error) return <p>进入失败</p>;
if (!handle || !snapshot) return <p>正在进入…</p>;
return (
<div>
<h1>{snapshot.info.name}({snapshot.info.member_count} 人在线)</h1>
<ul>
{snapshot.messages.map((m, i) => (
<li key={m.message_id ?? m.client_msg_id ?? i}>
{m.sender_nickname ?? m.sender}:{m.type === 'text' ? String((m.body as { text: string }).text) : '[消息]'}
</li>
))}
</ul>
<button onClick={() => void handle.send({ type: 'text', body: { text: '666' } })}>发送</button>
<button onClick={() => handle.like()}>点赞</button>
</div>
);
}The Vue bindings' useChatroom works the same way; its argument can be a ref or a getter:
<script setup lang="ts">
import { useChatroom } from '@deeprespond/im-web-vue';
const props = defineProps<{ roomId: string }>();
const { handle, snapshot } = useChatroom(() => props.roomId);
function sendText(text: string) {
void handle.value?.send({ type: 'text', body: { text } });
}
</script>
<template>
<div v-if="snapshot">
<h1 v-text="snapshot.info.name" />
<ul>
<li v-for="(m, i) in snapshot.messages" :key="m.message_id ?? i" v-text="m.type === 'text' ? m.body?.text : '[消息]'" />
</ul>
<button @click="sendText('666')">发送</button>
</div>
</template>In React StrictMode, components mount, unmount and mount again. Because repeated entries in one tab share a handle and leaving is counted, this doesn't actually leave and re-enter the room.
API reference
im.chatrooms.list()
Lists, page by page, chat rooms that are public to clients (listed: true) and active, newest first. When searching by name prefix, results are sorted by name.
| Parameter | Type | Required | Description |
|---|---|---|---|
p.name_prefix | string | No | Search by name prefix, 1 to 128 characters |
p.limit | number | No | Items per page; default 20, max 100 |
p.cursor | string | No | next_cursor from the previous page |
Returns: Promise<Page<ChatroomListItem>>. items are the rooms; next_cursor is the cursor for the next page, or null if there isn't one.
Errors: invalid_argument (invalid parameters).
im.chatrooms.get()
Gets one room's information, including the current online count and the user's role self, without entering.
| Parameter | Type | Required | Description |
|---|---|---|---|
room_id | string | Yes | Room ID |
Returns: Promise<ChatroomInfo>.
Errors: not_found (the room doesn't exist or was dissolved).
im.chatrooms.onlineCounts()
Queries online counts in a batch.
| Parameter | Type | Required | Description |
|---|---|---|---|
room_ids | string[] | Yes | Room IDs, 1 to 100 |
Returns: Promise<Array<{ room_id: string; member_count: number }>> in request order. Rooms that don't exist or were dissolved are left out; disabled rooms report 0.
Errors: local_validation (room_ids is empty or has more than 100).
im.chatrooms.create()
Creates a chat room from the client, with the user as owner. Requires the app to turn on client_chatroom_create_enabled.
| Parameter | Type | Required | Description |
|---|---|---|---|
p.name | string | Yes | Name, 1 to 128 characters |
p.description | string | No | Description, up to 512 characters |
p.avatar_url | string | No | Cover URL, obtained by uploading with purpose chatroom_avatar |
p.announcement | string | No | Announcement, up to 2048 characters |
p.max_members | number | No | Member limit, 1 to 500000. Follows the runtime policies if omitted |
p.listed | boolean | No | Whether the room appears in the room list; default true |
Returns: Promise<ChatroomInfo>. Call enter() afterward to enter.
Errors: permission_denied (client_create_disabled: client creation isn't on for the app), limit_exceeded (chatroom_limit), content_rejected (name, description or announcement failed content moderation), invalid_argument (for example the cover URL isn't allowed, url_not_allowed), app_unavailable (app is read-only), local_validation (name missing).
im.chatrooms.enter()
Enters a chat room and resolves once entered. Calling it again for the same room in the same tab returns the same handle and increments the entry count.
| Parameter | Type | Required | Description |
|---|---|---|---|
room_id | string | Yes | Room ID |
p.ext | string | No | Extra information for this entry, up to 512 bytes, sent to other members with the entry notification. Only takes effect on the first entry |
p.leave_others | boolean | No | When true, also leaves the other rooms this tab is in; used for switching rooms |
Returns: Promise<ChatroomHandle>.
Errors: not_found (the room doesn't exist or was dissolved), chatroom_disabled (the room is disabled), permission_denied (chatroom_banned: the user is banned; details.expires_at is when it ends, null if permanent), limit_exceeded (chatroom_member_limit: the room is full; connection_room_limit: the limit on rooms entered at once is reached), rate_limited (too many entries; the SDK retries automatically when the wait is 60 seconds or less), not_connected (the connection wasn't ready within 15 seconds), local_validation (ext too long).
im.chatrooms.entered
Read-only ReadonlyMap<string, ChatroomHandle>: the rooms this tab is currently in, from room ID to handle.
handle.getSnapshot()
Returns the handle's current snapshot (read-only; replaced as a whole when it changes). See ChatroomSnapshot.
Returns: ChatroomSnapshot.
handle.subscribe()
Subscribes to snapshot changes.
| Parameter | Type | Required | Description |
|---|---|---|---|
listener | () => void | Yes | Called when the snapshot changes; call getSnapshot() inside it to get the new snapshot |
Returns: () => void, which unsubscribes.
handle.on()
Listens for handle events.
| Parameter | Type | Required | Description |
|---|---|---|---|
event | string | Yes | Event name; see the table below |
listener | (payload) => void | Yes | Handler |
Returns: () => void, which stops listening.
| Event | Payload | Description |
|---|---|---|
message | { message: ChatroomMessageView } | A new message arrived, or a message you sent was confirmed by the server |
recalled | { message_id: string; recalled_by: string | null; recalled_at: string } | A message was recalled. recalled_by is who did it, null when your server did |
memberJoined | Record<string, unknown> | Someone entered (only pushed in smaller rooms). For fields, see Member list |
memberLeft | Record<string, unknown> | Someone left (only pushed in smaller rooms). For fields, see Member list |
like | { username: string; count: number } | Someone liked |
dropped | { count: number } | Too many messages; the server dropped count of them |
removed | { reason: string; operator: string | null; expires_at: string | null } | The user was removed; see Removed from a chat room |
handle.send()
Sends a chat room message. If called before entering has finished, it's sent once entering succeeds.
| Parameter | Type | Required | Description |
|---|---|---|---|
content | OutgoingContent | Yes | Message content { type, body }; see Message format. Upload attachments first and send their URLs |
p.priority | 'high' | 'normal' | 'low' | No | Priority; default normal. Only the owner and admins can use high |
p.ext | Record<string, unknown> | No | Extension fields |
Returns: Promise<ChatroomMessageView>, the sent message. When a low-priority message is dropped, _local.status is dropped and message_id is null.
Errors: chatroom_muted (the user is muted or everyone is muted; details.reason is member or all, details.muted_until is when it ends), user_muted (globally muted), permission_denied (priority_denied: a regular user sending a high-priority message), rate_limited (user_send_rate: the user is sending too fast; room_send_rate: too many messages in the room), message_rejected (rejected by content moderation or your before-send callback), payload_too_large, local_validation (invalid content, such as too_large beyond max_chatroom_message_bytes), unsupported (chatroom_attachment: a file was passed directly), not_chatroom_member, chatroom_disabled, not_found, app_unavailable, not_connected, invalid_state (the handle is finished).
handle.like()
Likes. All likes within one second are merged into a single low-priority message. Returns nothing and never throws.
| Parameter | Type | Required | Description |
|---|---|---|---|
count | number | No | Number of likes this time, a positive integer; default 1 |
Returns: nothing.
handle.setAttributes()
Sets chat room attributes.
| Parameter | Type | Required | Description |
|---|---|---|---|
attributes | Record<string, string> | Yes | Keys to values, 1 to 20, totalling at most 16 KB |
p.auto_delete | boolean | No | When true, these keys are deleted automatically when the user leaves the room; default false |
p.force | boolean | No | Overwrite keys set by others; owner and admins only; default false |
Returns: Promise<AttributeResult>. If some keys fail, it doesn't throw; the failed keys are in failed.
Errors: chatroom_muted, permission_denied (role_required: a regular user passed force), rate_limited (attribute_rate, room_attribute_rate), invalid_argument (invalid_attribute_key, invalid_attribute_value), local_validation (too many keys or too large), not_chatroom_member, chatroom_disabled, app_unavailable, not_connected, invalid_state.
handle.removeAttributes()
Removes chat room attributes.
| Parameter | Type | Required | Description |
|---|---|---|---|
keys | string[] | Yes | Keys to remove, 1 to 20; missing keys are ignored |
p.force | boolean | No | Remove keys set by others; owner and admins only |
Returns: Promise<AttributeResult>.
Errors: same as setAttributes(), except muted users can still remove.
handle.recall()
Recalls a chat room message.
| Parameter | Type | Required | Description |
|---|---|---|---|
message_id | string | Yes | Message ID |
Returns: Promise<{ changed: boolean }>. changed is false if it was already recalled.
Errors: permission_denied (recall_window_expired: past the recall window; recall_denied: not allowed to recall this message), not_found (the message is no longer among the recent messages), invalid_state.
handle.members()
Returns the users in the room right now, page by page, ordered by entry time from earliest and de-duplicated by username.
| Parameter | Type | Required | Description |
|---|---|---|---|
page.limit | number | No | Items per page, up to 100 |
page.cursor | string | No | next_cursor from the previous page; starts from the first page if omitted |
Returns: Promise<Page<ChatroomMember>>.
Errors: not_chatroom_member, invalid_state.
handle.refresh()
Refetches the room information (including the online count), attributes and recent messages, and updates the snapshot.
Returns: Promise<void>.
Errors: not_found, invalid_state.
handle.leave()
Leaves the room. Decrements the entry count; at 0 the user really leaves and the handle is finished. If called before entering has finished, it's handled once entering completes. Does nothing on a finished handle.
Returns: Promise<void>. Never fails.
handle.manage.update()
Changes room information, only the fields given.
| Parameter | Type | Required | Description |
|---|---|---|---|
patch.name | string | No | Name; can't be cleared (owner, admin) |
patch.description | string | No | Description; an empty string clears it (owner, admin) |
patch.avatar_url | string | No | Cover URL; an empty string clears it (owner, admin) |
patch.announcement | string | No | Announcement; an empty string clears it (owner, admin) |
patch.max_members | number | null | No | Member limit; null follows the runtime policies (owner) |
patch.listed | boolean | No | Whether the room appears in the room list (owner) |
patch.version | number | No | The info.info_version you read. Nothing is changed if it doesn't match |
Returns: Promise<ChatroomInfo>, the updated information.
Errors: permission_denied (role_required), version_conflict, content_rejected, invalid_argument, chatroom_disabled, app_unavailable, invalid_state.
handle.manage.setMuteAll()
Turns mute-everyone on or off (owner, admin).
| Parameter | Type | Required | Description |
|---|---|---|---|
on | boolean | Yes | true to turn on, false to turn off |
Returns: Promise<void>.
Errors: permission_denied (role_required), chatroom_disabled, invalid_state.
handle.manage.setAdmin()
Sets or revokes an admin (owner).
| Parameter | Type | Required | Description |
|---|---|---|---|
username | string | Yes | Username; the user doesn't need to be in the room |
admin | boolean | Yes | true to set, false to revoke |
Returns: Promise<void>.
Errors: permission_denied (role_required; target_banned: the user is banned), limit_exceeded (admin_limit: there are already 99 admins), not_found (the user doesn't exist), chatroom_disabled, invalid_state.
handle.manage.transferOwner()
Transfers ownership to another user (owner); the current owner becomes a regular user.
| Parameter | Type | Required | Description |
|---|---|---|---|
username | string | Yes | The new owner's username; they don't need to be in the room |
Returns: Promise<void>.
Errors: permission_denied (role_required), not_found, chatroom_disabled, invalid_state.
handle.manage.mute()
Mutes users (owner, admin).
| Parameter | Type | Required | Description |
|---|---|---|---|
usernames | string[] | Yes | Usernames, 1 to 100 |
p.duration_seconds | number | No | Duration, 1 to 315360000 seconds (10 years). Permanent if omitted |
p.reason | string | No | Reason |
Returns: Promise<ChatroomMemberResult[]>, one per user: muted or unchanged on success; failures carry a code (such as permission_denied with owner_protected or admin_protected, not_found, or limit_exceeded with mute_limit).
Errors: permission_denied (role_required), chatroom_disabled, invalid_argument, local_validation (usernames is empty), invalid_state.
handle.manage.unmute()
Unmutes users (owner, admin).
| Parameter | Type | Required | Description |
|---|---|---|---|
usernames | string[] | Yes | Usernames, 1 to 100 |
Returns: Promise<ChatroomMemberResult[]>: unmuted or unchanged (wasn't muted) on success; failures such as permission_denied (set_by_higher_role).
Errors: same as mute().
handle.manage.ban()
Bans users (owner, admin), removing them if they're in the room.
| Parameter | Type | Required | Description |
|---|---|---|---|
usernames | string[] | Yes | Usernames, 1 to 100 |
p.duration_seconds | number | No | Duration, 1 to 315360000 seconds. Permanent if omitted |
p.reason | string | No | Reason |
Returns: Promise<ChatroomMemberResult[]>: banned or unchanged on success; failures such as owner_protected, admin_protected, ban_limit.
Errors: same as mute().
handle.manage.unban()
Lifts bans (owner, admin).
| Parameter | Type | Required | Description |
|---|---|---|---|
usernames | string[] | Yes | Usernames, 1 to 100 |
Returns: Promise<ChatroomMemberResult[]>: unbanned or unchanged on success; failures such as set_by_higher_role.
Errors: same as mute().
handle.manage.kick()
Removes users from the room (owner, admin). They can enter again afterward.
| Parameter | Type | Required | Description |
|---|---|---|---|
usernames | string[] | Yes | Usernames, 1 to 100 |
Returns: Promise<ChatroomMemberResult[]>: removed or not_in_room (the user wasn't in the room) on success; failures such as owner_protected, admin_protected.
Errors: same as mute().
handle.manage.allowlist.list()
Queries the allowlist page by page (owner, admin).
| Parameter | Type | Required | Description |
|---|---|---|---|
page.limit | number | No | Items per page; default 20, max 100 |
page.cursor | string | No | next_cursor from the previous page |
Returns: Promise<Page<unknown>>; each item has username, nickname, avatar_url, created_by and created_at.
Errors: permission_denied (role_required), invalid_state.
handle.manage.allowlist.add()
Adds users to the allowlist (owner, admin).
| Parameter | Type | Required | Description |
|---|---|---|---|
usernames | string[] | Yes | Usernames, 1 to 100 |
Returns: Promise<ChatroomMemberResult[]>: added or unchanged on success; failures such as limit_exceeded (allowlist_limit: the allowlist already has 500 people).
Errors: same as mute(), plus app_unavailable (app is read-only).
handle.manage.allowlist.remove()
Removes users from the allowlist (owner, admin).
| Parameter | Type | Required | Description |
|---|---|---|---|
usernames | string[] | Yes | Usernames, 1 to 100 |
Returns: Promise<ChatroomMemberResult[]>: removed or unchanged on success.
Errors: same as mute().
handle.manage.mutes()
Queries unexpired mutes page by page (owner, admin).
| Parameter | Type | Required | Description |
|---|---|---|---|
page.limit | number | No | Items per page; default 20, max 100 |
page.cursor | string | No | next_cursor from the previous page |
Returns: Promise<Page<unknown>>; each item has username, nickname, avatar_url, muted_until (null if permanent), reason, created_by, created_by_role (owner, admin or server) and created_at.
Errors: permission_denied (role_required), invalid_state.
handle.manage.bans()
Queries unexpired bans page by page (owner, admin). Parameters are the same as mutes().
Returns: Promise<Page<unknown>>; items have the same fields as mutes, with expires_at in place of muted_until.
Errors: permission_denied (role_required), invalid_state.
handle.manage.dismiss()
Dissolves the room (owner). Can't be undone. All members are removed with reason dismissed.
Returns: Promise<void>.
Errors: permission_denied (role_required), chatroom_disabled (rooms disabled by the platform), invalid_state.
Types
ChatroomSnapshot
The handle's snapshot.
| Field | Type | Description |
|---|---|---|
info | ChatroomInfo | Room information, including self |
attributes | Record<string, ChatroomAttribute> | All attributes, from key to attribute |
attributes_version | number | Attribute version, incremented on every change |
messages | ChatroomMessageView[] | Recent messages plus messages received and sent since, in arrival order, up to 500 |
state | string | entering, in, reentering (re-entering after a disconnect), removed or left |
removed | object | Only when state is removed: reason, operator, expires_at; see Removed from a chat room |
ChatroomInfo
| Field | Type | Description |
|---|---|---|
room_id | string | Room ID |
name | string | Name |
description | string | Description |
avatar_url | string | Cover URL; empty string if none |
announcement | string | Announcement |
announcement_updated_at | string | null | When the announcement was changed |
announcement_updated_by | string | null | The user who changed the announcement from a client; null when your server changed it |
owner | string | null | The owner's username; null if there's no owner |
admins | string[] | Admins' usernames |
max_members | number | The member limit in effect |
mute_all | boolean | Whether mute-everyone is on |
listed | boolean | Whether the room appears in the room list |
status | string | active, disabled or dismissed (dissolved) |
disabled_by | string | null | Who disabled it; null when not disabled |
member_count | number | Online count, approximate |
info_version | number | Information version, incremented on every change |
attributes_version | number | Attribute version |
created_at | string | Creation time |
self | object | The user's status in the room: role (owner, admin or member), in_room (whether in the room), muted, muted_until (null for a permanent mute), allowlisted |
All times are ISO 8601 strings, such as 2026-10-04T19:19:52.149Z.
ChatroomListItem
An item from im.chatrooms.list().
| Field | Type | Description |
|---|---|---|
room_id | string | Room ID |
name | string | Name |
description | string | Description |
avatar_url | string | Cover URL |
owner | string | null | Owner |
max_members | number | Member limit |
member_count | number | Online count |
ChatroomMessageView
A message in messages: the server's chat room message ChatroomMessage plus local state _local.
| Field | Type | Description |
|---|---|---|
room_id | string | Room ID |
message_id | string | null | Message ID; null while sending, after failing, or when dropped |
client_msg_id | string | null | De-duplication ID generated by the sender |
sender | string | null | The sender's username; null for system messages |
sender_type | string | user or system |
sender_nickname | string | null | Nickname at send time |
sender_avatar_url | string | null | Avatar at send time |
sender_role | string | null | Role at send time: owner, admin or member |
type | string | Message type: text, image, voice, video, file, location, custom |
body | Record<string, unknown> | null | Message content; see Message format |
ext | Record<string, unknown> | null | Extension fields |
priority | string | high, normal or low |
via | string | How it was sent: client, openapi (your server), console or system |
created_at | string | null | Send time; null while sending |
_local.status | string | sending, sent, failed, or dropped (a low-priority message was dropped) |
_local.error | DRError | Why sending failed; only when failed |
_local.trusted | boolean | true if sent by your server, the console or the system |
Received messages (from the message event and recent messages) always have message_id and created_at.
ChatroomMember
An item from members().
| Field | Type | Description |
|---|---|---|
username | string | Username |
nickname | string | Nickname; empty string if none |
avatar_url | string | Avatar URL; empty string if none |
role | string | owner, admin or member |
joined_at | string | When they entered this time |
ChatroomAttribute
One attribute.
| Field | Type | Description |
|---|---|---|
value | string | Value |
owner | string | null | The user who last set this key; null if set by your server |
auto_delete | boolean | Whether it's deleted automatically when the setter leaves the room |
updated_at | string | When it was last set |
AttributeResult
The result of setAttributes() and removeAttributes().
| Field | Type | Description |
|---|---|---|
changed | boolean | Whether any key was written or removed |
attributes_version | number | The attribute version after the operation |
failed | Record<string, { code: string; reason?: string }> | Failed keys: permission_denied (attribute_owned: the key belongs to someone else) or limit_exceeded (attribute_limit: too many keys or too large in total). An empty object if nothing failed |
ChatroomMemberResult
The result for one user in a batch management operation, one of two forms:
| Field | Type | Description |
|---|---|---|
username | string | Username |
result | string | On success: muted, unmuted, banned, unbanned, removed, not_in_room, added or unchanged |
code | string | On failure: the error code |
message | string | On failure: a description |
details | Record<string, unknown> | On failure: details such as { reason: 'owner_protected' } |
ChatroomPatch
The argument to manage.update(); see handle.manage.update().
