diff --git a/web/src/features/groups/GroupDetailColumn.tsx b/web/src/features/groups/GroupDetailColumn.tsx index fdb842ab..368ea1dc 100644 --- a/web/src/features/groups/GroupDetailColumn.tsx +++ b/web/src/features/groups/GroupDetailColumn.tsx @@ -172,10 +172,13 @@ export function GroupDetailColumn({ topicId, peerName, onClose, onOpenHistory }: queryClient.setQueryData(['group', topicId], (prev: GroupProfile | undefined) => (prev ? { ...prev, ...data } : data)); setNameEditing(false); // The renamer's OWN sidebar/header (`contact.public?.fn`, native SDK state) doesn't update from - // this REST call either -- only from the `group.profile.updated` push topics.ts now listens for, - // which this account won't receive for its own action. Trigger the same SDK-sanctioned refresh - // directly instead of waiting on an unconfirmed self-echo of that push. - tinode.getTopic(topicId).getMeta(tinode.getTopic(topicId).startMetaQuery().withDesc().build()).catch(() => {}); + // this REST call either -- only from the `group.settings.updated` push topics.ts listens for, + // which this account won't receive for its own action. `data` here is already the fresh REST + // response, so apply it locally via `_processMetaDesc` (live-confirmed to reach `me.contacts()`, + // see topics.ts's comment on the same call) instead of an extra `getMeta(...withDesc())` round + // trip -- that SDK-sanctioned path was live-instrumented and found to reliably time out (504) or + // return stale data against this backend, so it's not used anywhere in this fix anymore. + tinode.getTopic(topicId)._processMetaDesc({ public: { fn: data.name, name: data.name } }); }, onError: (err) => showError(err, '群名保存失败'), }); diff --git a/web/src/lib/tinode/tinode-sdk.d.ts b/web/src/lib/tinode/tinode-sdk.d.ts index d4bf2f67..66ae9482 100644 --- a/web/src/lib/tinode/tinode-sdk.d.ts +++ b/web/src/lib/tinode/tinode-sdk.d.ts @@ -183,6 +183,24 @@ declare module 'tinode-sdk' { getMeta(query: unknown): Promise; startMetaQuery(): MetaGetBuilder; + /** + * Private/internal SDK method (underscore prefix, not part of the documented API) -- normally only + * ever called by the SDK itself when a real `{meta desc}` server response arrives. Declared here + * and used deliberately, NOT as a first choice: `getMeta(startMetaQuery().withDesc().build())` (the + * sanctioned public API for exactly this "re-pull desc from server" need) was live-instrumented + * against this backend and found to be genuinely unreliable for group topics -- consistently a + * 504 timeout (~5.4s, every attempt, including 10+s after the write) on a brand-new topic, and on + * an established topic a fast-but-STALE response whose `public.fn` still reflected the pre-rename + * name (confirmed by round-tripping a real REST rename immediately followed by this call). Calling + * this directly with data already known-fresh (from a REST response we trust) sidesteps that + * broken path entirely -- confirmed by reading the SDK's own source that it merges `e.public` onto + * the topic's existing `public` object (other fields like `version` survive) and, for a non-`me` + * topic, forwards the update to the `me` topic's own subscription cache via `onMetaSub`/ + * `onSubsUpdated` -- i.e. the exact same fan-out a genuine server push would trigger. Live-confirmed + * this reaches `me.contacts()` (same object reference as `topic.public`, verified live). + */ + _processMetaDesc(desc: { public?: Partial }): void; + /** Builds the `{pub}` packet shape but does not send it -- pair with `publishDraft`/`publishMessage`. */ createMessage(content: DraftyContent | string, noEcho?: boolean): StoredMessage; /** diff --git a/web/src/lib/tinode/topics.ts b/web/src/lib/tinode/topics.ts index 648eea0c..7019bba3 100644 --- a/web/src/lib/tinode/topics.ts +++ b/web/src/lib/tinode/topics.ts @@ -12,6 +12,7 @@ // getParams -- so this always leaves first when already subscribed, then resubscribes. import type { Contact, StoredMessage, Topic } from 'tinode-sdk'; +import { getGroup } from '@/lib/api/groups'; import { MESSAGES_PAGE } from '@/lib/config'; import { getTinode, onReauthenticated } from './connection'; @@ -128,7 +129,22 @@ function notifyTopic(topicName: string) { pendingAnyNotification = true; if (!notifyFrameScheduled) { notifyFrameScheduled = true; - requestAnimationFrame(flushTopicNotifications); + // Live-confirmed real bug (surfaced while testing an unrelated group-rename fix, in a webview whose + // frame compositing was paused, i.e. genuinely not painting -- not just an ordinary backgrounded + // browser tab): a `requestAnimationFrame` scheduled while the page isn't compositing frames can go + // uncalled indefinitely rather than merely deferred-until-visible. Since `notifyFrameScheduled` only + // resets to false inside the callback, one dropped frame permanently wedges this guard, silently + // freezing EVERY future topic-list update (not just the group name) until a full page reload -- data + // kept arriving and updating correctly underneath (confirmed live: `me.contacts()` had the right + // data the whole time), only the UI never found out. `document.hidden` costs nothing to check and + // `setTimeout` is never silently dropped the way this rAF was, so route through it whenever the page + // isn't visible; keep `requestAnimationFrame` for the visible case since that's what actually gives + // the intended one-flush-per-paint batching this function exists for. + if (typeof document !== 'undefined' && document.hidden) { + setTimeout(flushTopicNotifications, 0); + } else { + requestAnimationFrame(flushTopicNotifications); + } } } @@ -298,19 +314,24 @@ function wireTopicCallbacks(topic: Topic) { topic.getMeta(topic.startMetaQuery().withData(seq, seq + 1, 1).build()).catch(() => {}); } } - // docs/IM-前后端对接文档.md:1542: `PATCH .../groups/{topic}` (rename/avatar/description) pushes a - // custom `{info what:"group", event:"group.profile.updated"}` to every other online member -- same - // undocumented-to-the-SDK shape as `group.dissolved` above, so nothing here ever refreshed the - // topic's native `public` fields on receipt. Live-confirmed the actual gap: the sidebar/header name + // docs/IM-前后端对接文档.md:1542 documents this push as `group.profile.updated`, but a real rename, + // live-instrumented (logging the raw `info` object as it arrived), showed the actual wire event is + // `{what:"group", event:"group.settings.updated"}` -- same doc-vs-wire drift already seen elsewhere + // in this backend. Matching the real event name fixes only half the bug: the sidebar/header name // comes from `contact.public?.fn` (native SDK state, see ChatListColumn.tsx), which a REST-only - // mutation never touches for ANYONE -- not just other members, but the renamer's own other tabs/ - // views too -- and only ever got refreshed as a side effect of the unrelated focus-triggered - // `reconcileMeSubs()`, which is why refocus "fixed" it for everyone. `withDesc()` is the SDK's own - // sanctioned way to re-pull `public`/description from the server (contrast the abandoned approach - // of manually patching `Topic._processMetaDesc` tried earlier for a different bug -- that one was - // reverted for inconclusive verification; this one goes through a real server round-trip instead). - if (groupEvent?.what === 'group' && groupEvent.event === 'group.profile.updated') { - topic.getMeta(topic.startMetaQuery().withDesc().build()).catch(() => {}); + // mutation never touches for anyone, and the "obvious" fix -- `getMeta(startMetaQuery().withDesc() + // .build())`, the SDK's own sanctioned way to re-pull desc from the server -- was live-instrumented + // too and found to be genuinely broken against this backend: a consistent 504 timeout (~5.4s, every + // attempt, still failing 10+s after the write) on a brand-new topic, and on an established topic a + // fast-but-stale response whose `public.fn` still held the pre-rename name. Using the already- + // reliable REST `getGroup()` instead (confirmed live to return the fresh name immediately after a + // rename, every time) and applying it locally via `_processMetaDesc` -- the same internal method the + // SDK itself calls when a real `{meta desc}` response arrives, live-confirmed to correctly reach + // `me.contacts()` -- sidesteps the broken path entirely instead of routing through it. + if (groupEvent?.what === 'group' && groupEvent.event === 'group.settings.updated') { + getGroup(topic.name) + .then((data) => topic._processMetaDesc({ public: { fn: data.name, name: data.name } })) + .catch(() => {}); } }; topic.onMetaDesc = notify;