feat(web): refine media gallery UX in chat info
Some checks failed
CI / test (push) Failing after 18s
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:
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user