Chat rooms
Chat rooms suit live streams, voice rooms and event chat. They have no fixed membership: users enter when they open the screen 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 app. The live-room screen 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 screen, call leave().
import 'package:deeprespond_im_flutter/deeprespond_im_flutter.dart';
final room = await im.chatrooms.enter('100310941534519296');
room.stream.listen((s) {
print('${s.info.name}(${s.info.memberCount} 人)共 ${s.messages.length} 条消息');
});
await room.send(const OutgoingContent.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 Flutter app. Chat room messages and state live only in memory and aren't written to the local database.
| 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 |
| Write a live-room screen | Using it in a page |
| Know what happens in the background and after disconnects | Background and re-entry after disconnects |
| Tell the user they were kicked or banned | Removed from a chat room |
| Show and send messages, gift effects, likes | Receive messages, Send messages, Likes |
| Seats in a voice room | Chat room attributes |
| Moderators muting, kicking or muting everyone | Manage a chat room |
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.
final page = await im.chatrooms.list(namePrefix: '周末', limit: 20);
for (final item in page.items) {
print('${item.roomId} ${item.name} ${item.memberCount}');
}
// 还有下一页时:im.chatrooms.list(namePrefix: '周末', cursor: page.nextCursor)To show online counts for several rooms on a list page, query them in one call with onlineCounts(), up to 100 rooms:
final counts = await im.chatrooms.onlineCounts(['100310941534519296', '100311027031212032']);
for (final c in counts) {
print('${c.roomId}:${c.memberCount} 人在线'); // 不存在和已解散的不在结果中
}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() completes 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.
try {
final room = await im.chatrooms.enter('100310941534519296', ext: 'VIP3');
print(room.snapshot.state); // in
} on DRException catch (e) {
if (e.reason == 'chatroom_banned') {
showToast('你已被禁止进入该直播间');
} else if (e.code == 'not_found') {
showToast('直播已结束');
} else {
rethrow;
}
}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 the same room several times. Calling
enter()for the same room more than once returns the same handle (extandleaveOthersonly take effect the first time). The SDK counts entries and only really leaves onceleave()has been called the same number of times. So several pages or widgets in the app can enter and leave the same room independently. - How many rooms. A device 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 complete. - 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 (for example swiping up and down), pass leaveOthers: true when entering the new one. This also leaves the other rooms the app is in, and the old handles' state becomes left:
final next = await im.chatrooms.enter('100311027031212032', leaveOthers: true);Leave a chat room
final room = await im.chatrooms.enter('100310941534519296');
// ……
await room.leave();leave()leaves immediately, and other members get a leave notification (in smaller rooms). If the connection drops withoutleave()being called, the server treats the user as gone 15 seconds later.- Call
leave()when the live-room page closes (State.dispose); see Using it in a page below. - 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 the app is currently in (room ID to handle).- On sign-out, every handle finishes with
stateleft.
Using it in a page
The handle's stream emits the current snapshot on subscribe and a new one on every change, so you can pass it straight to StreamBuilder. Snapshots can't be modified, and unchanged messages keep their objects. Here's the skeleton of a live-room page: enter, display, send, like, and leave when the page closes.
import 'dart:async';
import 'package:deeprespond_im_flutter/deeprespond_im_flutter.dart';
import 'package:flutter/material.dart';
class LiveRoomPage extends StatefulWidget {
const LiveRoomPage({super.key, required this.im, required this.roomId});
final DRClient im;
final String roomId;
@override
State<LiveRoomPage> createState() => _LiveRoomPageState();
}
class _LiveRoomPageState extends State<LiveRoomPage> {
late final Future<ChatroomHandle> _room = widget.im.chatrooms.enter(widget.roomId);
@override
void dispose() {
// 进入失败的不需要离开
unawaited(_room.then((room) => room.leave(), onError: (Object _) {}));
super.dispose();
}
@override
Widget build(BuildContext context) => FutureBuilder<ChatroomHandle>(
future: _room,
builder: (context, entered) {
if (entered.hasError) return const Center(child: Text('进入失败'));
final room = entered.data;
if (room == null) return const Center(child: Text('正在进入…'));
return StreamBuilder<ChatroomSnapshot>(
stream: room.stream,
initialData: room.snapshot,
builder: (context, s) {
final snap = s.data!;
return Column(
children: [
Text('${snap.info.name}(${snap.info.memberCount} 人在线)'),
if (snap.state == 'reentering') const Text('正在重新连接…'),
Expanded(
child: ListView(
children: [
for (final m in snap.messages)
ListTile(
title: Text(m.senderNickname ?? m.sender ?? '系统'),
subtitle: Text(m.type == 'text' ? '${m.body?['text']}' : '[消息]'),
),
],
),
),
Row(
children: [
TextButton(onPressed: () => room.send(const OutgoingContent.text('666')), child: const Text('发送')),
IconButton(onPressed: room.like, icon: const Icon(Icons.favorite)),
],
),
],
);
},
);
},
);
}Background and re-entry after disconnects
Being in a chat room is tied to the connection: the server records it on that connection.
- The network drops and comes back, or switches between Wi-Fi and mobile data: the SDK opens a new connection right away and re-enters the rooms the user was in. The handle stays the same, and you don't need to do anything. While re-entering, the snapshot's
stateisreentering, so you can show "Reconnecting"; on success it returns toin, and the room information, attributes and recent messages are fetched again. If re-entry happens within 15 seconds, other members don't see the user leave and come back. - Going to the background: the connection stays, the user stays in the room and keeps receiving messages, and
statestaysin. - The system drops the connection in the background: once the system suspends the app or cuts the network, the connection closes and the SDK doesn't reconnect in the background. 15 seconds later the server treats the user as gone, other members get a leave notification, and attributes the user set with
autoDelete(such as a seat) are deleted. When the app returns to the foreground, the SDK reconnects and re-enters automatically;stategoes toreenteringand back toin, and other members see the user enter again.
Messages sent during a disconnect aren't delivered later: chat room messages only go to connections online at that moment. Recent messages fetched after re-entering are merged and de-duplicated with the existing messages.
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.
Live-streaming apps that don't want users to hold a room slot (or a seat) while in the background can call leave() themselves when going to the background and enter() again when returning:
import 'package:flutter/widgets.dart';
class RoomLifecycle {
RoomLifecycle(this.roomId) {
_listener = AppLifecycleListener(onHide: _onHide, onShow: _onShow);
}
final String roomId;
late final AppLifecycleListener _listener;
ChatroomHandle? room;
Future<void> _onHide() async {
await room?.leave();
room = null;
}
Future<void> _onShow() async {
room ??= await im.chatrooms.enter(roomId);
}
void dispose() => _listener.dispose();
}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 a ChatroomRemovedEvent, the snapshot's state becomes removed, and removed holds the reason. The SDK doesn't re-enter automatically.
final room = await im.chatrooms.enter('100310941534519296');
room.on<ChatroomRemovedEvent>().listen((e) {
final r = e.removed;
final until = r.expiresAt;
final text = switch (r.reason) {
'kicked' => '你已被移出直播间',
'banned' => until != null ? '你已被禁止进入,至 ${until.toLocal()}' : '你已被永久禁止进入',
'dismissed' => '直播已结束',
'disabled' => '该直播间已被关闭',
_ => '你已离开直播间',
};
showToast(text);
});reason | Description |
|---|---|
kicked | Removed. operator is who did it (null when your server did). The user can enter again |
banned | Banned. expiresAt 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:
final room = await im.chatrooms.enter('100310941534519296');
final sub = room.stream.listen((s) {
final info = s.info;
print('${info.name} ${info.avatarUrl} 在线 ${info.memberCount} 全员禁言 ${info.muteAll}');
});
// 不再需要时 await sub.cancel()info.self is the user's role and status in the room (ChatroomSelf):
| Field | Description |
|---|---|
role | owner, admin or member |
muted, mutedUntil | 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 |
memberCount 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():
final room = await im.chatrooms.enter('100310941534519296');
final timer = Timer.periodic(const Duration(seconds: 30), (_) {
unawaited(room.refresh().catchError((Object _) {}));
});
// 离开时 timer.cancel()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 build the message list from messages (see Using it in a page).
To act on each new message (for example playing a gift animation), listen for ChatroomMessageReceived:
final room = await im.chatrooms.enter('100310941534519296');
room.on<ChatroomMessageReceived>().listen((e) {
final m = e.message;
if (m.type == 'custom' && m.body?['gift'] != null) {
print('${m.senderNickname} 送出了 ${m.body!['gift']}');
}
});- Moderator and official messages.
senderRoleis the sender's role when sending (owner,admin,member), so you can show a moderator badge. Messages withlocal.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
senderTypeissystem,sender,senderNickname,senderAvatarUrlandsenderRoleare 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
ChatroomDropped, and you can show "Too many messages, some were skipped":
room.on<ChatroomDropped>().listen((e) {
print('消息过多,已省略 ${e.count} 条');
});- Recalls. When a message is recalled, the SDK removes it from
messagesand emitsChatroomMessageRecalled. - 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.
room.events is a stream of all of the handle's events (Stream<ChatroomEvent>, where ChatroomEvent is a sealed class), so you can handle them together with switch; room.on<T>() takes just one type.
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, location, custom, or already uploaded images, voice, video and files, in the same format as one-to-one and group messages; see Message format.
final room = await im.chatrooms.enter('100310941534519296');
final sent = await room.send(const OutgoingContent.text('唱得太好了!'));
print('${sent.messageId} ${sent.local.status}'); // sentNormal- 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. If called before entering has finished, the message is sent once entering succeeds; if the connection isn't ready, it waits up to 15 seconds.
Priority
Each chat room message has a priority (ChatroomPriority) 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 |
final room = await im.chatrooms.enter('100310941534519296');
// 进场特效:低优先级
await room.send(const OutgoingContent.custom({'effect': 'enter', 'level': 3}), priority: ChatroomPriority.low);
// 管理员公告:高优先级,普通用户发送以 permission_denied(priority_denied)拒绝
await room.send(const OutgoingContent.text('请文明发言'), priority: ChatroomPriority.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, then put its URL in the message with OutgoingContent.uploaded. Chat rooms don't accept files directly; content such as OutgoingContent.image(DRFile(...)) rejects with unsupported (chatroom_attachment):
Future<void> sendImage(ChatroomHandle room, String path) async {
final info = await im.files.upload(DRFile(path), purpose: UploadPurpose.attachment, kind: FileKind.image).result;
await room.send(OutgoingContent.uploaded('image', {
'url': info.url,
if (info.width != null) 'width': info.width,
if (info.height != null) 'height': info.height,
'size': info.size,
}));
}For uploading and showing attachments, see Files and images.
Mutes
If the user is muted (info.self.muted is true), or everyone is muted (info.muteAll 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:
bool canSpeak(ChatroomSnapshot s) {
final self = s.info.self;
final privileged = self?.role == 'owner' || self?.role == 'admin' || self?.allowlisted == true;
return self?.muted != true && (!s.info.muteAll || privileged);
}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, doesn't wait for a result and never throws:
// 用户每点一次
room.like();
// 其他人的点赞(同一个人一秒内的合并为一次)
room.on<ChatroomLike>().listen((e) {
print('${e.username} 点了 ${e.count} 个赞');
});Likes don't appear in messages; they're only delivered through the ChatroomLike event. Likes are low priority and may be dropped when the room is busy.
Recall messages
final room = await im.chatrooms.enter('100310941534519296');
final sent = await room.send(const OutgoingContent.text('发错了'));
final id = sent.messageId;
if (id != null) {
final changed = await room.recall(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 ChatroomMessageRecalled is emitted:
room.on<ChatroomMessageRecalled>().listen((e) {
final by = e.recalledBy;
print(by != null ? '$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 (starting again from the first page when called without cursor).
final room = await im.chatrooms.enter('100310941534519296');
var page = await room.members(limit: 50);
print(page.items.map((m) => '${m.nickname.isNotEmpty ? m.nickname : m.username}(${m.role})'));
final next = page.nextCursor;
if (next != null) {
page = await room.members(cursor: next, limit: 50);
}In smaller rooms (up to the runtime policy chatroom_member_notify_limit, 100 by default), the handle emits ChatroomMemberJoined and ChatroomMemberLeft 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<ChatroomMemberJoined>().listen((e) {
final nickname = e.data['nickname'];
print('${nickname is String && nickname.isNotEmpty ? nickname : e.username} 进入了直播间,ext:${e.data['ext']}');
});
room.on<ChatroomMemberLeft>().listen((e) {
print('${e.username} 离开了直播间(${e.data['reason']})');
});Both events have username, and data is the raw notification:
| Event | Fields in data |
|---|---|
ChatroomMemberJoined | room_id, username, nickname, avatar_url, role, ext (the ext passed on entry), joined_at, member_count |
ChatroomMemberLeft | 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 (keys to ChatroomAttribute), and the SDK updates the snapshot when they change.
final room = await im.chatrooms.enter('100310941534519296');
room.stream.listen((s) {
print('1 号麦:${s.attributes['seat_1']?.value ?? '空位'}');
});Set and remove attributes
final room = await im.chatrooms.enter('100310941534519296');
// 上麦:离开聊天室(包括断线 15 秒后)时自动删除,麦位不会一直被占着
final result = await room.setAttributes({'seat_1': 'zhangsan'}, autoDelete: true);
if (result.failed.containsKey('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 announcementUpdatedAt changes every time it's edited. To show "Announcement updated", remember the announcement time the user has seen (for example in shared_preferences) and compare:
DateTime? seenAnnouncementAt; // 从持久化存储中读取
void checkAnnouncement(ChatroomSnapshot s, void Function(String text) show) {
final info = s.info;
final updatedAt = info.announcementUpdatedAt;
if (info.announcement.isNotEmpty && updatedAt != null && updatedAt != seenAnnouncementAt) {
show(info.announcement);
seenAnnouncementAt = updatedAt; // 同时写回持久化存储
}
}The owner and admins can change the announcement:
await room.manage.update(const ChatroomPatch(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 (ChatroomMemberResult):
final room = await im.chatrooms.enter('100310941534519296');
// 禁言 10 分钟;省略 durationSeconds 为永久禁言
final results = await room.manage.mute(['spammer01', 'spammer02'], durationSeconds: 600, reason: '刷屏');
for (final r in results) {
if (!r.ok) showToast('${r.username}:${r.code}'); // 如 permission_denied(owner_protected)
}
await room.manage.unmute(['spammer02']);
// 封禁:在聊天室里的会被立即移出,到期前不能再进入
await room.manage.ban(['troll01'], durationSeconds: 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):
final mutes = await room.manage.mutes(limit: 20);
for (final entry in mutes.items) {
final name = entry.nickname.isNotEmpty ? entry.nickname : entry.username;
print('$name 禁言到 ${entry.mutedUntil?.toLocal() ?? '永久'}');
}Mute everyone and the allowlist
final 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 in ChatroomPatch and returns the updated room information. You can pass the info.infoVersion you read as version to avoid overwriting someone else's simultaneous change; if it doesn't match, the call rejects with version_conflict.
final room = await im.chatrooms.enter('100310941534519296');
final info = room.snapshot.info;
await room.manage.update(
const ChatroomPatch(name: '周末音乐会(返场)', description: '每周六晚八点'),
version: info.infoVersion,
);To change the cover, upload the image first with purpose chatroom_avatar:
Future<void> changeCover(ChatroomHandle room, String path) async {
final uploaded = await im.files.upload(DRFile(path), purpose: UploadPurpose.chatroomAvatar).result;
await room.manage.update(ChatroomPatch(avatarUrl: 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
final 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 ChatroomRemovedEvent. 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?.clientChatroomCreateEnabled == true) {
final info = await im.chatrooms.create(name: '我的直播间', description: '欢迎来玩', listed: true);
final room = await im.chatrooms.enter(info.roomId);
}The app has a limit on the number of chat rooms; once reached, the call rejects with limit_exceeded (chatroom_limit).
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. All parameters are named.
| Parameter | Type | Required | Description |
|---|---|---|---|
namePrefix | String? | No | Search by name prefix, 1 to 128 characters |
limit | int? | No | Items per page; default 20, max 100 |
cursor | String? | No | nextCursor from the previous page |
Returns: Future<DRPage<ChatroomListItem>>. nextCursor is the cursor for the next page, or null if there isn't one.
Errors: invalid_argument (invalid parameters), not_signed_in.
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 |
|---|---|---|---|
roomId | String | Yes | Room ID |
Returns: Future<ChatroomInfo>.
Errors: not_found (the room doesn't exist or was dissolved), local_validation (empty roomId).
im.chatrooms.onlineCounts()
Queries online counts in a batch.
| Parameter | Type | Required | Description |
|---|---|---|---|
roomIds | List<String> | Yes | Room IDs, 1 to 100 |
Returns: Future<List<ChatroomOnlineCount>> in request order. Rooms that don't exist or were dissolved are left out; disabled rooms report 0.
Errors: local_validation (roomIds 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. All parameters are named.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | String | Yes | Name, 1 to 128 characters |
description | String? | No | Description, up to 512 characters |
avatarUrl | String? | No | Cover URL, obtained by uploading with purpose chatroom_avatar |
announcement | String? | No | Announcement, up to 2048 characters |
maxMembers | int? | No | Member limit, 1 to 500000. Follows the runtime policies if omitted |
listed | bool? | No | Whether the room appears in the room list; default true |
Returns: Future<ChatroomInfo>. Call enter() afterward to enter.
Errors: permission_denied (client_create_disabled: client creation isn't on for the app; checked locally), 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 (empty name).
im.chatrooms.enter()
Enters a chat room and completes once entered. Calling it again for the same room returns the same handle and increments the entry count.
| Parameter | Type | Required | Description |
|---|---|---|---|
roomId | String | Yes | Room ID |
ext | String? | No | Named. Extra information for this entry, up to 512 bytes, sent to other members with the entry notification. Only takes effect on the first entry |
leaveOthers | bool | No | Named. When true, also leaves the other rooms the app is in; used for switching rooms. Default false |
Returns: Future<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 (empty roomId or ext too long), not_signed_in.
im.chatrooms.entered
Read-only Map<String, ChatroomHandle> (unmodifiable): the rooms the app is currently in, from room ID to handle.
ChatroomHandle.snapshot
Read-only: the handle's current snapshot (unmodifiable; replaced as a whole when it changes). See ChatroomSnapshot.
ChatroomHandle.stream
Stream<ChatroomSnapshot>: emits the current snapshot on subscribe, then a new one on every change. Can be subscribed to in several places; cancel subscriptions you no longer need.
ChatroomHandle.events and on()
events is all of the handle's events (Stream<ChatroomEvent>); on<T extends ChatroomEvent>() returns a Stream<T> filtered by type, such as room.on<ChatroomLike>().
| Event | Fields | Description |
|---|---|---|
ChatroomMessageReceived | message (ChatroomMessageView) | A new message arrived, or a message you sent was confirmed by the server |
ChatroomMessageRecalled | messageId, recalledBy (String?), recalledAt (DateTime?) | A message was recalled. recalledBy is who did it, null when your server did |
ChatroomMemberJoined | username, data | Someone entered (only pushed in smaller rooms). For the fields of data, see Member list |
ChatroomMemberLeft | username, data | Someone left (only pushed in smaller rooms) |
ChatroomLike | username, count | Someone liked |
ChatroomDropped | count | Too many messages; the server dropped count of them |
ChatroomRemovedEvent | removed (ChatroomRemoved) | The user was removed; see Removed from a chat room |
ChatroomHandle.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: text, location, custom, or uploaded (an already uploaded attachment). Files can't be passed directly |
priority | ChatroomPriority | No | Named. Priority; default normal. Only the owner and admins can use high |
ext | Map<String, Object?>? | No | Named. Extension fields |
Returns: Future<ChatroomMessageView>, the sent message. When a low-priority message is dropped, local.status is dropped and messageId 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).
ChatroomHandle.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 | int | No | Optional positional. Number of likes this time, a positive integer; default 1 |
Returns: void.
ChatroomHandle.setAttributes()
Sets chat room attributes.
| Parameter | Type | Required | Description |
|---|---|---|---|
attributes | Map<String, String> | Yes | Keys to values, 1 to 20, totalling at most 16 KB |
autoDelete | bool | No | Named. When true, these keys are deleted automatically when the user leaves the room. Default false |
force | bool | No | Named. Overwrite keys set by others; owner and admins only. Default false |
Returns: Future<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.
ChatroomHandle.removeAttributes()
Removes chat room attributes.
| Parameter | Type | Required | Description |
|---|---|---|---|
keys | List<String> | Yes | Keys to remove, 1 to 20; missing keys are ignored |
force | bool | No | Named. Remove keys set by others; owner and admins only. Default false |
Returns: Future<AttributeResult>.
Errors: same as setAttributes(), except muted users can still remove attributes they set.
ChatroomHandle.recall()
Recalls a chat room message.
| Parameter | Type | Required | Description |
|---|---|---|---|
messageId | String | Yes | Message ID |
Returns: Future<bool>, whether anything changed; 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), local_validation (empty messageId), invalid_state.
ChatroomHandle.members()
Returns the users in the room right now, page by page, ordered by entry time from earliest and de-duplicated by username. All parameters are named.
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | int? | No | Items per page, up to 100 |
cursor | String? | No | nextCursor from the previous page; starts from the first page if omitted |
Returns: Future<DRPage<ChatroomMember>>.
Errors: not_chatroom_member, invalid_state.
ChatroomHandle.refresh()
Refetches the room information (including the online count), attributes and recent messages, and updates the snapshot.
Returns: Future<void>.
Errors: not_found, invalid_state.
ChatroomHandle.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: Future<void>. Never fails.
ChatroomHandle.manage.update()
Changes room information, only the fields given.
| Parameter | Type | Required | Description |
|---|---|---|---|
patch | ChatroomPatch | Yes | Fields to change; see ChatroomPatch |
version | int? | No | Named. The info.infoVersion you read. Nothing is changed if it doesn't match |
Returns: Future<ChatroomInfo>, the updated information.
Errors: permission_denied (role_required), version_conflict, content_rejected, invalid_argument, chatroom_disabled, app_unavailable, invalid_state.
ChatroomHandle.manage.setMuteAll()
Turns mute-everyone on or off (owner, admin).
| Parameter | Type | Required | Description |
|---|---|---|---|
on | bool | Yes | true to turn on, false to turn off |
Returns: Future<void>.
Errors: permission_denied (role_required), chatroom_disabled, invalid_state.
ChatroomHandle.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 | bool | Yes | true to set, false to revoke |
Returns: Future<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.
ChatroomHandle.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: Future<void>.
Errors: permission_denied (role_required), not_found, chatroom_disabled, invalid_state.
ChatroomHandle.manage.mute()
Mutes users (owner, admin).
| Parameter | Type | Required | Description |
|---|---|---|---|
usernames | List<String> | Yes | Usernames, 1 to 100 |
durationSeconds | int? | No | Named. Duration, 1 to 315360000 seconds (10 years). Permanent if omitted |
reason | String? | No | Named. Reason |
Returns: Future<List<ChatroomMemberResult>>, one per user: on success result is muted or unchanged; 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.
ChatroomHandle.manage.unmute()
Unmutes users (owner, admin).
| Parameter | Type | Required | Description |
|---|---|---|---|
usernames | List<String> | Yes | Usernames, 1 to 100 |
Returns: Future<List<ChatroomMemberResult>>: unmuted or unchanged (wasn't muted) on success; failures such as permission_denied (set_by_higher_role).
Errors: same as mute().
ChatroomHandle.manage.ban()
Bans users (owner, admin), removing them if they're in the room.
| Parameter | Type | Required | Description |
|---|---|---|---|
usernames | List<String> | Yes | Usernames, 1 to 100 |
durationSeconds | int? | No | Named. Duration, 1 to 315360000 seconds. Permanent if omitted |
reason | String? | No | Named. Reason |
Returns: Future<List<ChatroomMemberResult>>: banned or unchanged on success; failures such as owner_protected, admin_protected, ban_limit.
Errors: same as mute().
ChatroomHandle.manage.unban()
Lifts bans (owner, admin).
| Parameter | Type | Required | Description |
|---|---|---|---|
usernames | List<String> | Yes | Usernames, 1 to 100 |
Returns: Future<List<ChatroomMemberResult>>: unbanned or unchanged on success; failures such as set_by_higher_role.
Errors: same as mute().
ChatroomHandle.manage.kick()
Removes users from the room (owner, admin). They can enter again afterward.
| Parameter | Type | Required | Description |
|---|---|---|---|
usernames | List<String> | Yes | Usernames, 1 to 100 |
Returns: Future<List<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().
ChatroomHandle.manage.allowlist.list()
Queries the allowlist page by page (owner, admin). All parameters are named.
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | int? | No | Items per page; default 20, max 100 |
cursor | String? | No | nextCursor from the previous page |
Returns: Future<DRPage<ChatroomAllowlistEntry>>.
Errors: permission_denied (role_required), invalid_state.
ChatroomHandle.manage.allowlist.add()
Adds users to the allowlist (owner, admin).
| Parameter | Type | Required | Description |
|---|---|---|---|
usernames | List<String> | Yes | Usernames, 1 to 100 |
Returns: Future<List<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).
ChatroomHandle.manage.allowlist.remove()
Removes users from the allowlist (owner, admin).
| Parameter | Type | Required | Description |
|---|---|---|---|
usernames | List<String> | Yes | Usernames, 1 to 100 |
Returns: Future<List<ChatroomMemberResult>>: removed or unchanged on success.
Errors: same as mute().
ChatroomHandle.manage.mutes()
Queries unexpired mutes page by page (owner, admin). Parameters are the same as allowlist.list().
Returns: Future<DRPage<ChatroomMuteEntry>>.
Errors: permission_denied (role_required), invalid_state.
ChatroomHandle.manage.bans()
Queries unexpired bans page by page (owner, admin). Parameters are the same as allowlist.list().
Returns: Future<DRPage<ChatroomBanEntry>>.
Errors: permission_denied (role_required), invalid_state.
ChatroomHandle.manage.dismiss()
Dissolves the room (owner). Can't be undone. All members are removed with reason dismissed in ChatroomRemovedEvent.
Returns: Future<void>.
Errors: permission_denied (role_required), chatroom_disabled (rooms disabled by the platform), invalid_state.
Types
Common fields of server objects (which extend DRObject) are provided as getters, with times as UTC DateTimes. Every field the server returned is in raw (Map<String, Object?>).
ChatroomSnapshot
The handle's snapshot; can't be modified.
| Field | Type | Description |
|---|---|---|
info | ChatroomInfo | Room information, including self |
attributes | Map<String, ChatroomAttribute> | All attributes, from key to attribute |
attributesVersion | int | Attribute version, incremented on every change |
messages | List<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 | ChatroomRemoved? | Only when state is removed; see below |
ChatroomRemoved
| Field | Type | Description |
|---|---|---|
reason | String | kicked, banned, dismissed, disabled and so on; see Removed from a chat room |
operator | String? | Who did it; only for kicked and banned |
expiresAt | DateTime? | When a ban ends; only for banned, null if permanent |
ChatroomInfo
| Field | Type | Description |
|---|---|---|
roomId | String | Room ID |
name | String | Name |
description | String | Description |
avatarUrl | String | Cover URL; empty string if none |
announcement | String | Announcement |
announcementUpdatedAt | DateTime? | When the announcement was changed |
announcementUpdatedBy | String? | The user who changed the announcement from a client; null when your server changed it |
owner | String? | The owner's username; null if there's no owner |
admins | List<String> | Admins' usernames |
maxMembers | int | The member limit in effect |
muteAll | bool | Whether mute-everyone is on |
listed | bool | Whether the room appears in the room list |
status | String | active, disabled or dismissed (dissolved) |
disabledBy | String? | Who disabled it; null when not disabled |
memberCount | int | Online count, approximate |
infoVersion | int | Information version, incremented on every change |
attributesVersion | int | Attribute version |
createdAt | DateTime? | Creation time |
self | ChatroomSelf? | The user's status in the room; absent in list and create results |
ChatroomSelf
| Field | Type | Description |
|---|---|---|
role | String | owner, admin or member |
inRoom | bool | Whether the user is in the room |
muted | bool | Whether the user is muted in this room (not counting global mutes) |
mutedUntil | DateTime? | When the mute ends; null for a permanent mute |
allowlisted | bool | Whether the user is on the allowlist |
ChatroomListItem
An item from im.chatrooms.list().
| Field | Type | Description |
|---|---|---|
roomId | String | Room ID |
name | String | Name |
description | String | Description |
avatarUrl | String | Cover URL |
owner | String? | Owner |
maxMembers | int | Member limit |
memberCount | int | Online count |
ChatroomOnlineCount
| Field | Type | Description |
|---|---|---|
roomId | String | Room ID |
memberCount | int | Online count |
ChatroomMessageView
A message in messages: the server's chat room message plus local state local.
| Field | Type | Description |
|---|---|---|
roomId | String | Room ID |
messageId | String? | Message ID; null while sending, after failing, or when dropped |
clientMsgId | String? | De-duplication ID generated by the sender |
sender | String? | The sender's username; null for system messages |
senderType | String | user or system |
senderNickname | String? | Nickname at send time |
senderAvatarUrl | String? | Avatar at send time |
senderRole | String? | Role at send time: owner, admin or member |
type | String | Message type: text, image, voice, video, file, location, custom |
body | Map<String, Object?>? | Message content; see Message format |
ext | Map<String, Object?>? | Extension fields |
priority | String | high, normal or low |
via | String | How it was sent: client, openapi (your server), console or system |
createdAt | DateTime? | Send time; null while sending |
local | ChatroomMessageLocal | Local state; see below |
Received messages (from ChatroomMessageReceived and recent messages) always have messageId and createdAt.
ChatroomMessageLocal
| Field | Type | Description |
|---|---|---|
status | String | sending, sent, failed, or dropped (a low-priority message was dropped) |
error | DRException? | Why sending failed; only when failed |
trusted | bool | true if sent by your server, the console or the system |
ChatroomMember
An item from members().
| Field | Type | Description |
|---|---|---|
username | String | Username |
nickname | String | Nickname; empty string if none |
avatarUrl | String | Avatar URL; empty string if none |
role | String | owner, admin or member |
joinedAt | DateTime? | When they entered this time |
ChatroomAttribute
One attribute.
| Field | Type | Description |
|---|---|---|
value | String? | Value |
owner | String? | The user who last set this key; null if set by your server |
autoDelete | bool | Whether it's deleted automatically when the setter leaves the room |
updatedAt | DateTime? | When it was last set |
AttributeResult
The result of setAttributes() and removeAttributes().
| Field | Type | Description |
|---|---|---|
changed | bool | Whether any key was written or removed |
attributesVersion | int | The attribute version after the operation |
failed | Map<String, Map<String, Object?>> | Failed keys to their code and reason: permission_denied (attribute_owned: the key belongs to someone else) or limit_exceeded (attribute_limit: too many keys or too large in total). Empty if nothing failed |
ChatroomMemberResult
The result for one user in a batch management operation: successes have result, failures have code.
| Field | Type | Description |
|---|---|---|
username | String | Username |
ok | bool | Whether it succeeded |
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 | Map<String, Object?>? | On failure: details, such as reason owner_protected |
ChatroomMuteEntry, ChatroomBanEntry, ChatroomAllowlistEntry
An item in the mute list, ban list or allowlist.
| Field | Type | Description |
|---|---|---|
username | String | Username |
nickname | String | Nickname |
avatarUrl | String | Avatar URL |
mutedUntil | DateTime? | Mute list only: when it ends, null if permanent |
expiresAt | DateTime? | Ban list only: when it ends, null if permanent |
reason | String | Reason for the mute or ban (not on the allowlist) |
createdBy | String? | Username of whoever did it |
createdByRole | String | For mutes and bans, the role of whoever did it: owner, admin or server (not on the allowlist) |
createdAt | DateTime? | When it was added to the list |
ChatroomPatch
The argument to manage.update(). All constructor parameters are optional and named, and omitted fields aren't changed.
| Field | Type | Description |
|---|---|---|
name | String? | Name; can't be cleared (owner, admin) |
description | String? | Description; an empty string clears it (owner, admin) |
avatarUrl | String? | Cover URL; an empty string clears it (owner, admin) |
announcement | String? | Announcement; an empty string clears it (owner, admin) |
maxMembers | int? | Member limit (owner) |
resetMaxMembers | bool | When true, the member limit goes back to following the runtime policies (owner). Default false |
listed | bool? | Whether the room appears in the room list (owner) |
ChatroomPriority
Enum: high, normal, low; see Priority.
