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 isGroupLike = chat?.type === "group" || chat?.type === "channel";
const showMembersSection = Boolean(chat && isGroupLike && !chat.is_saved); const showMembersSection = Boolean(chat && isGroupLike && !chat.is_saved);
const canManageMembers = Boolean(isGroupLike && (myRole === "owner" || myRole === "admin")); const canManageMembers = Boolean(isGroupLike && (myRole === "owner" || myRole === "admin"));
const photoAttachments = useMemo(() => attachments.filter((item) => item.file_type.startsWith("image/")), [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/")), [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"), [attachments]); const voiceAttachments = useMemo(() => attachments.filter((item) => item.message_type === "voice").sort((a, b) => b.id - a.id), [attachments]);
const audioAttachments = useMemo( 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] [attachments]
); );
const allAttachmentItems = useMemo(() => [...attachments].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"> <p className="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-300">
Media & Files {attachments.length > 0 ? `(${attachments.length})` : ""} Media & Files {attachments.length > 0 ? `(${attachments.length})` : ""}
</p> </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 <button
className={`rounded px-2 py-1 ${attachmentsTab === "all" ? "bg-sky-500/30 text-sky-200" : "text-slate-300"}`} className={`rounded px-2 py-1 ${attachmentsTab === "all" ? "bg-sky-500/30 text-sky-200" : "text-slate-300"}`}
onClick={() => setAttachmentsTab("all")} onClick={() => setAttachmentsTab("all")}
type="button" type="button"
> >
All All ({attachments.length})
</button> </button>
<button <button
className={`rounded px-2 py-1 ${attachmentsTab === "photos" ? "bg-sky-500/30 text-sky-200" : "text-slate-300"}`} className={`rounded px-2 py-1 ${attachmentsTab === "photos" ? "bg-sky-500/30 text-sky-200" : "text-slate-300"}`}
onClick={() => setAttachmentsTab("photos")} onClick={() => setAttachmentsTab("photos")}
type="button" type="button"
> >
Photos Photos ({photoAttachments.length})
</button> </button>
<button <button
className={`rounded px-2 py-1 ${attachmentsTab === "videos" ? "bg-sky-500/30 text-sky-200" : "text-slate-300"}`} className={`rounded px-2 py-1 ${attachmentsTab === "videos" ? "bg-sky-500/30 text-sky-200" : "text-slate-300"}`}
onClick={() => setAttachmentsTab("videos")} onClick={() => setAttachmentsTab("videos")}
type="button" type="button"
> >
Videos Videos ({videoAttachments.length})
</button> </button>
<button <button
className={`rounded px-2 py-1 ${attachmentsTab === "audio" ? "bg-sky-500/30 text-sky-200" : "text-slate-300"}`} className={`rounded px-2 py-1 ${attachmentsTab === "audio" ? "bg-sky-500/30 text-sky-200" : "text-slate-300"}`}
onClick={() => setAttachmentsTab("audio")} onClick={() => setAttachmentsTab("audio")}
type="button" type="button"
> >
Audio Audio ({audioAttachments.length})
</button> </button>
<button <button
className={`rounded px-2 py-1 ${attachmentsTab === "voice" ? "bg-sky-500/30 text-sky-200" : "text-slate-300"}`} className={`rounded px-2 py-1 ${attachmentsTab === "voice" ? "bg-sky-500/30 text-sky-200" : "text-slate-300"}`}
onClick={() => setAttachmentsTab("voice")} onClick={() => setAttachmentsTab("voice")}
type="button" type="button"
> >
Voice Voice ({voiceAttachments.length})
</button> </button>
<button <button
className={`rounded px-2 py-1 ${attachmentsTab === "links" ? "bg-sky-500/30 text-sky-200" : "text-slate-300"}`} className={`rounded px-2 py-1 ${attachmentsTab === "links" ? "bg-sky-500/30 text-sky-200" : "text-slate-300"}`}
onClick={() => setAttachmentsTab("links")} onClick={() => setAttachmentsTab("links")}
type="button" type="button"
> >
Links Links ({linkItems.length})
</button> </button>
</div> </div>
{attachmentsLoading ? <p className="text-xs text-slate-400">Loading attachments...</p> : null} {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" type="button"
> >
<p className="truncate text-xs font-semibold text-sky-300">{item.url}</p> <p className="truncate text-xs font-semibold text-sky-300">{shortLink(item.url)}</p>
<p className="text-[11px] text-slate-400">{new Date(item.createdAt).toLocaleString()}</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> </button>
))} ))}
</div> </div>
@@ -821,3 +825,12 @@ function extractLinkItems(messages: Message[]): Array<{ url: string; messageId:
out.sort((a, b) => b.messageId - a.messageId); out.sort((a, b) => b.messageId - a.messageId);
return out; return out;
} }
function shortLink(url: string): string {
try {
const parsed = new URL(url);
return `${parsed.hostname}${parsed.pathname === "/" ? "" : parsed.pathname}`;
} catch {
return url;
}
}