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
@@ -0,0 +1,94 @@
|
||||
// QR code REST endpoints (docs/IM-前后端对接文档.md §5.15.1). Confirmed LIVE against the real
|
||||
// deployment (2026-09-08, two fresh test accounts) -- `resolveQRCode`'s real response shape nests
|
||||
// `relationship`/`can_add`/`action`/`public_account` all under `user`, unlike the doc's own worked
|
||||
// example which shows them as siblings of `user` at the top level. Also confirmed live: resolving a
|
||||
// code against the SAME account that minted it is rejected (`400 MALFORMED_REQUEST`, `field:"token"`,
|
||||
// message "malformed request" -- distinct from a genuinely garbage token, which instead gets message
|
||||
// "qr code invalid" with no `details.field` at all) -- cross-account resolution works correctly.
|
||||
//
|
||||
// The security rationale for why this exists at all instead of just encoding a raw user id (doc line
|
||||
// 1299): "二维码只含不透明、签名、带租户和用途约束的短期 Token,不直接放手机号、用户 ID 或登录
|
||||
// Token" -- the QR image payload is `qr_payload` (an `im://qr?token=...&purpose=...` URI), not
|
||||
// `qr_token` alone, matching the old app's own `im://qr` scheme (im_webapp/src/views/tinode-web.jsx
|
||||
// confirmed this exact prefix check before ever sending a scanned string to `resolve`).
|
||||
import { imPost } from './client';
|
||||
|
||||
export interface QRCodeDisplayProfile {
|
||||
user_id: string;
|
||||
display_name: string;
|
||||
public_account?: string;
|
||||
}
|
||||
|
||||
export interface MyQRCode {
|
||||
qr_token: string;
|
||||
/** What actually gets encoded into the QR image -- `im://qr?token=...&purpose=...`. */
|
||||
qr_payload: string;
|
||||
expires_at: string;
|
||||
display_profile: QRCodeDisplayProfile;
|
||||
}
|
||||
|
||||
export function createMyQRCode(expiresInSeconds = 86400) {
|
||||
return imPost<MyQRCode>('/me/qrcodes', { purpose: 'add_friend', expires_in_seconds: expiresInSeconds });
|
||||
}
|
||||
|
||||
/** `identifier`: user_id/username/public_account -- viewing a specific friend's own QR card. */
|
||||
export function createUserQRCode(identifier: string, expiresInSeconds = 86400) {
|
||||
return imPost<MyQRCode>(`/users/${encodeURIComponent(identifier)}/qrcodes`, { purpose: 'add_friend', expires_in_seconds: expiresInSeconds });
|
||||
}
|
||||
|
||||
export interface QRResolveUser {
|
||||
user_id: string;
|
||||
display_name: string;
|
||||
public_account?: string;
|
||||
// docs/IM-前后端对接文档.md:1289 -- same real direction-specific enum as friends.ts's
|
||||
// UserSearchResult/UserProfile, never a bare "pending".
|
||||
relationship: 'none' | 'friend' | 'pending_inbound' | 'pending_outbound' | string;
|
||||
can_add: boolean;
|
||||
action: string;
|
||||
}
|
||||
|
||||
/** Confirmed LIVE (2026-09-08, same account that minted the code -- a real cross-account "scan a
|
||||
* stranger's group code" resolve hasn't been exercised, this session only had one account free):
|
||||
* a `purpose:"join_group"` resolve nests the group under `group` (NOT `group_snapshot`, despite
|
||||
* the mint response using that key -- resolve and mint use different field names for the same
|
||||
* shape), and carries a `status` this specific case's doc example (line 1737, mint-only) never
|
||||
* showed: resolving your own group's code (i.e. already a member) comes back `status:"joined"`.
|
||||
* What a genuinely-not-yet-a-member resolve returns for `status` is unconfirmed -- treated as
|
||||
* "anything other than 'joined'" in QRScannerModal/AddFriendModal, i.e. the general case that still
|
||||
* needs a real join-request submission. */
|
||||
export interface QRResolveGroup {
|
||||
topic: string;
|
||||
name: string;
|
||||
member_count: number;
|
||||
}
|
||||
|
||||
export interface QRResolveResult {
|
||||
purpose: 'add_friend' | 'join_group' | string;
|
||||
source: string;
|
||||
source_topic: string;
|
||||
expires_at: string;
|
||||
user?: QRResolveUser;
|
||||
group?: QRResolveGroup;
|
||||
group_snapshot?: QRResolveGroup;
|
||||
/** join_group only -- `"joined"` means the scanning account is already a member (confirmed live);
|
||||
* any other value (including absent) is treated as "not a member yet, submit a join request". */
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export function resolveQRCode(token: string, purpose: 'add_friend' | 'join_group' | string = 'add_friend') {
|
||||
return imPost<QRResolveResult>('/qrcodes/resolve', { token, purpose });
|
||||
}
|
||||
|
||||
/** docs/IM-前后端对接文档.md:1590 -- 群管理员/群主展示的入群二维码 (mirrors createMyQRCode/
|
||||
* createUserQRCode's shape, just scoped to a group and carrying `group_snapshot` instead of a
|
||||
* personal `display_profile`). */
|
||||
export interface GroupQRCode {
|
||||
qr_token: string;
|
||||
qr_payload: string;
|
||||
expires_at: string;
|
||||
group_snapshot: QRResolveGroup;
|
||||
}
|
||||
|
||||
export function createGroupQRCode(topic: string, expiresInSeconds = 86400) {
|
||||
return imPost<GroupQRCode>(`/groups/${encodeURIComponent(topic)}/qrcodes`, { expires_in_seconds: expiresInSeconds });
|
||||
}
|
||||
Reference in New Issue
Block a user