feat(web): refine media gallery UX in chat info
Some checks failed
CI / test (push) Failing after 18s

- add per-tab counters and sticky media tabs

- normalize media ordering by newest first

- improve links tab readability with short host/path preview
This commit is contained in:
2026-03-08 11:10:25 +03:00
parent 48f521e551
commit c58678ee09

View File

@@ -63,11 +63,14 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) {
const isGroupLike = chat?.type === "group" || chat?.type === "channel";
const showMembersSection = Boolean(chat && isGroupLike && !chat.is_saved);
const canManageMembers = Boolean(isGroupLike && (myRole === "owner" || myRole === "admin"));
const photoAttachments = useMemo(() => attachments.filter((item) => item.file_type.startsWith("image/")), [attachments]);
const videoAttachments = useMemo(() => attachments.filter((item) => item.file_type.startsWith("video/")), [attachments]);
const voiceAttachments = useMemo(() => attachments.filter((item) => item.message_type === "voice"), [attachments]);
const photoAttachments = useMemo(() => attachments.filter((item) => item.file_type.startsWith("image/")).sort((a, b) => b.id - a.id), [attachments]);
const videoAttachments = useMemo(() => attachments.filter((item) => item.file_type.startsWith("video/")).sort((a, b) => b.id - a.id), [attachments]);
const voiceAttachments = useMemo(() => attachments.filter((item) => item.message_type === "voice").sort((a, b) => b.id - a.id), [attachments]);
const audioAttachments = useMemo(
() => attachments.filter((item) => item.message_type === "audio" || (item.file_type.startsWith("audio/") && item.message_type !== "voice")),
() =>
attachments
.filter((item) => item.message_type === "audio" || (item.file_type.startsWith("audio/") && item.message_type !== "voice"))
.sort((a, b) => b.id - a.id),
[attachments]
);
const allAttachmentItems = useMemo(() => [...attachments].sort((a, b) => b.id - a.id), [attachments]);
@@ -386,48 +389,48 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) {
<p className="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-300">
Media & Files {attachments.length > 0 ? `(${attachments.length})` : ""}
</p>
<div className="mb-2 flex items-center gap-2 border-b border-slate-700/60 pb-2 text-xs">
<div className="sticky top-0 z-10 mb-2 flex items-center gap-2 border-b border-slate-700/60 bg-slate-800/90 pb-2 pt-1 text-xs backdrop-blur">
<button
className={`rounded px-2 py-1 ${attachmentsTab === "all" ? "bg-sky-500/30 text-sky-200" : "text-slate-300"}`}
onClick={() => setAttachmentsTab("all")}
type="button"
>
All
All ({attachments.length})
</button>
<button
className={`rounded px-2 py-1 ${attachmentsTab === "photos" ? "bg-sky-500/30 text-sky-200" : "text-slate-300"}`}
onClick={() => setAttachmentsTab("photos")}
type="button"
>
Photos
Photos ({photoAttachments.length})
</button>
<button
className={`rounded px-2 py-1 ${attachmentsTab === "videos" ? "bg-sky-500/30 text-sky-200" : "text-slate-300"}`}
onClick={() => setAttachmentsTab("videos")}
type="button"
>
Videos
Videos ({videoAttachments.length})
</button>
<button
className={`rounded px-2 py-1 ${attachmentsTab === "audio" ? "bg-sky-500/30 text-sky-200" : "text-slate-300"}`}
onClick={() => setAttachmentsTab("audio")}
type="button"
>
Audio
Audio ({audioAttachments.length})
</button>
<button
className={`rounded px-2 py-1 ${attachmentsTab === "voice" ? "bg-sky-500/30 text-sky-200" : "text-slate-300"}`}
onClick={() => setAttachmentsTab("voice")}
type="button"
>
Voice
Voice ({voiceAttachments.length})
</button>
<button
className={`rounded px-2 py-1 ${attachmentsTab === "links" ? "bg-sky-500/30 text-sky-200" : "text-slate-300"}`}
onClick={() => setAttachmentsTab("links")}
type="button"
>
Links
Links ({linkItems.length})
</button>
</div>
{attachmentsLoading ? <p className="text-xs text-slate-400">Loading attachments...</p> : null}
@@ -518,8 +521,9 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) {
}}
type="button"
>
<p className="truncate text-xs font-semibold text-sky-300">{item.url}</p>
<p className="text-[11px] text-slate-400">{new Date(item.createdAt).toLocaleString()}</p>
<p className="truncate text-xs font-semibold text-sky-300">{shortLink(item.url)}</p>
<p className="truncate text-[11px] text-slate-400">{item.url}</p>
<p className="text-[11px] text-slate-500">{new Date(item.createdAt).toLocaleString()}</p>
</button>
))}
</div>
@@ -821,3 +825,12 @@ function extractLinkItems(messages: Message[]): Array<{ url: string; messageId:
out.sort((a, b) => b.messageId - a.messageId);
return out;
}
function shortLink(url: string): string {
try {
const parsed = new URL(url);
return `${parsed.hostname}${parsed.pathname === "/" ? "" : parsed.pathname}`;
} catch {
return url;
}
}