Alerts, do not disturb and reports
This page covers three things: deciding whether to alert the user when a new message arrives while the app is in the foreground; reading and changing the user's push settings (do not disturb, quiet hours, preview mode) and conversation mutes with im.push; and reporting messages, users, groups and chat rooms with im.reports.
Alerts while the app is in the background or killed come from offline push; see Offline push. The push settings and conversation mutes here decide both offline push and in-app alerts.
| You want to | See |
|---|---|
| Alert for new messages while the app is in the foreground | Alerts for new messages |
| Do not disturb, quiet hours, what notifications show | Push settings |
| Mute one chat or group | Conversation mutes |
| Report a message, user, group or chat room | Reports |
Alerts for new messages
The Flutter SDK never shows in-app alerts or plays sounds by itself; you decide when you receive the MessageReceived event. The event's notify has already decided, from the user's push settings and conversation mutes, whether this message should alert, and gives the alert's title and body (NotifyDecision):
im.on<MessageReceived>().listen((e) {
final decision = e.notify;
if (!decision.notify) return;
// 用户正在看这个会话时不需要提醒
if (e.message.conversationId == currentConversationId) return;
showBanner(decision.title ?? '新消息', decision.body ?? '', conversationId: e.message.conversationId ?? '');
});MessageReceivedis only emitted for messages from others received in real time over the connection. Messages from while offline are filled in by sync after reconnecting and don't emit this event, so the user doesn't get a burst of alerts on coming online.- The conversation the user is looking at usually doesn't need an alert. The SDK's decision doesn't take this into account, so check it yourself.
- On phones, only alert inside the app. While the app is in the background, the server treats the device as not in the foreground and pushes the message, and the system shows a notification. Don't show it again with a local notification plugin, or the user gets two.
- On desktops (macOS and others). A desktop app holding a connection always counts as in the foreground, so the server doesn't push. Show a system notification yourself with a local notification plugin when you receive
MessageReceived.
The alert decision
notify is the result of im.push.shouldNotify(message). You can also call it yourself for any message:
final decision = im.push.shouldNotify(message);
if (decision.notify) {
print('${decision.title}:${decision.body}');
} else {
print('不提醒:${decision.reason}');
}When it shouldn't alert, reason says why:
reason | Description |
|---|---|
self | The user's own message |
tip | A group notice. Being removed, group dissolution and so on are known from group events; see Groups |
call | A call record. Incoming calls are alerted separately through CallIncoming; see Audio and video calls |
excluded | Sent with exclude_from_unread |
not_loaded | Push settings haven't synced yet (right after sign-in); decide for yourself whether to alert |
dnd | The user turned on do not disturb |
quiet_hours | It's currently within quiet hours |
muted | This conversation is muted |
not_mentioned | The group is set to "alert only when mentioned", and this message doesn't mention the user or everyone |
When it should alert, title and body follow the user's preview mode (effectivePreview) and match the push the user would get on their phone. The SDK uses its built-in Chinese templates:
| Preview mode | One-to-one | Group |
|---|---|---|
full | Title is the sender; body is the message summary | Title is the group name; body is "sender: summary", prefixed with "[有人@我]" (someone mentioned you) when the user is mentioned |
senderOnly | Title is the sender; body is "发来一条新消息" (sent a new message) | Title is the group name; body is "sender 发来一条新消息" |
none | Title is empty; body is "你收到了一条新消息" (you received a new message) | Same |
The sender is shown by nickname (username if there's no nickname); friend remarks and group nicknames aren't used. Message summaries follow the same rules as server pushes: the first 100 characters of text, "[图片]" for images, "[文件] filename" for files, and "[消息]" for custom messages.
This decision approximates the server's offline push rules, with these differences:
push.disabledandpush.forceset when sending aren't delivered with the message, so they're treated as unset.- Mentioning everyone is treated as breaking through "alert only when mentioned" (the server's default).
- Title and body only use the built-in Chinese templates, not the notification templates you override in the console.
Push settings
A user's push settings are do not disturb, quiet hours and preview mode. They decide the offline pushes the user gets, and they also drive the in-app alert decision above. Settings are stored on the server and synced across all of the user's devices: change them on one phone and pushes on their other devices follow.
im.push.settings() reads the locally synced settings synchronously, and PushChanged is emitted when they change:
void renderSettings() {
final s = im.push.settings();
if (s == null) return; // 还没有同步到
final until = s.dndUntil;
final quiet = s.quietHours;
render({
'dnd': !s.dndEnabled ? '关闭' : (until == null ? '一直免打扰' : '免打扰至 ${until.toLocal()}'),
'quietHours': quiet == null ? '关闭' : '${quiet.start}~${quiet.end}',
'preview': s.effectivePreview.wire, // full / sender_only / none
});
}
im.on<PushChanged>().where((e) => e.settings).listen((_) => renderSettings());
renderSettings();When updating, pass only what you're changing:
// 8 小时内免打扰
await im.push.updateSettings(dnd: const DndChange.on(durationSeconds: 8 * 3600));
// 一直免打扰;关闭免打扰
await im.push.updateSettings(dnd: const DndChange.on());
await im.push.updateSettings(dnd: const DndChange.off());
// 每天 22:00 到次日 07:30 免打扰,按上海时间
await im.push.updateSettings(
quietHours: const QuietHoursChange.set(QuietHours(start: '22:00', end: '07:30', timezone: 'Asia/Shanghai')),
);
await im.push.updateSettings(quietHours: const QuietHoursChange.off());
// 通知只显示发送者;恢复为应用的默认方式
await im.push.updateSettings(preview: const PreviewChange.set(PushPreview.senderOnly));
await im.push.updateSettings(preview: const PreviewChange.useDefault());- Do not disturb. No pushes for a period (1 second to 10 years) or indefinitely. When it ends, the SDK clears it locally and emits
PushChanged. - Quiet hours. A daily time range. If
endis earlier thanstart, the range crosses midnight; the start time is inside the range and the end time isn't. Times areHH:MM(24-hour). The time zone is an IANA name (such asAsia/Shanghai). Dart'sDateTime.now().timeZoneNameis an abbreviation (such asCST) and can't be used directly; get the device's IANA time zone with a plugin such asflutter_timezone. The time zone doesn't follow the device, so users who change time zones need to set it again. - Preview mode.
fullshows the sender and content,senderOnlyonly the sender, andnoneneither.previewis the user's own choice, andnullmeans the app default;effectivePreviewis the mode actually in effect.
Conversation mutes
Mute a one-to-one chat or a group:
// 单聊:不提醒
await im.push.mute(const MuteTarget.single('lisi'), mode: MuteMode.none);
// 群:只在有人 @ 我时提醒,24 小时后自动恢复
await im.push.mute(MuteTarget.group(groupId), mode: MuteMode.mentionOnly, durationSeconds: 86400);
// 取消
await im.push.unmute(MuteTarget.group(groupId));
// 读取(同步)
final mute = im.push.getMute(MuteTarget.group(groupId));
print('${mute?.muteMode} ${mute?.until}');MuteMode.nonemeans no alerts;MuteMode.mentionOnlyalerts only when the user or everyone is mentioned, for groups only.- One-to-one chats only allow
MuteMode.none, and you can't mute yourself. The SDK rejects both locally withlocal_validation(reasoninvalid_modeandself_conversationrespectively). - Without
durationSecondsthe mute lasts indefinitely. With one (1 second to 10 years), it ends automatically; the SDK updates the conversation list and emitsPushChangedwhen it expires locally. - Once set, the conversation's
mutedfield in the conversation list changes (see Conversations),im.conversations.getUnreadTotal(excludeMuted: true)stops counting it, and by default the badge reported when going to the background doesn't count it either. - Mutes only affect pushes and alerts, not message delivery or unread counts. Leaving or being removed from a group, or the group being dissolved, deletes that group's mute. Each user can have up to 10000 conversation mutes.
In the PushChanged event, settings: true means the push settings changed, and mutes lists conversations whose mute changed (MuteTarget). Changes made on other devices sync in real time too:
im.on<PushChanged>().listen((e) {
for (final target in e.mutes) {
final mute = im.push.getMute(target);
print('${target.conversationType} ${target.target}:${mute == null ? '已取消免打扰' : '免打扰'}');
}
});Reports
Users can report a message, a user, a group, a chat room or a chat room message. Reports go into the moderation records in your console for reviewers to handle; see Content moderation.
// 应用关闭了客户端举报时,隐藏举报入口
final canReport = im.config?.reportEnabled ?? false;
if (canReport) {
// 举报一条消息
final result = await im.reports.submit(const ReportTarget.message('99584445836689408'), reason: ReportReason.fraud);
showToast(result.duplicate ? '你已举报过' : '已举报,我们会尽快处理');
}
// 举报一个用户,附上他发来的消息作为证据(最多 5 条)
await im.reports.submit(
const ReportTarget.user('lisi', evidenceMessageIds: ['99584445836689408', '99584445836689409']),
reason: ReportReason.abuse,
description: '多次发送辱骂信息',
);
// 本人 90 天内的举报
final page = await im.reports.mine(limit: 20);
for (final r in page.items) {
print('${r.targetType} ${r.reason} ${r.status == 'pending' ? '处理中' : '已处理'}');
}- Entry point. When
reportEnabledin the runtime configuration isfalse, the app has turned off client reports; hide the report option. Calls then reject withpermission_denied(reasonreport_disabled). - Reasons. Choose from 10 fixed reasons; see ReportReason. The description is up to 500 characters, with line breaks allowed.
- Evidence. When reporting a user or group, attach up to 5 message
messageIds. Evidence for a user report must be sent by that user, and for a group report must be from that group; recalled messages aren't allowed. The SDK checks against locally stored messages and rejects withlocal_validation(reasoninvalid_evidence) if they don't qualify. In your UI, only let users select qualifying messages. - Only what the user can see. Group messages from before joining or after leaving, and messages the user deleted or that were recalled, can't be reported; the server returns
not_found. Chat room messages can only be reported within 24 hours; if one can't be read, report the chat room or the sender instead. Users can't report themselves (local_validation,reasonself_report). - Duplicate reports. One user reporting the same target counts once. Reporting again returns the original report with
duplicate: true; still tell the user it was reported. - Rate. 5 per user per minute and 30 per day. Beyond that, calls reject with
rate_limitedand aren't retried. - Outcome. Reporters only see "pending" (
pending) or "handled" (closed), not the decision.
API reference
Methods returning a Future throw DRException on failure: not_signed_in when signed out, and network_error or timeout for network errors. For error codes, see Events and errors and the server error codes.
im.push.settings()
Reads the local push settings synchronously. Do not disturb that has expired counts as off. PushChanged (with settings: true) is emitted when they change.
Returns: PushSettings?, null until synced. See PushSettings.
im.push.updateSettings()
Changes push settings, only the items given.
| Parameter | Type | Required | Description |
|---|---|---|---|
dnd | DndChange? | No (named) | Do not disturb: DndChange.on(durationSeconds: ...) turns it on, with durationSeconds from 1 to 315360000 seconds, or indefinitely if omitted; DndChange.off() turns it off |
quietHours | QuietHoursChange? | No (named) | Quiet hours: QuietHoursChange.set(QuietHours(...)) sets them, QuietHoursChange.off() turns them off |
preview | PreviewChange? | No (named) | Preview mode: PreviewChange.set(PushPreview.xxx) sets it, PreviewChange.useDefault() uses the app default |
Give at least one.
Returns: Future<PushSettings>, the full updated settings.
Errors: local_validation (reason is required: nothing given or time zone missing; out_of_range: duration out of range; invalid_format: bad time format), invalid_argument (invalid_quiet_hours: start equals end; invalid_timezone: not an IANA time zone name).
im.push.mute()
Mutes a conversation.
| Parameter | Type | Required | Description |
|---|---|---|---|
target | MuteTarget | Yes | MuteTarget.single(username) or MuteTarget.group(groupId) |
mode | MuteMode | Yes (named) | MuteMode.none: no alerts. MuteMode.mentionOnly: alert only when the user is mentioned; groups only |
durationSeconds | int? | No (named) | Duration, 1 to 315360000 seconds. Indefinite if omitted |
Returns: Future<void>.
Errors: local_validation (reason is invalid_mode: mentionOnly on a one-to-one chat; self_conversation: muting yourself; out_of_range: duration out of range; required: empty target), limit_exceeded (10000 conversation mutes reached).
im.push.unmute()
Unmutes a conversation.
| Parameter | Type | Required | Description |
|---|---|---|---|
target | MuteTarget | Yes | See MuteTarget |
Returns: Future<void>.
Errors: local_validation (empty target, or yourself).
im.push.getMute()
Reads one conversation's mute synchronously. PushChanged (with this conversation in mutes) is emitted when it changes.
| Parameter | Type | Required | Description |
|---|---|---|---|
target | MuteTarget | Yes | See MuteTarget |
Returns: MuteInfo?, null if not set or expired. See MuteInfo.
im.push.shouldNotify()
Decides, from the local push settings and conversation mutes, whether a new message should alert, and gives the alert's title and body. Runs synchronously.
| Parameter | Type | Required | Description |
|---|---|---|---|
message | MessageView | Yes | The message; see Messages |
Returns: NotifyDecision; see NotifyDecision.
im.reports.submit()
Submits a report. Never retried automatically.
| Parameter | Type | Required | Description |
|---|---|---|---|
target | ReportTarget | Yes | What to report; see ReportTarget |
reason | String | Yes (named) | The reason, one of the constants in ReportReason |
description | String? | No (named) | Description, up to 500 characters, line breaks allowed |
Returns: Future<ReportSubmitResult>. duplicate: true means a repeat report, and report is the original one.
Errors: local_validation (reason is invalid_value: bad reason; required: target ID missing; too_long: description over 500 characters; control_characters: control characters in the description; self_report: reporting yourself; invalid_evidence: evidence doesn't qualify; too_many_items: more than 5 pieces of evidence), permission_denied (report_disabled), not_found (the target doesn't exist or isn't visible to the user), invalid_argument (invalid_evidence), rate_limited.
im.reports.mine()
Queries the user's reports from the last 90 days, page by page.
| Parameter | Type | Required | Description |
|---|---|---|---|
cursor | String? | No (named) | nextCursor from the previous page |
limit | int? | No (named) | Items per page |
Returns: Future<DRPage<Report>>: items are this page's reports, and nextCursor is the next page's cursor, or null if there isn't one.
Types
NotifyDecision
| Field | Type | Description |
|---|---|---|
notify | bool | Whether to alert |
title | String? | Given when alerting: the title; empty when the preview mode is none |
body | String? | Given when alerting: the body |
reason | String? | Given when not alerting: self, tip, call, excluded, not_loaded, dnd, quiet_hours, muted, not_mentioned |
PushSettings
Push settings; the raw data is in raw, with the same fields as the server's push settings.
| Field | Type | Description |
|---|---|---|
dndEnabled | bool | Whether do not disturb is on (false once expired) |
dndUntil | DateTime? | When do not disturb ends (UTC); null when on indefinitely or off, so use dndEnabled to tell them apart |
quietHours | QuietHours? | Quiet hours; null if not set |
preview | PushPreview? | The user's chosen preview mode; null if not chosen |
effectivePreview | PushPreview | The preview mode in effect |
settingsVersion | int | Settings version, used by the SDK for sync |
QuietHours
| Field | Type | Description |
|---|---|---|
start | String | Start time, HH:MM |
end | String | End time, HH:MM; earlier than start means the range crosses midnight |
timezone | String | IANA time zone name, such as Asia/Shanghai |
PushPreview
Enum: full (show sender and content), senderOnly (show only the sender), none (show neither). wire is the server value (full, sender_only, none).
DndChange, QuietHoursChange, PreviewChange
Arguments to updateSettings():
| Constructor | Description |
|---|---|
DndChange.on({int? durationSeconds}) | Turns on do not disturb, ending after durationSeconds seconds; indefinitely if omitted |
DndChange.off() | Turns off do not disturb |
QuietHoursChange.set(QuietHours value) | Sets quiet hours |
QuietHoursChange.off() | Turns off quiet hours |
PreviewChange.set(PushPreview value) | Sets the preview mode |
PreviewChange.useDefault() | Uses the app default |
MuteTarget
| Constructor | Description |
|---|---|
MuteTarget.single(String username) | The one-to-one chat with this user |
MuteTarget.group(String groupId) | This group |
Fields: conversationType (single or group) and target (username or group ID). Two MuteTargets are equal when type and target match, so they can be used as Map keys.
MuteMode
Enum: none (no alerts), mentionOnly (alert only when mentioned; groups only). wire is the server value (none, mention_only).
MuteInfo
| Field | Type | Description |
|---|---|---|
mode | String | none or mention_only |
muteMode | MuteMode? | mode as an enum; null for unknown values |
until | DateTime? | When it ends; null for indefinitely |
PushChanged
| Field | Type | Description |
|---|---|---|
settings | bool | Whether the push settings changed |
mutes | List<MuteTarget> | Conversations whose mute changed |
ReportTarget
What to report (a sealed class):
| Constructor | targetType | Description |
|---|---|---|
ReportTarget.message(String messageId) | message | Report a one-to-one or group message |
ReportTarget.user(String username, {List<String>? evidenceMessageIds}) | user | Report a user, optionally with up to 5 messages they sent |
ReportTarget.group(String groupId, {List<String>? evidenceMessageIds}) | group | Report a group, optionally with up to 5 messages from it |
ReportTarget.chatroom(String roomId) | chatroom | Report a chat room |
ReportTarget.chatroomMessage(String roomId, String messageId) | chatroom_message | Report a chat room message |
ReportReason
Report reason constants (String); ReportReason.all holds every value:
| Constant | Value | Meaning |
|---|---|---|
ReportReason.ad | ad | Ads or harassment |
ReportReason.fraud | fraud | Fraud |
ReportReason.porn | porn | Sexual or vulgar content |
ReportReason.politics | politics | Political content |
ReportReason.terrorism | terrorism | Violence or terrorism |
ReportReason.abuse | abuse | Abuse or attacks |
ReportReason.illegal | illegal | Illegal or prohibited content |
ReportReason.infringement | infringement | Infringement |
ReportReason.minor | minor | Involves minors |
ReportReason.other | other | Other |
ReportSubmitResult
| Field | Type | Description |
|---|---|---|
report | Report | This report; for a repeat report, the original one |
duplicate | bool | Whether it's a repeat report |
Report
The raw data is in raw.
| Field | Type | Description |
|---|---|---|
reportId | String | Report ID |
targetType | String | Target type, as in ReportTarget |
username | String? | The reported user (for user reports) |
messageId | String? | The reported message |
groupId | String? | The reported group |
roomId | String? | The reported chat room |
reason | String | Reason; see ReportReason |
status | String | pending or closed |
createdAt | DateTime? | When it was reported |
