被拉入新群时标题栏/桌面通知没有任何提示——扩展兜底同步机制覆盖这个场景
reconcileMeSubs()本来就能发现"此前完全不认识的话题"(今天早些时候为解决群聊 被动成员看不到新群加的兜底机制),但发现后只是原样信任服务端返回的read/unread 字段——服务端对一个刚加入的新成员,默认给的读游标经常是"视为已读"(read==seq), 如果这个群目前恰好一条消息都没有,unread就会是0,导致今天新加的标题栏滚动/ favicon角标(由同一个未读数驱动)完全没有反应,被拉群这件事本身悄无声息。 改成:对每一个新发现的话题,不管服务端给的read游标是什么,一律按"我自己完全 没确认过"处理(read清零,unread按当前seq计算);如果是群,额外通过 subscribeNewGroupDiscovered(新增的发布-订阅,push.ts订阅它去弹真正的桌面 通知"你被拉入了这个群聊",因为topics.ts不能反向依赖push.ts)触发一条独立的 桌面通知,不依赖未读数是否恰好大于0。 实机验证:systest04建一个只邀请systest05的全新群,systest05这边模拟切回 标签页触发兜底同步后,确认收到桌面通知(title/body/tag均正确)且标题栏 未读数正确增加。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
f570483b62
commit
3fab8b08b9
@@ -6,7 +6,13 @@
|
|||||||
import type { StoredMessage } from 'tinode-sdk';
|
import type { StoredMessage } from 'tinode-sdk';
|
||||||
|
|
||||||
import { formatMessagePreview } from '@/lib/messages/preview';
|
import { formatMessagePreview } from '@/lib/messages/preview';
|
||||||
import { getActiveConversationTopic, getChatListSnapshot, getMessagesSnapshot, subscribeAnyTopicChange } from '@/lib/tinode/topics';
|
import {
|
||||||
|
getActiveConversationTopic,
|
||||||
|
getChatListSnapshot,
|
||||||
|
getMessagesSnapshot,
|
||||||
|
subscribeAnyTopicChange,
|
||||||
|
subscribeNewGroupDiscovered,
|
||||||
|
} from '@/lib/tinode/topics';
|
||||||
import { useNotificationNavStore } from '@/stores/notificationNav';
|
import { useNotificationNavStore } from '@/stores/notificationNav';
|
||||||
import { useSessionStore } from '@/stores/session';
|
import { useSessionStore } from '@/stores/session';
|
||||||
|
|
||||||
@@ -85,16 +91,12 @@ function shouldNotify(topicName: string, msg: StoredMessage): boolean {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function fireNotification(topicName: string, msg: StoredMessage) {
|
/** Shared by both real call sites below -- a new message, or being newly added to a group. `tag:
|
||||||
const contact = getChatListSnapshot().find((c) => c.topic === topicName);
|
* topicName` makes a second notification from the same conversation REPLACE the first in the OS
|
||||||
const title = contact?.public?.fn || topicName;
|
* notification tray instead of stacking, matching every mainstream IM web client. */
|
||||||
const myUserId = useSessionStore.getState().myUserId;
|
function showNotification(topicName: string, title: string, body: string) {
|
||||||
const body = formatMessagePreview(msg, myUserId) || '发来一条新消息';
|
|
||||||
let notif: Notification;
|
let notif: Notification;
|
||||||
try {
|
try {
|
||||||
// `tag: topicName` -- a second notification from the same conversation REPLACES the first in the
|
|
||||||
// OS notification tray instead of stacking, matching every mainstream IM web client (you get one
|
|
||||||
// live-updating notification per conversation, not a pile of them from one chatty sender).
|
|
||||||
notif = new Notification(title, { body, tag: topicName, icon: '/favicon.svg' });
|
notif = new Notification(title, { body, tag: topicName, icon: '/favicon.svg' });
|
||||||
} catch {
|
} catch {
|
||||||
// Some browsers/contexts throw constructing Notification even with permission granted (e.g. an
|
// Some browsers/contexts throw constructing Notification even with permission granted (e.g. an
|
||||||
@@ -109,13 +111,38 @@ function fireNotification(topicName: string, msg: StoredMessage) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fireNotification(topicName: string, msg: StoredMessage) {
|
||||||
|
const contact = getChatListSnapshot().find((c) => c.topic === topicName);
|
||||||
|
const title = contact?.public?.fn || topicName;
|
||||||
|
const myUserId = useSessionStore.getState().myUserId;
|
||||||
|
const body = formatMessagePreview(msg, myUserId) || '发来一条新消息';
|
||||||
|
showNotification(topicName, title, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** "被拉入群时title的消息没有提示" -- topics.ts's reconcileMeSubs() is the only thing that can even
|
||||||
|
* detect this (nothing else observes a group appearing that this client never knew about), so it
|
||||||
|
* fires this via subscribeNewGroupDiscovered rather than this module trying to infer "newly added"
|
||||||
|
* from message/seq activity, which a group with zero messages so far wouldn't have at all. */
|
||||||
|
function onNewGroupDiscovered(topicName: string) {
|
||||||
|
if (typeof Notification === 'undefined' || Notification.permission !== 'granted') return;
|
||||||
|
const contact = getChatListSnapshot().find((c) => c.topic === topicName);
|
||||||
|
if (contact?.private?.mute_notifications) return;
|
||||||
|
const title = contact?.public?.fn || topicName;
|
||||||
|
showNotification(topicName, title, '你被拉入了这个群聊');
|
||||||
|
}
|
||||||
|
|
||||||
let unsubscribe: (() => void) | null = null;
|
let unsubscribe: (() => void) | null = null;
|
||||||
|
|
||||||
/** Call once per authenticated session (RequireAuth.tsx, alongside startMeReconciliation) and invoke
|
/** Call once per authenticated session (RequireAuth.tsx, alongside startMeReconciliation) and invoke
|
||||||
* the returned function on logout/unmount. Safe to start/stop repeatedly. */
|
* the returned function on logout/unmount. Safe to start/stop repeatedly. */
|
||||||
export function initMessageNotifications(): () => void {
|
export function initMessageNotifications(): () => void {
|
||||||
if (unsubscribe) return unsubscribe;
|
if (unsubscribe) return unsubscribe;
|
||||||
unsubscribe = subscribeAnyTopicChange(scheduleScan);
|
const unsubScan = subscribeAnyTopicChange(scheduleScan);
|
||||||
|
const unsubNewGroup = subscribeNewGroupDiscovered(onNewGroupDiscovered);
|
||||||
|
unsubscribe = () => {
|
||||||
|
unsubScan();
|
||||||
|
unsubNewGroup();
|
||||||
|
};
|
||||||
return () => {
|
return () => {
|
||||||
unsubscribe?.();
|
unsubscribe?.();
|
||||||
unsubscribe = null;
|
unsubscribe = null;
|
||||||
|
|||||||
@@ -99,6 +99,18 @@ export function subscribeTopicInfo(callback: (topicName: string, info: RawTopicI
|
|||||||
|
|
||||||
const goneListeners = new Set<(topicName: string) => void>();
|
const goneListeners = new Set<(topicName: string) => void>();
|
||||||
|
|
||||||
|
const newGroupListeners = new Set<(topicName: string) => void>();
|
||||||
|
|
||||||
|
/** Fires when reconcileMeSubs() (below) discovers a group topic this client has never seen before --
|
||||||
|
* the "you were just added to a group" case (Zentao/user-reported: "被拉入群时title的消息没有提示").
|
||||||
|
* Consumed by lib/notifications/push.ts to fire a desktop notification -- can't live there itself
|
||||||
|
* since push.ts already depends on topics.ts and the reverse would be a cycle, same reasoning as
|
||||||
|
* setActiveConversationTopic living here instead of in push.ts. */
|
||||||
|
export function subscribeNewGroupDiscovered(callback: (topicName: string) => void): () => void {
|
||||||
|
newGroupListeners.add(callback);
|
||||||
|
return () => newGroupListeners.delete(callback);
|
||||||
|
}
|
||||||
|
|
||||||
/** Fires once a topic is confirmed gone (group dissolved by anyone, or removed from this account) --
|
/** Fires once a topic is confirmed gone (group dissolved by anyone, or removed from this account) --
|
||||||
* the SDK's own `Topic#_gone()` clears the topic's public/private/messages and calls
|
* the SDK's own `Topic#_gone()` clears the topic's public/private/messages and calls
|
||||||
* `onDeleteTopic()` (confirmed reading tinode.dev.js). Without ConversationView reacting to this,
|
* `onDeleteTopic()` (confirmed reading tinode.dev.js). Without ConversationView reacting to this,
|
||||||
@@ -481,7 +493,21 @@ function reconcileMeSubs() {
|
|||||||
for (const c of getChatListSnapshot()) {
|
for (const c of getChatListSnapshot()) {
|
||||||
if (!c.topic) continue;
|
if (!c.topic) continue;
|
||||||
const prev = before.get(c.topic);
|
const prev = before.get(c.topic);
|
||||||
if (!prev) continue; // brand-new topic to this session -- exactly what this mitigation is for
|
if (!prev) {
|
||||||
|
// Brand-new topic to this session -- exactly what this mitigation is for. A fresh member's
|
||||||
|
// sub record can legitimately come back "already caught up" (read === seq) from the
|
||||||
|
// server's own default, which would otherwise leave being newly added to a group producing
|
||||||
|
// NO unread signal at all whenever it happens to have zero messages so far -- real,
|
||||||
|
// reported UX gap ("被拉入群时title的消息没有提示"): the tab-title/favicon-badge (see lib/
|
||||||
|
// notifications/tabIndicator.ts) are driven by this same unread count, so nothing tells the
|
||||||
|
// user anything happened. Force it to read as genuinely unread from this client's own
|
||||||
|
// perspective regardless of what the server defaulted it to.
|
||||||
|
c.read = 0;
|
||||||
|
c.unread = c.seq || 0;
|
||||||
|
notifyTopic(c.topic);
|
||||||
|
if (c.topic.startsWith('grp')) newGroupListeners.forEach((cb) => cb(c.topic!));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const seq = c.seq || 0;
|
const seq = c.seq || 0;
|
||||||
const unread = c.unread || 0;
|
const unread = c.unread || 0;
|
||||||
if (seq <= prev.seq && unread > prev.unread) {
|
if (seq <= prev.seq && unread > prev.unread) {
|
||||||
|
|||||||
Reference in New Issue
Block a user