feat(web): real P2P chat-settings detail panel
Replaces the detail-column placeholder with the actual "聊天设置" panel
(README §3.4): profile header, quick-action buttons, remark, pin/mute
toggles, blacklist, clear-history, delete-friend -- backed by real REST
endpoints (lib/api/friends.ts, ported from the old app's real
contact-management-view.jsx settings panel, not its unrelated mock
direct-message-view.jsx).
First real use of TanStack Query in this app (useQuery for
friends/conversation-settings/blocks, useMutation for the writes) --
previously only wired up in App.tsx with nothing consuming it yet.
Verified end to end: pin toggle PATCHes /conversations/{topic}/settings
and persists; remark PATCHes /friends/{userId} and reflects immediately
via query invalidation; blacklist PUT/DELETEs /blocks/{userId}.
Known minor gap: the drawer variant (960-1279px tier) stays mounted
(CSS-hidden) alongside the docked panel at >=1280px rather than
unmounting, so both instances' queries run -- harmless since TanStack
Query dedupes/shares the cache by key, but worth a follow-up cleanup.
Also still missing: the <960px "detail settings push as a full page"
variant the mobile spec calls for.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
f549248c08
commit
7f5756671d
@@ -0,0 +1,100 @@
|
||||
// Friend/conversation-settings/blocklist REST endpoints. Paths and payload shapes ported from the
|
||||
// old app's src/lib/im-api.js (contact-management-view.jsx's real "聊天设置" panel, not the mock
|
||||
// direct-message-view.jsx) -- verified there, not guessed.
|
||||
import { imDelete, imGet, imPatch, imPost, imPut } from './client';
|
||||
|
||||
export interface Friend {
|
||||
user_id: string;
|
||||
display_name?: string;
|
||||
remark?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export function getFriends(query = '', cursor = '') {
|
||||
const qs = new URLSearchParams();
|
||||
if (query) qs.set('query', query);
|
||||
if (cursor) qs.set('cursor', cursor);
|
||||
const suffix = qs.toString();
|
||||
return imGet<{ items: Friend[]; next_cursor?: string }>(`/friends${suffix ? `?${suffix}` : ''}`);
|
||||
}
|
||||
|
||||
export function deleteFriend(userId: string) {
|
||||
return imDelete<void>(`/friends/${encodeURIComponent(userId)}`);
|
||||
}
|
||||
|
||||
export function updateFriendRemark(userId: string, remark: string) {
|
||||
return imPatch<void>(`/friends/${encodeURIComponent(userId)}`, { remark });
|
||||
}
|
||||
|
||||
export interface UserSearchResult {
|
||||
user_id: string;
|
||||
display_name: string;
|
||||
relationship: 'none' | 'friend' | 'pending' | string;
|
||||
can_add: boolean;
|
||||
action: string;
|
||||
}
|
||||
|
||||
export function searchUsers(query: string, type: 'account' = '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()}`);
|
||||
}
|
||||
|
||||
export function sendFriendRequest(targetUserId: string, message?: string, source = 'search', sourceTopic?: string) {
|
||||
return imPost<{ request_id: string; topic: string; status: string }>('/friend-requests', {
|
||||
target_user_id: targetUserId,
|
||||
message,
|
||||
source,
|
||||
source_topic: sourceTopic,
|
||||
});
|
||||
}
|
||||
|
||||
export interface FriendRequest {
|
||||
request_id: string;
|
||||
applicant_id: string;
|
||||
target_user_id: string;
|
||||
direction: 'inbound' | 'outbound';
|
||||
status: string;
|
||||
message?: string;
|
||||
peer?: { user_id: string; display_name?: string };
|
||||
topic: string;
|
||||
}
|
||||
|
||||
export function getFriendRequests() {
|
||||
return imGet<{ items: FriendRequest[]; next_cursor?: string }>('/friend-requests');
|
||||
}
|
||||
|
||||
export function acceptFriendRequest(requestId: string, version?: number) {
|
||||
return imPost<void>(`/friend-requests/${encodeURIComponent(requestId)}/accept`, version ? { version } : {});
|
||||
}
|
||||
|
||||
export function rejectFriendRequest(requestId: string, version?: number) {
|
||||
return imPost<void>(`/friend-requests/${encodeURIComponent(requestId)}/reject`, version ? { version } : {});
|
||||
}
|
||||
|
||||
export interface ConversationSettings {
|
||||
pinned: boolean;
|
||||
mute_notifications: boolean;
|
||||
version: number;
|
||||
}
|
||||
|
||||
/** P2P-only -- group pin/mute goes through a different, group-specific mechanism (native Tinode private/tags). */
|
||||
export function getConversationSettings(topic: string) {
|
||||
return imGet<ConversationSettings>(`/conversations/${encodeURIComponent(topic)}/settings`);
|
||||
}
|
||||
|
||||
export function updateConversationSettings(topic: string, body: Partial<Pick<ConversationSettings, 'pinned' | 'mute_notifications'>> & { version: number }) {
|
||||
return imPatch<ConversationSettings>(`/conversations/${encodeURIComponent(topic)}/settings`, body);
|
||||
}
|
||||
|
||||
export function blockUser(userId: string) {
|
||||
return imPut<void>(`/blocks/${encodeURIComponent(userId)}`);
|
||||
}
|
||||
|
||||
export function unblockUser(userId: string) {
|
||||
return imDelete<void>(`/blocks/${encodeURIComponent(userId)}`);
|
||||
}
|
||||
|
||||
export function getBlocks() {
|
||||
return imGet<{ items: Friend[]; next_cursor?: string }>('/blocks');
|
||||
}
|
||||
Reference in New Issue
Block a user