fix(web): history search used response shapes the real server doesn't send
Confirmed live against the real backend: docs/IM-前后端对接文档.md's flat
`/media` items ({thumbnail_file_id,file_id,width,height}) and flat search
hits ({file:{...}}, {url,title,...}) don't match what the server actually
returns. Both endpoints really return the raw original message (`{txt,fmt,
ent}`, search hits wrapped one level deeper in `snapshot`) -- the same shape
lib/messages/drafty.tsx's renderDrafty already knows how to render, reused
here instead of hand-picking fields from a shape that doesn't exist. This is
also why image/video thumbnails were rendering as generic icons: the field
they were reading was never present in a real response.
Extracted useAuthedBlobUrl (authenticated file-fetch-to-blob-URL) out of
AttachmentPreview into lib/hooks.ts so the history modal's own thumbnail
grid can reuse it instead of duplicating the fetch/header logic.
Also adds the URL safety-check gate (docs/IM-前后端对接文档.md:1822,1898)
before opening a link found via the 链接 tab, which this view didn't have
at all before.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
93d4077d1e
commit
1bd8d7f804
@@ -1,45 +1,9 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { Icon } from '@/app/Icon';
|
||||
import { fileAuthHeaders, fileDownloadUrl } from '@/lib/api/upload';
|
||||
import { useAuthedBlobUrl } from '@/lib/hooks';
|
||||
import { fileTypeStyle, formatFileSize } from '@/lib/messages/attachments';
|
||||
|
||||
/**
|
||||
* GET /v0/file/s/{file_id} requires the same auth header as upload (see upload.ts's fileAuthHeaders
|
||||
* for exactly which one, and why) -- a plain <img src="..."> or <a href="..."> can't attach a
|
||||
* custom header, so an externally-referenced (non-inline) attachment needs an authenticated fetch
|
||||
* into a blob URL rather than a direct URL. Inline (base64 `val`) attachments skip this path entirely.
|
||||
*/
|
||||
function useAuthedBlobUrl(ref: string | undefined): { url: string | null; loading: boolean; error: boolean } {
|
||||
const [state, setState] = useState<{ url: string | null; loading: boolean; error: boolean }>({ url: null, loading: !!ref, error: false });
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref) return undefined;
|
||||
let cancelled = false;
|
||||
let objectUrl: string | null = null;
|
||||
setState({ url: null, loading: true, error: false });
|
||||
fetch(fileDownloadUrl(ref), { headers: fileAuthHeaders() })
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error('download failed');
|
||||
return res.blob();
|
||||
})
|
||||
.then((blob) => {
|
||||
if (cancelled) return;
|
||||
objectUrl = URL.createObjectURL(blob);
|
||||
setState({ url: objectUrl, loading: false, error: false });
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setState({ url: null, loading: false, error: true });
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
||||
};
|
||||
}, [ref]);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
interface ImageData {
|
||||
val?: string;
|
||||
ref?: string;
|
||||
@@ -60,7 +24,8 @@ export function ImageAttachment({ data }: { data: ImageData }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <img src={src} alt="" className="max-h-[260px] max-w-[260px] rounded-lg object-cover" />;
|
||||
// README §3.2: fixed 260x170, not a same-aspect-ratio-preserving max-height/width box.
|
||||
return <img src={src} alt="" className="h-[170px] w-[260px] rounded-lg object-cover" />;
|
||||
}
|
||||
|
||||
interface FileData {
|
||||
|
||||
Reference in New Issue
Block a user