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:
Your Name
2026-09-08 02:27:12 +08:00
co-authored by Claude Sonnet 5
parent 0b48346d02
commit 80bf0aa41f
4 changed files with 216 additions and 2 deletions
+45 -1
View File
@@ -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()}`);
}
+11
View File
@@ -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;
}