feat(web): 查找聊天记录 history search + media grid in the detail panel
Closes a real visual gap flagged directly: the detail panel jumped straight from the quick-action buttons to 备注/置顶聊天/etc, skipping the "查找聊天 记录" row + 4-icon media grid (图片/视频/文件/链接) that's prominently visible in the reference design (README §3.4) -- deliberately deferred in an earlier pass, but a dead-end-free real implementation was worth building now rather than leaving a visible hole. - lib/api/messages.ts: searchTopicMessages/getTopicMedia, endpoint shapes from docs/IM-前后端对接文档.md:1251-1254 (verified live: both return real 200s against the actual backend, not just matching the docs on paper). - HistorySearchModal: centered modal (README §1.5's 弹层 pattern) with the five tabs, debounced search (lib/hooks.ts's useDebouncedValue, ported from the old app's use-cached-data.js), empty/loading states. - DetailColumn: wires the row + grid to open the modal on the right tab. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
0b48346d02
commit
80bf0aa41f
@@ -1,7 +1,7 @@
|
||||
// Message-level REST endpoints that sit alongside the native WS protocol. Recall goes through REST
|
||||
// (not a Tinode WS operation) -- ported from the old app's context-menu.jsx recallOwnMessage, which
|
||||
// calls this then hard-deletes the message from the local topic cache itself.
|
||||
import { imPost } from './client';
|
||||
import { imGet, imPost } from './client';
|
||||
|
||||
export interface RecallResult {
|
||||
topic: string;
|
||||
@@ -24,3 +24,47 @@ export interface RecallResult {
|
||||
export function recallMessage(topic: string, seq: number, version = 1, reason?: string) {
|
||||
return imPost<RecallResult>(`/topics/${encodeURIComponent(topic)}/messages/${seq}/recall`, { version, reason });
|
||||
}
|
||||
|
||||
export interface SearchTextHit {
|
||||
seq: number;
|
||||
from: string;
|
||||
ts: string;
|
||||
snippet: string;
|
||||
matches?: Array<{ start: number; end: number }>;
|
||||
}
|
||||
|
||||
export interface SearchFileHit {
|
||||
seq: number;
|
||||
from: string;
|
||||
ts: string;
|
||||
file: { file_id: string; name: string; size: number; mime: string };
|
||||
availability: string;
|
||||
}
|
||||
|
||||
/** README §3.4's "查找聊天记录" -- ported endpoint shapes from docs/IM-前后端对接文档.md:1251-1252. */
|
||||
export function searchTopicMessages(topic: string, query: string, type: 'text' | 'file' = 'text', cursor = '') {
|
||||
const qs = new URLSearchParams({ q: query, type });
|
||||
if (cursor) qs.set('cursor', cursor);
|
||||
return imGet<{ topic: string; items: Array<SearchTextHit | SearchFileHit>; next_cursor?: string }>(
|
||||
`/topics/${encodeURIComponent(topic)}/messages/search?${qs.toString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
export type MediaType = 'image' | 'video' | 'file' | 'link';
|
||||
|
||||
export interface MediaItem {
|
||||
seq: number;
|
||||
thumbnail_file_id?: string;
|
||||
file_id?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
ts: string;
|
||||
availability: string;
|
||||
}
|
||||
|
||||
/** README §3.4's 4-icon grid (图片/视频/文件/链接). */
|
||||
export function getTopicMedia(topic: string, type: MediaType, cursor = '') {
|
||||
const qs = new URLSearchParams({ type });
|
||||
if (cursor) qs.set('cursor', cursor);
|
||||
return imGet<{ topic: string; items: MediaItem[]; next_cursor?: string }>(`/topics/${encodeURIComponent(topic)}/media?${qs.toString()}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
/** Ported from the old app's use-cached-data.js -- debounces a fast-changing value (e.g. search input). */
|
||||
export function useDebouncedValue<T>(value: T, delay = 300): T {
|
||||
const [debounced, setDebounced] = useState(value);
|
||||
useEffect(() => {
|
||||
const timer = window.setTimeout(() => setDebounced(value), delay);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [value, delay]);
|
||||
return debounced;
|
||||
}
|
||||
Reference in New Issue
Block a user