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
@@ -3,16 +3,26 @@
|
||||
// direct-message-view.jsx) -- verified there, not guessed.
|
||||
import { imDelete, imGet, imPatch, imPost, imPut } from './client';
|
||||
|
||||
/** Confirmed live shape (doc line 1417): `initial` is server-computed (handles pinyin-to-letter
|
||||
* itself), so the contacts list groups by THIS field, not a client-side first-character guess like
|
||||
* CreateGroupModal's friend picker had to fall back to (that one has no `initial` in its response). */
|
||||
export interface Friend {
|
||||
user_id: string;
|
||||
display_name?: string;
|
||||
remark?: string;
|
||||
avatar_file_id?: string;
|
||||
initial?: string;
|
||||
topic?: string;
|
||||
relationship?: string;
|
||||
online_visibility?: string;
|
||||
capabilities?: { can_send?: boolean; can_call?: boolean };
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export function getFriends(query = '', cursor = '') {
|
||||
export function getFriends(query = '', cursor = '', initial = '') {
|
||||
const qs = new URLSearchParams();
|
||||
if (query) qs.set('query', query);
|
||||
if (initial) qs.set('initial', initial);
|
||||
if (cursor) qs.set('cursor', cursor);
|
||||
const suffix = qs.toString();
|
||||
return imGet<{ items: Friend[]; next_cursor?: string }>(`/friends${suffix ? `?${suffix}` : ''}`);
|
||||
@@ -26,15 +36,22 @@ export function updateFriendRemark(userId: string, remark: string) {
|
||||
return imPatch<void>(`/friends/${encodeURIComponent(userId)}`, { remark });
|
||||
}
|
||||
|
||||
/** Confirmed live shape (doc line 1423): no `action` field as this file originally typed (there's
|
||||
* nothing in the response describing what UI affordance to show -- that's derived client-side
|
||||
* from `relationship`/`can_add` instead), and `avatar_file_id` is present. */
|
||||
export interface UserSearchResult {
|
||||
user_id: string;
|
||||
display_name: string;
|
||||
relationship: 'none' | 'friend' | 'pending' | string;
|
||||
avatar_file_id?: string;
|
||||
public_account?: string;
|
||||
// docs/IM-前后端对接文档.md:1289 -- real enum is these 4 values, direction-specific, never a bare
|
||||
// "pending" (a prior version of this type had that wrong, which meant no caller's `=== 'pending'`
|
||||
// comparison could ever match either real pending state).
|
||||
relationship: 'none' | 'friend' | 'pending_inbound' | 'pending_outbound' | string;
|
||||
can_add: boolean;
|
||||
action: string;
|
||||
}
|
||||
|
||||
export function searchUsers(query: string, type: 'account' = 'account', cursor = '') {
|
||||
export function searchUsers(query: string, type: 'account' | 'exact_account' | 'phone' = 'account', cursor = '') {
|
||||
const qs = new URLSearchParams({ q: query, type });
|
||||
if (cursor) qs.set('cursor', cursor);
|
||||
return imGet<{ items: UserSearchResult[]; next_cursor?: string }>(`/users/search?${qs.toString()}`);
|
||||
@@ -49,6 +66,9 @@ export function sendFriendRequest(targetUserId: string, message?: string, source
|
||||
});
|
||||
}
|
||||
|
||||
/** `source` and `created_at` (doc line 1419's worked example) were missing from this file's
|
||||
* original type -- needed for the 新朋友 list's "今天 10:12 · 通过手机号搜索" line. `handled_at`
|
||||
* is mentioned in the doc's prose but not its own worked example, kept optional either way. */
|
||||
export interface FriendRequest {
|
||||
request_id: string;
|
||||
applicant_id: string;
|
||||
@@ -56,12 +76,22 @@ export interface FriendRequest {
|
||||
direction: 'inbound' | 'outbound';
|
||||
status: string;
|
||||
message?: string;
|
||||
source?: string;
|
||||
created_at: string;
|
||||
handled_at?: string;
|
||||
peer?: { user_id: string; display_name?: string };
|
||||
topic: string;
|
||||
topic?: string;
|
||||
}
|
||||
|
||||
export function getFriendRequests() {
|
||||
return imGet<{ items: FriendRequest[]; next_cursor?: string }>('/friend-requests');
|
||||
/** Doc's confirmed query params (line 1277/1419) -- the original zero-arg version always hit the
|
||||
* endpoint with no filters at all, whose default direction/status scope isn't documented; the
|
||||
* design's 新朋友 list needs BOTH pending and already-handled inbound requests shown together
|
||||
* (处理过的显示灰字"已通过/已拒绝"), so this defaults to exactly that rather than relying on
|
||||
* whatever an unfiltered call happens to return. */
|
||||
export function getFriendRequests(direction: 'inbound' | 'outbound' = 'inbound', status: 'pending' | 'all' = 'all', cursor = '') {
|
||||
const qs = new URLSearchParams({ direction, status });
|
||||
if (cursor) qs.set('cursor', cursor);
|
||||
return imGet<{ items: FriendRequest[]; next_cursor?: string }>(`/friend-requests?${qs.toString()}`);
|
||||
}
|
||||
|
||||
export function acceptFriendRequest(requestId: string, version?: number) {
|
||||
@@ -72,6 +102,14 @@ export function rejectFriendRequest(requestId: string, version?: number) {
|
||||
return imPost<void>(`/friend-requests/${encodeURIComponent(requestId)}/reject`, version !== undefined ? { version } : {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirmed live (2026-09-08, real friend `usruF4Uil-jCKM`): `capabilities.can_add_friend`, not the
|
||||
* doc's own `can_add` (line 1425) -- another confirmed field-name mismatch, not this file's guess.
|
||||
* `remark` was also confirmed absent from a real friend's response even though the doc's worked
|
||||
* example shows `"remark":""` present -- FriendProfileView.tsx's `profile.remark || profile.
|
||||
* display_name` already degrades correctly either way, so this isn't a rendering bug, just a gap
|
||||
* between the doc and what this deployment actually returns right now.
|
||||
*/
|
||||
export interface UserProfile {
|
||||
user_id: string;
|
||||
display_name: string;
|
||||
@@ -81,8 +119,11 @@ export interface UserProfile {
|
||||
region?: string;
|
||||
public_account?: string;
|
||||
signature?: string;
|
||||
relationship: string;
|
||||
// Same real enum as UserSearchResult's (docs/IM-前后端对接文档.md:1289) -- direction-specific
|
||||
// pending states, never a bare "pending".
|
||||
relationship: 'none' | 'friend' | 'pending_inbound' | 'pending_outbound' | string;
|
||||
topic?: string;
|
||||
capabilities?: { can_send?: boolean; can_call?: boolean; can_add_friend?: boolean };
|
||||
}
|
||||
|
||||
/** `identifier` accepts `user_id`, `username`, or `public_account` -- P2P topic names are the peer's
|
||||
@@ -114,6 +155,15 @@ export function unblockUser(userId: string) {
|
||||
return imDelete<void>(`/blocks/${encodeURIComponent(userId)}`);
|
||||
}
|
||||
|
||||
/** docs/IM-前后端对接文档.md:1380,1431 -- up to 100 ids per call, per-item result in `items[]` (this
|
||||
* backend's usual batch-endpoint shape, see fetchAllPages's own header comment on the class of bug
|
||||
* that comes from assuming a 200 means every item succeeded). */
|
||||
export function batchUnblockUsers(userIds: string[]) {
|
||||
return imPost<{ items: Array<{ user_id: string; blocked: boolean }>; failed?: Array<{ user_id: string; reason: string }> }>('/blocks/batch-delete', {
|
||||
user_ids: userIds,
|
||||
});
|
||||
}
|
||||
|
||||
export interface BlockedUser {
|
||||
user: { user_id: string; display_name?: string; avatar_file_id?: string };
|
||||
blocked_at: string;
|
||||
|
||||
Reference in New Issue
Block a user