feat(web): complete groups/contacts/favorites/settings/calls/system domains + extensive review-round fixes
Rounds out the full rewrite with the remaining feature domains: group management (admin panel, mute, files, announcements, QR-based invite/join), contacts (friend profile, add-friend, blocklist, new-friends), favorites (real media rendering across text/image/video/voice/file/link), settings (profile/security/privacy/general/about), calls (Agora-backed audio/video signaling + call history), and system messages/file assistant. Also folds in a large batch of live-verified fixes surfaced by repeated fresh code-review passes and hands-on E2E testing against the real backend, including: WS reconnect not resubscribing any topic after a disconnect, missing x-im-client-mid on every send path (protocol compliance for retry dedup), group mute/member-settings/self-profile endpoint corrections, relationship-state type fixes across search/QR/ friend-profile, tenant-switch logout gap, password-policy now read from the real per-tenant config instead of guessed, NavRail highlight/ aria-current desync, several call-state-machine races, and responsive layout fixes across all four breakpoints (mobile/tablet/laptop/desktop) including a full mobile rebuild of favorites, call UI, and the settings home/sub-list split. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
72b7b79caf
commit
5c3f3b7653
@@ -14,7 +14,7 @@ import type { Contact, StoredMessage, Topic } from 'tinode-sdk';
|
||||
|
||||
import { MESSAGES_PAGE } from '@/lib/config';
|
||||
|
||||
import { getTinode } from './connection';
|
||||
import { getTinode, onReauthenticated } from './connection';
|
||||
|
||||
const topicListeners = new Map<string, Set<() => void>>();
|
||||
const anyTopicListeners = new Set<() => void>();
|
||||
@@ -45,11 +45,33 @@ export function subscribeAnyTopicChange(callback: () => void): () => void {
|
||||
return () => anyTopicListeners.delete(callback);
|
||||
}
|
||||
|
||||
export interface RawTopicInfo {
|
||||
what: 'read' | 'recv' | 'kp' | 'call';
|
||||
seq?: number;
|
||||
from?: string;
|
||||
event?: string;
|
||||
payload?: Record<string, unknown>;
|
||||
}
|
||||
const infoListeners = new Set<(topicName: string, info: RawTopicInfo) => void>();
|
||||
|
||||
/** Raw `{info}` payloads (read/recv/kp/call), forwarded verbatim -- unlike `subscribeAnyTopicChange`/
|
||||
* `subscribeTopicChanges`, which only signal "something changed, re-read the snapshot" for the
|
||||
* chat-list/message-list hooks, calls.ts needs the actual `event`/`payload` fields off a `what:
|
||||
* "call"` info to drive its own ringing/accept/hang-up state machine -- those aren't reconstructable
|
||||
* from a message snapshot re-read the way a new chat message is. */
|
||||
export function subscribeTopicInfo(callback: (topicName: string, info: RawTopicInfo) => void): () => void {
|
||||
infoListeners.add(callback);
|
||||
return () => infoListeners.delete(callback);
|
||||
}
|
||||
|
||||
function wireTopicCallbacks(topic: Topic) {
|
||||
if (wiredTopics.has(topic)) return;
|
||||
const notify = () => notifyTopic(topic.name);
|
||||
topic.onData = notify;
|
||||
topic.onInfo = notify;
|
||||
topic.onInfo = (info) => {
|
||||
notify();
|
||||
if (info) infoListeners.forEach((cb) => cb(topic.name, info));
|
||||
};
|
||||
topic.onMetaDesc = notify;
|
||||
topic.onSubsUpdated = notify;
|
||||
topic.onPres = notify;
|
||||
@@ -220,3 +242,34 @@ function invalidateChatListSnapshot() {
|
||||
chatListSnapshot = null;
|
||||
}
|
||||
anyTopicListeners.add(invalidateChatListSnapshot);
|
||||
|
||||
/**
|
||||
* Re-fires the `me` topic subscribe and every contact's preview subscribe once a WS reconnect has
|
||||
* successfully re-authenticated (connection.ts's onReauthenticated). Bypasses subscribeMeTopic's/
|
||||
* prefetchPreview's own "already done" guards on purpose -- those guards exist to stop a second
|
||||
* *concurrent* call from firing a redundant `{sub}` while one is already in flight, not to reflect
|
||||
* whether the topic is still genuinely attached, and after any disconnect it never is (the SDK
|
||||
* resets `_attached` on every cached topic, confirmed by reading tinode.dev.js's #disconnected()).
|
||||
* `Topic.subscribe()` itself only short-circuits when `_attached` is still true, so calling it again
|
||||
* here for an already-detached topic genuinely resends `{sub}`, not a silent no-op.
|
||||
*
|
||||
* Deliberately scoped to topics this module already knows about (the `me` topic + everything
|
||||
* prefetchPreview has ever touched) rather than tracking "the one conversation currently open" as
|
||||
* separate state -- in practice the chat list and an open conversation are mounted together in this
|
||||
* app's layout, so the open topic's contact row has already gone through prefetchPreview by the time
|
||||
* anyone could be looking at it. A conversation reached by a deep link that never rendered the chat
|
||||
* list at all is the one case this doesn't cover; also out of scope: backfilling any messages that
|
||||
* were sent by the peer during the drop itself -- this only resumes *live* updates going forward,
|
||||
* it doesn't gap-fill what was missed while disconnected.
|
||||
*/
|
||||
onReauthenticated(() => {
|
||||
const me = getTinode().getMeTopic();
|
||||
wireTopicCallbacks(me);
|
||||
me.onContactUpdate = () => notifyTopic(me.name);
|
||||
me.subscribe(me.startMetaQuery().withLaterSub().withDesc().withTags().withCred().build()).catch(() => {});
|
||||
for (const topicName of previewPrefetched) {
|
||||
const topic = getTinode().getTopic(topicName);
|
||||
wireTopicCallbacks(topic);
|
||||
topic.subscribe(topic.startMetaQuery().withLaterData(1).build()).catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user