提醒、免打扰与举报
本页介绍三件事:App 在前台收到新消息时怎样决定是否提醒用户;用 im.push 读取和修改用户的推送设置(全局免打扰、夜间免打扰、预览方式)与会话免打扰;用 im.reports 举报消息、用户、群和聊天室。
App 在后台或被杀时的提醒由离线推送完成,见离线推送。这里的推送设置和会话免打扰同时决定离线推送和 App 内的提醒。
收到消息时提醒
Flutter SDK 不会自己在 App 内弹出提示或播放提示音,由你在收到 MessageReceived 事件时决定。事件的 notify 已经按用户的推送设置和会话免打扰判断好这条消息是否应该提醒,并给出了提醒的标题和正文(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 ?? '');
});MessageReceived只对通过长连接实时收到的、别人发来的消息发出;离线期间的消息在重新连接后由同步补齐,不发出这个事件,也就不会在上线时弹出一连串提示。- 用户正在看的会话通常不需要提醒,SDK 的判断不考虑这一点,请自行判断。
- 手机上只做 App 内的提醒:App 在后台时,服务端认为这台设备不在前台,会推送这条消息,由系统显示通知。不要再用本地通知插件显示一次,否则用户会收到两条。
- 桌面端(macOS 等):桌面 App 保持长连接时一直算在前台,服务端不推送,需要你在收到
MessageReceived时用本地通知插件显示系统通知。
提醒的判断
notify 就是 im.push.shouldNotify(message) 的结果。也可以对任何一条消息自己调用:
final decision = im.push.shouldNotify(message);
if (decision.notify) {
print('${decision.title}:${decision.body}');
} else {
print('不提醒:${decision.reason}');
}不提醒时 reason 说明原因:
reason | 说明 |
|---|---|
self | 本人发的消息 |
tip | 群提示。被移出群、群解散等由群组的事件得知,见群组 |
call | 通话记录。来电由 CallIncoming 单独提醒,见音视频通话 |
excluded | 发送时设置了 exclude_from_unread |
not_loaded | 推送设置还没有同步到(刚登录时),可以自行决定是否提醒 |
dnd | 用户开启了全局免打扰 |
quiet_hours | 当前在夜间免打扰时段内 |
muted | 这个会话设置了免打扰 |
not_mentioned | 群设置了“只提醒 @ 我”,这条消息没有 @ 本人或全体成员 |
提醒时 title 和 body 按用户的预览方式(effectivePreview)生成,与手机上收到的推送一致:
| 预览方式 | 单聊 | 群聊 |
|---|---|---|
full | 标题为发送者,正文为消息摘要 | 标题为群名,正文为“发送者: 摘要”,@ 了本人的前面加“[有人@我]” |
senderOnly | 标题为发送者,正文为“发来一条新消息” | 标题为群名,正文为“某某 发来一条新消息” |
none | 标题为空,正文为“你收到了一条新消息” | 同左 |
发送者取昵称(没有昵称为用户名),不使用好友备注和群昵称。消息摘要的规则与服务端的推送相同:文本取前 100 个字符,图片为“[图片]”,文件为“[文件] 文件名”,自定义消息为“[消息]”。
这个判断与服务端的离线推送规则近似,有以下差别:
- 发送消息时的
push.disabled、push.force不随消息下发,按都没有设置处理; - @ 全体成员按能突破“只提醒 @ 我”处理(服务端的默认值);
- 标题和正文只使用内置的中文模板,不使用你在控制台中覆盖的通知模板。
推送设置
用户的推送设置包括全局免打扰、夜间免打扰和预览方式。它们决定用户收到的离线推送,也是上面 App 内提醒判断的依据。设置保存在服务端,在用户的全部设备间同步:用户在一台手机上修改后,他其他设备上的推送随之变化。
im.push.settings() 同步读取本地已同步到的设置,变化时发出 PushChanged 事件:
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();修改时只给出要改的项:
// 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());- 全局免打扰:一段时间内(1 秒到 10 年)或一直不推送。到期后 SDK 在本地自动恢复,发出
PushChanged。 - 夜间免打扰:每天的一个时段,
end早于start表示跨过午夜,开始时刻算在时段内、结束时刻不算。时刻为HH:MM(24 小时制);时区为 IANA 时区名称(如Asia/Shanghai)。Dart 的DateTime.now().timeZoneName是时区的缩写(如CST),不能直接使用,可以用flutter_timezone等插件取得设备的 IANA 时区。时区不随设备变化,用户换了时区需要重新设置。 - 预览方式:
full显示发送者和内容,senderOnly只显示发送者,none都不显示。preview是用户自己的选择,为null时使用应用的默认方式;effectivePreview是实际生效的方式。
会话免打扰
对一个单聊或群设置免打扰:
// 单聊:不提醒
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.none不提醒;MuteMode.mentionOnly只在 @ 本人或全体成员时提醒,只用于群。- 单聊只能设置
MuteMode.none;不能对自己设置。这两种情况 SDK 在本地以local_validation拒绝(reason分别为invalid_mode、self_conversation)。 - 省略
durationSeconds一直有效;给出的(1 秒到 10 年)到期后自动恢复,SDK 在本地到期时更新会话列表并发出PushChanged。 - 设置后会话列表中这个会话的
muted随之变化(见会话),im.conversations.getUnreadTotal(excludeMuted: true)不再计入它,切到后台时上报的角标默认也不计入它。 - 免打扰只影响推送和提醒,不影响消息的接收和未读数。退出或被移出群、群解散后,对这个群的免打扰随之删除。每个用户最多 10000 个会话免打扰。
PushChanged 事件的 settings 为 true 表示推送设置变了;mutes 列出免打扰变化了的会话(MuteTarget)。其他设备上的修改同样实时同步过来:
im.on<PushChanged>().listen((e) {
for (final target in e.mutes) {
final mute = im.push.getMute(target);
print('${target.conversationType} ${target.target}:${mute == null ? '已取消免打扰' : '免打扰'}');
}
});举报
用户可以举报一条消息、一个用户、一个群、一个聊天室或一条聊天室消息。举报进入你在控制台中的审核记录,由审核人员处理,见内容安全。
// 应用关闭了客户端举报时,隐藏举报入口
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' ? '处理中' : '已处理'}');
}- 入口:运行配置的
reportEnabled为false时应用关闭了客户端举报,请隐藏举报入口;这时调用以permission_denied(reason为report_disabled)拒绝。 - 原因:只能从固定的 10 种中选择,见 ReportReason。说明最多 500 个字符,可以有换行。
- 证据:举报用户和群时可以附上最多 5 条消息的
messageId。举报用户的证据必须是被举报的人发的,举报群的必须是这个群里的,都不能是已撤回的;SDK 先用本地保存的消息检查,不符合的以local_validation(reason为invalid_evidence)拒绝。请在界面上只让用户勾选符合条件的消息。 - 只能举报看得到的内容:入群之前、离开群之后的群消息,本人已删除的、已撤回的消息不能举报,服务端返回
not_found。聊天室消息只能举报最近 24 小时内的,读不到时可以改为举报聊天室或发送者。不能举报自己(local_validation,reason为self_report)。 - 重复举报:同一个人对同一个对象只算一次,再次举报返回原来的那一次,
duplicate为true,同样提示“已举报”。 - 频率:每个用户每分钟 5 次、每天 30 次,超出时以
rate_limited拒绝,不会自动重试。 - 结果:举报人只能看到“处理中”(
pending)和“已处理”(closed),看不到处理结论。
接口参考
返回 Future 的方法失败时抛出 DRException;没有登录时为 not_signed_in,网络错误为 network_error、timeout。错误码见事件与错误处理和服务端的错误码。
im.push.settings()
同步读取本地的推送设置。全局免打扰已到期的视为关闭。变化时发出 PushChanged(settings 为 true)。
返回值:PushSettings?,还没有同步到时为 null。见 PushSettings。
im.push.updateSettings()
修改推送设置,只修改给出的项。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
dnd | DndChange? | 否(命名参数) | 全局免打扰:DndChange.on(durationSeconds: ...) 开启,durationSeconds 为 1 到 315360000 秒,省略为一直开启;DndChange.off() 关闭 |
quietHours | QuietHoursChange? | 否(命名参数) | 夜间免打扰:QuietHoursChange.set(QuietHours(...)) 设置,QuietHoursChange.off() 关闭 |
preview | PreviewChange? | 否(命名参数) | 预览方式:PreviewChange.set(PushPreview.xxx) 设置,PreviewChange.useDefault() 使用应用的默认方式 |
至少给出一项。
返回值:Future<PushSettings>,修改后的完整设置。
可能的错误:local_validation(reason 为 required,一项也没有给出或缺少时区;out_of_range,时长超出范围;invalid_format,时刻的格式不对)、invalid_argument(invalid_quiet_hours,开始等于结束;invalid_timezone,时区不是 IANA 时区名称)。
im.push.mute()
设置会话免打扰。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
target | MuteTarget | 是 | MuteTarget.single(username) 或 MuteTarget.group(groupId) |
mode | MuteMode | 是(命名参数) | MuteMode.none 不提醒;MuteMode.mentionOnly 只在 @ 本人时提醒,只用于群 |
durationSeconds | int? | 否(命名参数) | 时长,1 到 315360000 秒;省略一直有效 |
返回值:Future<void>。
可能的错误:local_validation(reason 为 invalid_mode,单聊设置了 mentionOnly;self_conversation,对自己设置;out_of_range,时长超出范围;required,目标为空)、limit_exceeded(会话免打扰已达 10000 个)。
im.push.unmute()
取消会话免打扰。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
target | MuteTarget | 是 | 见 MuteTarget |
返回值:Future<void>。
可能的错误:local_validation(目标为空、对自己设置)。
im.push.getMute()
同步读取一个会话的免打扰。变化时发出 PushChanged(mutes 中列出这个会话)。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
target | MuteTarget | 是 | 见 MuteTarget |
返回值:MuteInfo?,没有设置或已到期时为 null。见 MuteInfo。
im.push.shouldNotify()
按本地的推送设置和会话免打扰,判断一条新消息是否应该提醒,并给出提醒的标题和正文。同步执行。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
message | MessageView | 是 | 消息,见消息 |
返回值:NotifyDecision,见 NotifyDecision。
im.reports.submit()
提交举报。不会自动重试。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
target | ReportTarget | 是 | 举报的对象,见 ReportTarget |
reason | String | 是(命名参数) | 原因,取 ReportReason 中的常量 |
description | String? | 否(命名参数) | 说明,最多 500 个字符,可以有换行 |
返回值:Future<ReportSubmitResult>。duplicate 为 true 表示重复举报,report 为原来的那一次。
可能的错误:local_validation(reason 为 invalid_value,原因不对;required,缺少对象的 ID;too_long,说明超过 500 个字符;control_characters,说明中有控制字符;self_report,举报自己;invalid_evidence,证据不符合要求;too_many_items,证据超过 5 条)、permission_denied(report_disabled)、not_found(对象不存在或本人看不到)、invalid_argument(invalid_evidence)、rate_limited。
im.reports.mine()
查询本人 90 天内的举报,翻页。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
cursor | String? | 否(命名参数) | 上一页返回的 nextCursor |
limit | int? | 否(命名参数) | 每页条数 |
返回值:Future<DRPage<Report>>:items 为本页的举报,nextCursor 为下一页的游标,没有下一页时为 null。
数据结构
NotifyDecision
| 字段 | 类型 | 说明 |
|---|---|---|
notify | bool | 是否应该提醒 |
title | String? | 提醒时给出:标题,预览方式为 none 时为空字符串 |
body | String? | 提醒时给出:正文 |
reason | String? | 不提醒时给出原因:self、tip、call、excluded、not_loaded、dnd、quiet_hours、muted、not_mentioned |
PushSettings
推送设置,原始数据在 raw 中,字段与服务端的推送设置相同。
| 字段 | 类型 | 说明 |
|---|---|---|
dndEnabled | bool | 是否开启了全局免打扰(已到期的为 false) |
dndUntil | DateTime? | 全局免打扰的到期时间(UTC);一直开启或没有开启为 null,用 dndEnabled 区分 |
quietHours | QuietHours? | 夜间免打扰,没有设置为 null |
preview | PushPreview? | 用户选择的预览方式,没有选择为 null |
effectivePreview | PushPreview | 实际生效的预览方式 |
settingsVersion | int | 设置的版本号,SDK 同步用 |
QuietHours
| 字段 | 类型 | 说明 |
|---|---|---|
start | String | 开始时刻,HH:MM |
end | String | 结束时刻,HH:MM;早于 start 表示跨过午夜 |
timezone | String | IANA 时区名称,如 Asia/Shanghai |
PushPreview
枚举:full 显示发送者和内容、senderOnly 只显示发送者、none 都不显示。wire 为服务端的取值(full、sender_only、none)。
DndChange、QuietHoursChange、PreviewChange
updateSettings() 的参数:
| 构造函数 | 说明 |
|---|---|
DndChange.on({int? durationSeconds}) | 开启全局免打扰,durationSeconds 秒后到期;省略为一直开启 |
DndChange.off() | 关闭全局免打扰 |
QuietHoursChange.set(QuietHours value) | 设置夜间免打扰 |
QuietHoursChange.off() | 关闭夜间免打扰 |
PreviewChange.set(PushPreview value) | 设置预览方式 |
PreviewChange.useDefault() | 使用应用的默认方式 |
MuteTarget
| 构造函数 | 说明 |
|---|---|
MuteTarget.single(String username) | 与这个用户的单聊 |
MuteTarget.group(String groupId) | 这个群 |
字段 conversationType(single 或 group)和 target(用户名或群 ID)。两个 MuteTarget 的类型和目标相同即相等,可以用作 Map 的键。
MuteMode
枚举:none 不提醒、mentionOnly 只提醒 @ 我(只用于群)。wire 为服务端的取值(none、mention_only)。
MuteInfo
| 字段 | 类型 | 说明 |
|---|---|---|
mode | String | none 或 mention_only |
muteMode | MuteMode? | mode 的枚举形式,不认识的取值为 null |
until | DateTime? | 到期时间;一直有效为 null |
PushChanged
| 字段 | 类型 | 说明 |
|---|---|---|
settings | bool | 推送设置是否变了 |
mutes | List<MuteTarget> | 免打扰变化了的会话 |
ReportTarget
举报的对象(sealed class):
| 构造函数 | targetType | 说明 |
|---|---|---|
ReportTarget.message(String messageId) | message | 举报一条单聊或群消息 |
ReportTarget.user(String username, {List<String>? evidenceMessageIds}) | user | 举报一个用户,可以附上最多 5 条他发的消息 |
ReportTarget.group(String groupId, {List<String>? evidenceMessageIds}) | group | 举报一个群,可以附上最多 5 条这个群里的消息 |
ReportTarget.chatroom(String roomId) | chatroom | 举报一个聊天室 |
ReportTarget.chatroomMessage(String roomId, String messageId) | chatroom_message | 举报一条聊天室消息 |
ReportReason
举报原因的常量(String),ReportReason.all 为全部取值:
| 常量 | 取值 | 含义 |
|---|---|---|
ReportReason.ad | ad | 广告、骚扰 |
ReportReason.fraud | fraud | 诈骗 |
ReportReason.porn | porn | 色情低俗 |
ReportReason.politics | politics | 涉政 |
ReportReason.terrorism | terrorism | 暴恐 |
ReportReason.abuse | abuse | 辱骂攻击 |
ReportReason.illegal | illegal | 违法违禁 |
ReportReason.infringement | infringement | 侵权 |
ReportReason.minor | minor | 涉及未成年人 |
ReportReason.other | other | 其他 |
ReportSubmitResult
| 字段 | 类型 | 说明 |
|---|---|---|
report | Report | 这次举报;重复举报时为原来的那一次 |
duplicate | bool | 是否为重复举报 |
Report
原始数据在 raw 中。
| 字段 | 类型 | 说明 |
|---|---|---|
reportId | String | 举报 ID |
targetType | String | 举报的对象类型,同 ReportTarget |
username | String? | 被举报的用户(举报用户时) |
messageId | String? | 被举报的消息 |
groupId | String? | 被举报的群 |
roomId | String? | 被举报的聊天室 |
reason | String | 原因,见 ReportReason |
status | String | pending 处理中或 closed 已处理 |
createdAt | DateTime? | 举报时间 |
