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:
Your Name
2026-09-09 20:16:33 +08:00
co-authored by Claude Sonnet 5
parent 72b7b79caf
commit 5c3f3b7653
89 changed files with 10158 additions and 442 deletions
+23 -1
View File
@@ -1,5 +1,6 @@
import { useState } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { Link, useNavigate } from 'react-router-dom';
import { Icon } from '@/app/Icon';
@@ -10,7 +11,9 @@ import { parseWebsocketEndpoint, useSessionStore } from '@/stores/session';
export function TenantEntryPage() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const setTenant = useSessionStore((s) => s.setTenant);
const setMyUserId = useSessionStore((s) => s.setMyUserId);
const myUserId = useSessionStore((s) => s.myUserId);
const [code, setCode] = useState('');
const [error, setError] = useState('');
@@ -32,7 +35,26 @@ export function TenantEntryPage() {
// host: getTinode() lazily creates one singleton and reuses it forever once created, and
// createTinode() only ever reads useSessionStore's tenant at that one construction moment.
resetTinode();
setTenant({ code: res.tenant.code, name: res.tenant.name, endpoints: res.endpoints, host, secure });
// The confirm() dialog above literally promises "切换企业将退出当前登录" -- without these,
// that was a false promise: `myUserId` is a separate module-level store that switching
// tenant never touched, so RequireAuth's `if (myUserId) return <Outlet/>` would let a
// subsequent in-app navigation straight through with the OLD tenant's uid and the OLD
// tenant's sessionStorage token, against a freshly-reset Tinode connection that has never
// authenticated with the NEW tenant at all. Same teardown SettingsLayout's own logout and
// SecuritySettingsPage's account deletion already do.
sessionStorage.removeItem('im-session-token');
setMyUserId(null);
queryClient.clear();
setTenant({
code: res.tenant.code,
name: res.tenant.name,
endpoints: res.endpoints,
host,
secure,
passwordPolicy: res.password_policy
? { minLength: res.password_policy.min_length ?? 8, maxLength: res.password_policy.max_length ?? 64 }
: undefined,
});
navigate('/login');
} catch (err) {
setError(err instanceof ImApiError ? err.message : '企业码不存在或已停用,请核实后重试');