Users, friends and presence
This page covers everything about people: the user's own profile, other users' profiles, friends and friend requests, the blocklist, presence, and the user's signed-in devices.
- Profiles (
im.users): the user's own profile comes with sign-in and is saved locally; the user can change their nickname, avatar and custom attributes. Other users' profiles are fetched in batches as needed and cached in the local database, so list UIs can read them synchronously. - Friends (
im.friends): the friend list and blocklist are stored locally in full and exposed as live lists (LiveList), with changes from other devices synced in real time. Friend requests are queried page by page. - Presence (
im.presence): subscribe to the people you're showing and get an event when their status changes, or query once. - Signed-in devices (
im.devices): see the user's devices that are online now and all signed-in devices, and sign other devices out.
Call these methods after signing in (see Initialization, sign-in and connection). The general rules for live lists, synchronous state and events are in Events and errors; for using lists in widgets, see Using it in Flutter UIs.
| You want to | See |
|---|---|
| Show and edit the user's own nickname and avatar | Your own profile |
| Show other users' nicknames and avatars in a list | Show nicknames and avatars in lists |
| Show the friend list, or check whether someone is a friend | Show the friend list, Check your relationship with someone |
| Add friends and handle incoming requests | Send a request, Received requests and unread count |
| Block someone | Blocklist |
| Show online or offline | Subscribe to the people you show |
| See and sign out the user's other devices | Your signed-in devices |
Your own profile
Read your profile
im.users.me() returns the locally saved profile synchronously, or null when signed out. When the profile is changed on another device or by your server, the SDK updates the local copy and emits MeChanged.
import 'package:deeprespond_im_flutter/deeprespond_im_flutter.dart';
final me = im.users.me();
print(me == null ? '未登录' : (me.nickname.isNotEmpty ? me.nickname : me.username));
final sub = im.on<MeChanged>().listen((e) {
final profile = e.me;
if (profile != null) print('资料已更新:${profile.nickname}');
});
// 不再需要时 sub.cancel()To make sure you have the latest profile from the server, call im.users.refreshMe().
Change nickname, avatar and custom attributes
im.users.updateMe() sends only the arguments you pass; the rest stay unchanged. Pass an empty string to clear the nickname or avatar. Custom attributes are merged key by key, and keys set to null are deleted; to clear all custom attributes, pass clearAttributes: true. On success the local profile updates and MeChanged is emitted.
final updated = await im.users.updateMe(
nickname: '张三',
attributes: {'sign': '今天也要加油', 'city': null}, // 设置 sign,删除 city
);
print(updated.version);Nicknames and avatars go through content moderation. If they fail, the call rejects with content_rejected and details['field'] is nickname or avatar_url. For field formats, see User management in the server docs.
If several devices may edit the profile at once, pass the version you read. If the profile has been changed elsewhere, the call rejects with version_conflict and nothing is changed:
final current = im.users.me();
try {
await im.users.updateMe(nickname: '张三', version: current?.version);
} on DRException catch (e) {
if (e.code == 'version_conflict') {
await im.users.refreshMe(); // 取得最新的资料,让用户重新确认
}
}Set an avatar
Upload the avatar first with purpose user_avatar, then write the returned public URL to the profile. Avatar images can be up to 5 MB; for details on uploading and image processing, see Files and images.
Future<void> changeAvatar(String path) async {
final info = await im.files.upload(DRFile(path), purpose: UploadPurpose.userAvatar).result;
await im.users.updateMe(avatarUrl: info.url);
}If the app turns on media_url_only in its runtime policies, only avatar URLs uploaded by this app (or https URLs on allowed hosts) are accepted. Other URLs reject with invalid_argument and details['reason'] is url_not_allowed.
Whether the user is muted
Your server can mute users globally. im.users.myMute gives the user's mute in three scopes: one-to-one (chat), groups (group) and chat rooms (room), each null if not muted. MeMuteChanged is emitted when it changes:
im.on<MeMuteChanged>().listen((e) {
if (e.muted) {
final until = e.expiresAt;
showToast(until == null ? '你已被禁言' : '你已被禁言,到 ${until.toLocal()} 解除');
}
});Display only
The server has no API for querying your own mute, so myMute only reflects changes received while the app is running. After the app restarts it's empty even if the user is still muted. Don't disable the input box based on it; rely on the user_muted error the server returns when sending.
Other users' profiles
Show nicknames and avatars in lists
im.users.get(username) returns the cached profile synchronously. If it isn't cached or has expired (10 minutes by default; change it with the DRClientOptions.userCacheTtl option), it returns whatever it has (possibly null) and fetches in the background. Once fetched, UsersChanged is emitted, listing the usernames whose profiles changed.
For UIs that show many people at once, such as the conversation list or a member list, use get and rebuild on UsersChanged instead of awaiting each user. Background fetches within 50 ms are merged into one request of up to 100 users.
import 'dart:async';
import 'package:deeprespond_im_flutter/deeprespond_im_flutter.dart';
import 'package:flutter/material.dart';
class UserName extends StatefulWidget {
const UserName({super.key, required this.im, required this.username});
final DRClient im;
final String username;
@override
State<UserName> createState() => _UserNameState();
}
class _UserNameState extends State<UserName> {
late final StreamSubscription<UsersChanged> _sub;
@override
void initState() {
super.initState();
_sub = widget.im.on<UsersChanged>().listen((e) {
if (e.usernames.contains(widget.username.toLowerCase())) setState(() {});
});
}
@override
void dispose() {
unawaited(_sub.cancel());
super.dispose();
}
@override
Widget build(BuildContext context) {
final profile = widget.im.users.get(widget.username);
return Text(profile?.displayName ?? widget.username);
}
}To show names in the order "friend remark, group nickname, nickname, username", use displayName(im, username) from package:deeprespond_im_flutter/render.dart. It also reads synchronously and updates through the event once profiles arrive:
import 'package:deeprespond_im_flutter/render.dart';
final name = displayName(im, 'lisi');
final inGroup = displayName(im, 'lisi', groupId: '1840012345678901'); // 群里优先显示群昵称Query profiles
When you really need to wait for the result (for example when opening someone's profile page), call im.users.fetch(). The result is a Map keyed by lowercase username; users that don't exist or were deleted are left out. Unexpired cache entries are returned directly; with force: true the server is always queried.
final profiles = await im.users.fetch(['lisi', 'wangwu']);
final lisi = profiles['lisi'];
if (lisi == null) showToast('用户不存在');The profile cache is kept in the local database and survives an app restart. The server doesn't push friends' profile changes, so other users' profiles update at most one cache period later.
Friends
Show the friend list
im.friends.list() returns a live list of all friends (LiveList<Friend>), sorted by the first non-empty value of remark, nickname and username. The SDK syncs the friend list after sign-in, and later additions, removals and edits (including those made on other devices) show up in the list in real time. Call dispose() when you no longer show it.
In widgets, show it with LiveListBuilder from deeprespond_im_flutter:
import 'package:deeprespond_im_flutter/deeprespond_im_flutter.dart';
import 'package:flutter/material.dart';
class FriendListPage extends StatefulWidget {
const FriendListPage({super.key, required this.im});
final DRClient im;
@override
State<FriendListPage> createState() => _FriendListPageState();
}
class _FriendListPageState extends State<FriendListPage> {
late final LiveList<Friend> _friends = widget.im.friends.list();
@override
void dispose() {
_friends.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => LiveListBuilder<Friend>(
list: _friends,
builder: (context, snapshot) {
if (snapshot.loading && snapshot.items.isEmpty) return const Center(child: CircularProgressIndicator());
return ListView.builder(
itemCount: snapshot.items.length,
itemBuilder: (context, i) => ListTile(title: Text(snapshot.items[i].displayName)),
);
},
);
}Outside widgets, read snapshot directly or listen to stream:
final friends = im.friends.list();
final sub = friends.stream.listen((s) {
if (!s.loading) print(s.items.map((f) => f.displayName).join('、'));
});
// 不再需要时
await sub.cancel();
friends.dispose();To just check whether someone is a friend or read their remark, use the synchronous im.friends.get(username), which returns null for non-friends. im.friends.listInfo gives the friend count and limit:
final info = im.friends.listInfo;
if (info != null) print('好友 ${info.count} / ${info.maxCount}');Check your relationship with someone
To show states like "Add friend", "Awaiting approval" or "Blocked" on a profile page, use im.friends.check(), with up to 100 users per call. The user themself and nonexistent users are left out of the result.
final result = await im.friends.check(['lisi']);
final relation = result.isEmpty ? null : result.first;
if (relation?.isFriend == true) {
print('发消息');
} else if (relation?.request == 'sent') {
print('等待验证');
} else if (relation?.request == 'received') {
print('对方已向你发出申请');
} else {
print('加好友');
}blocked only says whether the user has blocked the other person; whether the other person has blocked the user is never revealed.
Set remarks and custom attributes
Remarks and custom attributes belong only to the user's side; the friend can't see them. Custom attributes can be used for starred friends or friend groups. They're merged key by key, keys set to null are deleted, and clearAttributes: true clears them all.
await im.friends.update('lisi', remark: '老李', attributes: {'star': '1', 'group': '同事'});Remove a friend
After removal, neither side is a friend of the other, and the user is also removed from the other person's friend list. Removing someone who isn't a friend also succeeds.
await im.friends.remove('lisi');Friend requests
Send a request
final result = await im.friends.requests.send(
username: 'lisi',
message: '我是张三,项目组的',
remark: '李四', // 成为好友后自动设置的备注
addSource: 'search', // 添加来源,由你定义,会展示给对方
);
if (result.accepted) {
showToast('已成为好友');
} else {
showToast('已发送,等待对方验证');
}The result depends on the other user's friend add mode. If they require approval, status is pending. If they allow anyone to add them, or they had already sent the user a request, the two become friends right away: status is accepted and friend is the new friend. If they reject everyone, the call rejects with friend_add_denied.
Common errors:
code | Description |
|---|---|
already_exists | Already friends. The SDK treats this as success and returns status accepted; it never rejects with this error |
friend_add_denied | The other user rejects all friend requests |
user_blocked | One of the two has blocked the other |
limit_exceeded | The user's friend list is full, or the other user allows anyone to add them and their friend list is full |
rate_limited | Too many requests. details['reason'] is request_rate (20 per minute, 500 per day), pair_daily_limit (5 per day to the same person) or declined_cooldown (within 24 hours of being declined) |
content_rejected | The message or add source failed content moderation |
permission_denied | Your server rejected the request through the before-add-friend callback. details['reason'] is app_rejected or callback_unavailable |
timeout | Network error or timeout, and the SDK checked and confirmed the request wasn't sent |
Sending a request is never retried automatically. After a network error or timeout, the SDK checks the relationship first: if the two are already friends or the request went through, it returns success; if it confirms the request wasn't sent, it rejects with timeout and the user decides whether to send again. For rate_limited, tell the user based on details['reason'].
A sent request can't be withdrawn. For the full rules (requests expire after 30 days, sending again updates the existing request, and so on), see Friend requests in the server docs.
Received requests and unread count
im.friends.requests.unreadCount gives the number of received requests the user hasn't viewed yet, synchronously (counted up to 100, so you can show "99+" above 99). FriendRequestsChanged is emitted when it changes:
im.on<FriendRequestsChanged>().listen((e) {
final n = e.unreadCount;
print(n > 99 ? '99+' : (n == 0 ? '' : '$n'));
});When the user opens the "New friends" page, query received requests and mark them as read once shown:
final page = await im.friends.requests.received();
for (final r in page.items) {
print('${r.nickname.isNotEmpty ? r.nickname : r.username}:${r.message}(${r.status?.name})');
}
await im.friends.requests.markRead();- Without
cursorandstatus, and withlimitno larger than the cached count, it returns the first page held in SDK memory without sending a request. The SDK refreshes that page on sign-in, on reconnect and when request notifications arrive, so it's current. Filtering bystatusor paging queries the server. markRead()without arguments marks the unread requests on the current first page as read. You can also passreadUntil(therequestedAtof the newest request shown).
Query sent requests with im.friends.requests.sent(), which works the same way. A request not handled within 30 days of the last send expires and its status becomes expired.
Accept or decline
final friend = await im.friends.requests.accept('wangwu', remark: '王五');
await im.friends.requests.decline('zhaoliu');Accepting makes the two friends and the friend list updates right away. After declining, the other user can't send the user another request for 24 hours. Expired requests reject with not_found. If either side's friend list is full when accepting, the call rejects with limit_exceeded.
Blocklist
Once the user blocks someone, that person can't send them one-to-one messages, send them friend requests or invite them to groups, and any pending friend request the user sent them is withdrawn. Blocking doesn't remove the friendship and doesn't notify the other person. See Blocklist in the server docs.
await im.friends.blacklist.add('spammer');
if (im.friends.blacklist.has('spammer')) print('已拉黑');
final blacklist = im.friends.blacklist.list(); // 按拉黑时间从新到旧
final sub = blacklist.stream.listen((s) => print(s.items.map((e) => e.username)));
await im.friends.blacklist.remove('spammer');The blocklist holds up to 1000 people. If the app turns off blocklist enforcement (user_blacklist_enabled) in its runtime policies, users can no longer block others and add rejects with permission_denied; viewing and unblocking still work. Read im.config?.userBlacklistEnabled to decide whether to show a "Block" option.
Friend add mode
Each user can choose how friend requests sent to them are handled:
FriendAddMode | Server value | Description |
|---|---|---|
needConfirm | need_confirm | Approval required: a pending request is created, and the two become friends once the user accepts |
allowAny | allow_any | Anyone can add: the two become friends right away, without approval |
denyAny | deny_any | Nobody can add: requests fail with friend_add_denied |
final settings = await im.friends.settings.get();
print(settings.friendAddMode == null ? '跟随应用默认(${settings.effectiveFriendAddMode?.name})' : settings.friendAddMode!.name);
await im.friends.settings.set(FriendAddMode.needConfirm);
await im.friends.settings.set(null); // 清除本人的设置,改用应用的默认值If the user hasn't set it, the default from the app's runtime policies applies (need_confirm by default).
Presence
Presence requires the app to turn on presence_enabled in its runtime policies (off by default). The policies also control whose status can be seen: with presence_scope set to friends (the default) only friends' status is visible; with all, anyone's. When presence_last_seen_visible is off, last-seen times aren't shown (lastSeenAt is null). Read these from im.config to decide whether to show presence in the UI:
final config = im.config;
final showPresence = config?.presenceEnabled == true;
final scope = config?.presenceScope; // friends 或 allWhen presence is off, subscribe throws synchronously and query rejects with the same error: permission_denied (details['reason'] is presence_disabled).
Subscribe to the people you show
im.presence.subscribe() registers people to follow and returns a function that cancels this subscription. PresenceChanged is emitted when a status changes; then read it with the synchronous im.presence.get(username):
import 'dart:async';
import 'package:deeprespond_im_flutter/deeprespond_im_flutter.dart';
import 'package:flutter/material.dart';
class OnlineDot extends StatefulWidget {
const OnlineDot({super.key, required this.im, required this.username});
final DRClient im;
final String username;
@override
State<OnlineDot> createState() => _OnlineDotState();
}
class _OnlineDotState extends State<OnlineDot> {
late final void Function() _unsubscribe;
late final StreamSubscription<PresenceChanged> _sub;
@override
void initState() {
super.initState();
_unsubscribe = widget.im.presence.subscribe([widget.username]);
_sub = widget.im.on<PresenceChanged>().listen((_) => setState(() {}));
}
@override
void dispose() {
unawaited(_sub.cancel());
_unsubscribe();
super.dispose();
}
@override
Widget build(BuildContext context) {
final state = widget.im.presence.get(widget.username);
final color = state == null ? Colors.grey.shade300 : (state.online ? Colors.green : Colors.grey);
return Icon(Icons.circle, size: 10, color: color);
}
}- Subscribe only to what's visible, such as friends visible in the conversation list or the other user in an open chat. Subscribing to the same person several times is reference-counted; they're only unsubscribed once every subscription is cancelled.
- Delays. Subscription changes are batched for 2 seconds before being sent to the server, and unsubscribing waits 30 seconds (skipped if the person is needed again), so fast scrolling doesn't flood the server.
- Limit. One device can subscribe to at most 3000 people at once.
- Reconnecting. After a reconnect the SDK resubscribes automatically.
PresenceChangedthen hasreset: true, meaning all previous status was cleared.
get returns null in these cases, and the UI should show "Unknown" rather than "Offline": no subscription result yet; the person doesn't exist or isn't visible; resubscribing after a reconnect hasn't finished; or presence_scope is friends and the person is no longer a friend (they're then listed in cleared of PresenceChanged).
People who couldn't be subscribed are reported with PresenceSubscribeFailed:
im.on<PresenceSubscribeFailed>().listen((e) {
// reason:denied(不存在或看不到)、limitExceeded(超出上限)、presenceDisabled(应用关闭了在线状态)
print('在线状态订阅失败 ${e.reason?.wire} ${e.usernames}');
});Server rate limits (30 subscription calls per user per minute) are handled by the SDK, which waits and retries without emitting an event.
How foreground and background affect presence
Online means "at least one device is connected to the IM service", and a device in the background still counts. When a mobile device (iOS, Android) switches between foreground and background:
- Going to the background: the SDK tells the server the app is in the background, keeps the connection and lengthens the heartbeat interval. As long as the connection stays, others still see the user online (
iosorandroidis still in the platform list), and presence this device subscribes to keeps updating. - After the system drops the connection: once the system suspends the app or cuts the network, the connection closes and the server marks this device offline right away (if the user has no other online device, others see them offline). If the connection breaks abnormally without closing properly, the server only notices after the background idle timeout (up to 360 seconds). While in the background the SDK doesn't reconnect, so presence this device subscribes to stops updating, and
getreturns the values from before the disconnect. - Returning to the foreground: the SDK reconnects right away and others see the user come back online. This device first emits
PresenceChangedwithreset: true(all previous status cleared), then resubscribes and emitsPresenceChangedagain once results arrive.
So the app doesn't need to cancel and restore subscriptions on foreground/background switches: keep each page's subscriptions, and status refreshes automatically on returning to the foreground. Desktops (macOS, Windows, Linux) don't distinguish foreground and background; the connection is always treated as foreground.
When the network switches between Wi-Fi and mobile data, the SDK opens a new connection right away, which replaces the old one, so others don't see the user go offline and back online.
Query once
When you don't need live updates (for example showing status once on a profile page), use im.presence.query(), with up to 100 users per call. Query results don't update what get returns.
final states = await im.presence.query(['lisi']);
final state = states.isEmpty ? null : states.first;
final lastSeen = state?.lastSeenAt;
if (state != null && !state.online && lastSeen != null) {
print('最近在线:${lastSeen.toLocal()}');
}Going offline is detected with a delay: when a device loses its network abruptly, the server only marks it offline after the connection's idle timeout. See Presence in the server docs.
Your signed-in devices
Devices online now
im.devices.online() returns the user's devices that currently hold a connection (including this one), synchronously. DevicesChanged is emitted when they change. Use it to show "Signed in on desktop":
im.on<DevicesChanged>().listen((e) {
final others = e.devices.where((d) => !d.current).toList();
print(others.isEmpty ? '' : '${others.map((d) => d.platform).join('、')} 已登录');
});When the user signs in on another device, DevicesNewLogin is emitted, and you can notify the user:
im.on<DevicesNewLogin>().listen((e) {
final name = e.deviceName.isNotEmpty ? e.deviceName : e.platform;
showToast('你的账号于 ${e.createdAt?.toLocal()} 在 $name 上登录');
});All signed-in devices and signing them out
im.devices.sessions() returns all of the user's active sessions (including devices not connected right now), most recently active first, with current marking this device. im.devices.kick() signs out a given device:
final sessions = await im.devices.sessions();
for (final s in sessions) {
print('${s.deviceName ?? s.platform}${s.current ? '(本机)' : ''}');
}
final others = sessions.where((s) => !s.current);
if (others.isNotEmpty) await im.devices.kick(others.first.sessionId);The kicked device goes offline at once and has to sign in again. Kicking the current device is the same as signing out. The number of devices signed in at once is limited by the runtime policies, and the oldest device is signed out when the limit is exceeded; see Sign-in and tickets in the server docs.
deviceName defaults to the name the system provides (from iOS 16, only a generic name like "iPhone" is available); set it with DRClientOptions.deviceName when creating the client; see Initialization, sign-in and connection.
API reference
im.users.me()
Returns the locally saved profile synchronously.
Returns: SelfProfile?, null when signed out. MeChanged is emitted when it changes.
im.users.refreshMe()
Fetches the user's profile from the server and updates the local copy.
Returns: Future<SelfProfile>.
im.users.updateMe()
Changes the user's profile, sending only the arguments passed. All parameters are named.
| Parameter | Type | Required | Description |
|---|---|---|---|
nickname | String? | No | Nickname, up to 64 characters. An empty string clears it |
avatarUrl | String? | No | Avatar URL. An empty string clears it |
attributes | Map<String, String?>? | No | Custom attributes, merged key by key: keys set to null are deleted. Up to 32 keys and 4 KB in total |
clearAttributes | bool | No | When true, clears all custom attributes (ignoring attributes). Default false |
version | int? | No | The profile version you read. Nothing is changed if it doesn't match the server |
Returns: Future<SelfProfile>, the updated profile.
Errors:
code | Description |
|---|---|
invalid_argument | A field is malformed, or the avatar URL isn't allowed (details['reason'] is url_not_allowed) |
content_rejected | The nickname or avatar failed content moderation |
version_conflict | version doesn't match the server |
im.users.myMute
Read-only MyMute: the user's global mute in three scopes, chat, group and room, each GlobalMuteInfo?. Only reflects changes received while the app is running; MeMuteChanged is emitted when it changes.
im.users.get()
Returns a cached user profile synchronously. If it's missing or expired, fetches it in the background and emits UsersChanged once fetched.
| Parameter | Type | Required | Description |
|---|---|---|---|
username | String | Yes | Username |
Returns: UserProfile?.
im.users.fetch()
Queries user profiles in a batch, using unexpired cache entries directly.
| Parameter | Type | Required | Description |
|---|---|---|---|
usernames | List<String> | Yes | Usernames. More than 100 are queried in batches |
force | bool | No | Named. When true, the cache isn't used. Default false |
Returns: Future<Map<String, UserProfile>> keyed by lowercase username. Users that don't exist or were deleted are left out.
Errors: not_signed_in.
im.friends.list()
Returns a live list of all friends (LiveList<Friend>), sorted by the first non-empty value of remark, nickname and username. The snapshot's hasMore is always false. Call dispose() when you're done with it.
Throws not_signed_in when signed out.
im.friends.get()
Returns one friend synchronously, or null if not a friend. FriendsChanged is emitted on changes.
| Parameter | Type | Required | Description |
|---|---|---|---|
username | String | Yes | Username |
Returns: Friend?.
im.friends.listInfo
Read-only FriendListInfo?: the friend count count and limit maxCount. null until the friend list has synced.
im.friends.check()
Checks the user's relationship with other users.
| Parameter | Type | Required | Description |
|---|---|---|---|
usernames | List<String> | Yes | 1 to 100 usernames |
Returns: Future<List<FriendCheck>> in request order. The user themself and nonexistent users are left out.
Errors: local_validation (usernames is empty or has more than 100).
im.friends.update()
Changes a friend's remark and custom attributes.
| Parameter | Type | Required | Description |
|---|---|---|---|
username | String | Yes | The friend's username |
remark | String? | No | Named. Remark, up to 64 characters. An empty string clears it |
attributes | Map<String, String?>? | No | Named. Custom attributes, merged key by key; keys set to null are deleted. Up to 16 keys and 1 KB in total |
clearAttributes | bool | No | Named. When true, clears all custom attributes. Default false |
Pass at least one of remark, attributes and clearAttributes.
Returns: Future<Friend>.
Errors: local_validation (none of them was given), not_found (not a friend), invalid_argument (remark or attributes break the rules).
im.friends.remove()
Removes a friend from both sides' friend lists. Succeeds even if not a friend.
| Parameter | Type | Required | Description |
|---|---|---|---|
username | String | Yes | The friend's username |
Returns: Future<void>.
im.friends.requests.send()
Sends a friend request. Never retried automatically; see Send a request for the rules. All parameters are named.
| Parameter | Type | Required | Description |
|---|---|---|---|
username | String | Yes | The other user's username |
message | String? | No | Message, up to 256 characters |
remark | String? | No | Remark to set for the other user once you're friends, up to 64 characters |
addSource | String? | No | Where the request came from, such as search or qrcode |
Returns: Future<FriendRequestResult>.
Errors: friend_add_denied, user_blocked, limit_exceeded, rate_limited, content_rejected, permission_denied, not_found (the user doesn't exist), timeout. See Send a request.
im.friends.requests.received()
Queries received friend requests, newest first. All parameters are named.
| Parameter | Type | Required | Description |
|---|---|---|---|
status | FriendRequestStatus? | No | List only requests with this status: pending, accepted, declined, expired |
cursor | String? | No | Cursor for the next page, from the previous page's nextCursor |
limit | int? | No | Items per page |
Returns: Future<DRPage<FriendRequest>>. Without cursor and status, and with limit no larger than the cached count, returns the first page held in SDK memory.
im.friends.requests.sent()
Queries friend requests the user sent. Parameters and return value are the same as im.friends.requests.received().
im.friends.requests.unreadCount
Read-only int: the number of unread received requests, up to 100. FriendRequestsChanged is emitted when it changes.
im.friends.requests.markRead()
Marks received requests as read.
| Parameter | Type | Required | Description |
|---|---|---|---|
readUntil | DateTime? | No | Named. The requestedAt of the newest request shown; requests at or before this time are marked as read. If omitted, the unread requests on the first page in SDK memory are used |
Returns: Future<void>.
im.friends.requests.accept()
Accepts a friend request; the two become friends.
| Parameter | Type | Required | Description |
|---|---|---|---|
username | String | Yes | The requester's username |
remark | String? | No | Named. Remark to set for the requester |
Returns: Future<Friend>, the new friend.
Errors: not_found (the request doesn't exist or has expired), limit_exceeded (friend list full), permission_denied (rejected by the before-add-friend callback).
im.friends.requests.decline()
Declines a friend request. The requester can't send the user another request for 24 hours.
| Parameter | Type | Required | Description |
|---|---|---|---|
username | String | Yes | The requester's username |
Returns: Future<void>.
Errors: not_found (the request doesn't exist, was already accepted, or has expired).
im.friends.blacklist.list()
Returns a live list of the blocklist (LiveList<BlacklistEntry>), most recently blocked first. Call dispose() when you're done with it.
im.friends.blacklist.has()
Checks synchronously whether the user has blocked someone. BlacklistChanged is emitted on changes.
| Parameter | Type | Required | Description |
|---|---|---|---|
username | String | Yes | Username |
Returns: bool.
im.friends.blacklist.add()
Blocks a user. Succeeds if already blocked.
| Parameter | Type | Required | Description |
|---|---|---|---|
username | String | Yes | Username |
Returns: Future<void>.
Errors: permission_denied (the app turned off blocklist enforcement; rejected locally), limit_exceeded (the blocklist already has 1000 people; details['reason'] is blacklist_limit), not_found (the user doesn't exist), invalid_argument (blocking yourself).
im.friends.blacklist.remove()
Unblocks a user. Succeeds if not blocked.
| Parameter | Type | Required | Description |
|---|---|---|---|
username | String | Yes | Username |
Returns: Future<void>.
im.friends.settings.get()
Gets the user's friend add mode.
Returns: Future<FriendAddSettings>. friendAddMode is the user's own setting, null if never set; effectiveFriendAddMode is the mode actually in effect.
im.friends.settings.set()
Sets the user's friend add mode.
| Parameter | Type | Required | Description |
|---|---|---|---|
mode | FriendAddMode? | Yes | needConfirm, allowAny or denyAny. null clears the setting and uses the app default |
Returns: Future<void>.
im.presence.subscribe()
Registers people whose presence to follow. Runs synchronously without waiting for the server.
| Parameter | Type | Required | Description |
|---|---|---|---|
usernames | List<String> | Yes | Usernames |
Returns: void Function(), a function that cancels this subscription. Safe to call more than once.
Errors (thrown synchronously): not_signed_in, permission_denied (presence_disabled). After the client is closed it does nothing, and the returned function does nothing either.
im.presence.get()
Returns the presence of a subscribed person synchronously. PresenceChanged is emitted on changes.
| Parameter | Type | Required | Description |
|---|---|---|---|
username | String | Yes | Username |
Returns: PresenceState?, null when unknown.
im.presence.query()
Queries presence once without subscribing.
| Parameter | Type | Required | Description |
|---|---|---|---|
usernames | List<String> | Yes | 1 to 100 usernames |
Returns: Future<List<PresenceState>>. Users that aren't visible are left out.
Errors: local_validation (empty or more than 100), permission_denied (presence_disabled), rate_limited.
im.devices.online()
Returns the user's devices that are online now, synchronously. DevicesChanged is emitted on changes.
Returns: List<OnlineDevice>.
im.devices.sessions()
Queries all of the user's active signed-in devices.
Returns: Future<List<LoginSession>>, most recently active first.
im.devices.kick()
Signs out one of the user's devices. Kicking the current device is the same as signing out. Succeeds if the device is already signed out.
| Parameter | Type | Required | Description |
|---|---|---|---|
sessionId | String | Yes | The device's sessionId |
Returns: Future<void>.
Errors: local_validation (sessionId is empty), not_found (not one of the user's devices).
Related events
Subscribe by type with im.on<T>(); see Events and errors.
| Event | Fields | Description |
|---|---|---|
MeChanged | me (SelfProfile?) | The user's profile changed |
MeMuteChanged | scope, muted, expiresAt | The user's global mute changed; scope is chat, group or room, and expiresAt: null means permanent |
UsersChanged | usernames | Cached profiles of these users (lowercase) changed |
FriendsChanged | upserted, removed | The friend list changed (usernames) |
BlacklistChanged | upserted, removed | The blocklist changed (usernames) |
FriendRequestsChanged | lists, unreadCount | Friend requests or the unread count changed; lists contains received and/or sent. The first page in SDK memory has been refreshed |
PresenceChanged | items, reset, cleared | Presence changed. When reset is true, all status was cleared; status for people in cleared was cleared |
PresenceSubscribeFailed | usernames, reason (PresenceFailure?) | Subscription failed; reason is denied, limitExceeded or presenceDisabled |
DevicesChanged | devices (List<OnlineDevice>) | The user's online devices changed |
DevicesNewLogin | sessionId, deviceName, platform, loginMethod, createdAt | The user signed in on a new device |
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?>), so you can read fields that have no getter.
SelfProfile
The user's own profile.
| Field | Type | Description |
|---|---|---|
username | String | Username |
nickname | String | Nickname; empty string if none |
avatarUrl | String | Avatar URL; empty string if none |
attributes | Map<String, String>? | Custom attributes; null if none |
hasPassword | bool | Whether a password is set |
version | int | Profile version, incremented on every change |
UserProfile
Another user's public profile.
| Field | Type | Description |
|---|---|---|
username | String | Username |
nickname | String | Nickname; empty string if none |
avatarUrl | String | Avatar URL; empty string if none |
attributes | Map<String, String>? | Custom attributes |
displayName | String | Nickname, or the username if there's none |
MyMute and GlobalMuteInfo
| Field | Type | Description |
|---|---|---|
MyMute.chat | GlobalMuteInfo? | Mute for one-to-one chats; null if not muted |
MyMute.group | GlobalMuteInfo? | Mute for group chats |
MyMute.room | GlobalMuteInfo? | Mute for chat rooms |
GlobalMuteInfo.expiresAt | DateTime? | When the mute ends; null for a permanent mute |
Friend
A friend, from the user's point of view.
| Field | Type | Description |
|---|---|---|
username | String | The friend's username |
nickname | String | The friend's nickname |
avatarUrl | String | The friend's avatar URL |
remark | String | Remark the user set for the friend; empty string if none |
attributes | Map<String, String>? | Custom attributes the user set for the friend |
addSource | String | Where the friend was added from; empty string if none |
createdAt | DateTime? | When they became friends |
displayName | String | First non-empty of remark, nickname and username (the friend list is sorted by it) |
FriendListInfo
| Field | Type | Description |
|---|---|---|
count | int | Number of friends |
maxCount | int | Friend limit |
FriendRequest
A friend request from the user's point of view: in received requests "the other user" is the requester, in sent requests it's the recipient.
| Field | Type | Description |
|---|---|---|
username | String | The other user's username |
nickname | String | The other user's nickname |
avatarUrl | String | The other user's avatar URL |
message | String | The message; empty string if none |
addSource | String | Where the request came from |
status | FriendRequestStatus? | pending, accepted (now friends), declined or expired; null for an unknown status (the original value is in raw['status']) |
unread | bool | Only meaningful for received requests: whether the user hasn't viewed it yet |
remark | String? | Sent requests only: the remark preset for the other user |
requestedAt | DateTime? | When it was last sent |
expiresAt | DateTime? | When it expires; only set for pending requests |
handledAt | DateTime? | When it was accepted, declined or the two became friends |
FriendRequestResult
| Field | Type | Description |
|---|---|---|
status | String | pending (waiting for the other user) or accepted (now friends) |
friend | Friend? | The friend when you became friends directly; absent when success was confirmed by a check after a timeout |
accepted | bool | Whether status is accepted |
FriendCheck
| Field | Type | Description |
|---|---|---|
username | String | The other user's username |
isFriend | bool | Whether they're friends |
blocked | bool | Whether the user has blocked the other user |
request | String? | An unexpired pending request between them: sent by the user, or received from the other user; null if none |
BlacklistEntry
| Field | Type | Description |
|---|---|---|
username | String | The blocked username |
nickname | String | Nickname |
avatarUrl | String | Avatar URL |
createdAt | DateTime? | When they were blocked |
FriendAddSettings
| Field | Type | Description |
|---|---|---|
friendAddMode | FriendAddMode? | The user's own setting; null if never set |
effectiveFriendAddMode | FriendAddMode? | The mode in effect |
FriendAddMode and FriendRequestStatus
FriendAddMode is the enum allowAny, needConfirm, denyAny, with wire as the server value (allow_any and so on); see Friend add mode. FriendRequestStatus is the enum pending, accepted, declined, expired.
PresenceState
| Field | Type | Description |
|---|---|---|
username | String | Username |
online | bool | Whether online |
platforms | List<String> | Platforms online, such as ios, android, web, windows; empty when offline |
lastSeenAt | DateTime? | When last online. null when online, not seen in 30 days, or the app doesn't allow viewing it |
version | int | Status version |
PresenceFailure
Enum: denied (doesn't exist or isn't visible), limitExceeded (over 3000 per device, or the server's total subscription count is full), presenceDisabled (presence is off for the app).
OnlineDevice
One of the user's devices that's online now.
| Field | Type | Description |
|---|---|---|
sessionId | String | The device's session ID, same as in LoginSession |
platform | String | Platform |
connectedAt | DateTime? | When it connected |
current | bool | Whether it's this device |
LoginSession
One of the user's active sessions.
| Field | Type | Description |
|---|---|---|
sessionId | String | Session ID, used with kick() |
deviceId | String? | Device ID |
deviceName | String? | Device name |
platform | String? | Platform: ios, android, harmony, windows, macos, linux, web, mini_program or other |
current | bool | Whether it's this device |
raw also has sdk_version (client version), login_method (ticket or password), ip (IP at sign-in), created_at (sign-in time), last_active_at (last sign-in or token refresh) and expires_at (when the session expires; null means never), all as strings.
