From 661f8acf632e99d52c3773bfc60aa03c54d91445 Mon Sep 17 00:00:00 2001 From: benya Date: Sun, 8 Mar 2026 18:54:55 +0300 Subject: [PATCH] web(group-ui): show sender avatars on incoming clusters --- docs/core-checklist-status.md | 2 +- web/src/components/MessageList.tsx | 49 +++++++++++++++++++++++++++--- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/docs/core-checklist-status.md b/docs/core-checklist-status.md index 5549916..c7470fd 100644 --- a/docs/core-checklist-status.md +++ b/docs/core-checklist-status.md @@ -14,7 +14,7 @@ Legend: 5. Chat List - `DONE` (all/pinned/archive/sort/unread) 6. Chat Types - `DONE` (private/group/channel) 7. Chat Creation - `DONE` (private/group/channel) -8. Messages (base) - `DONE` (send/read/edit/delete/delete for all; group UI shows sender names over bubbles) +8. Messages (base) - `DONE` (send/read/edit/delete/delete for all; group UI shows sender names over bubbles + sender avatars on incoming message clusters) 9. Message Types - `PARTIAL` (text/photo/video/docs/audio/voice/circle; GIF/stickers via dedicated system missing) 10. Reply/Quote/Threads - `PARTIAL` (reply + quote-like UI + thread panel with nested replies, no dedicated full thread navigation yet) 11. Forwarding - `PARTIAL` (single + bulk; "without author" missing) diff --git a/web/src/components/MessageList.tsx b/web/src/components/MessageList.tsx index df3d160..e3a94e1 100644 --- a/web/src/components/MessageList.tsx +++ b/web/src/components/MessageList.tsx @@ -80,7 +80,7 @@ export function MessageList() { const [threadLoading, setThreadLoading] = useState(false); const [threadError, setThreadError] = useState(null); const [showScrollToBottom, setShowScrollToBottom] = useState(false); - const [senderProfiles, setSenderProfiles] = useState>>({}); + const [senderProfiles, setSenderProfiles] = useState>>({}); const scrollContainerRef = useRef(null); const messages = useMemo(() => { @@ -200,10 +200,15 @@ export function MessageList() { if (cancelled) { return; } - const patch: Record> = {}; + const patch: Record> = {}; for (const row of rows) { if (row.status === "fulfilled") { - patch[row.value.id] = { id: row.value.id, name: row.value.name, username: row.value.username }; + patch[row.value.id] = { + id: row.value.id, + name: row.value.name, + username: row.value.username, + avatar_url: row.value.avatar_url ?? null, + }; } } if (Object.keys(patch).length) { @@ -522,6 +527,8 @@ export function MessageList() { const senderColor = senderNameColor(message.sender_id); const isSelected = selectedIds.has(message.id); const messageReactions = reactionsByMessage[message.id] ?? []; + const showSenderAvatar = !own && activeChat?.type === "group" && !groupedWithPrev; + const senderAvatarUrl = senderProfiles[message.sender_id]?.avatar_url ?? null; return (
@@ -535,7 +542,27 @@ export function MessageList() {
) : null} -
+
+ {!own && activeChat?.type === "group" ? ( + showSenderAvatar ? ( + senderAvatarUrl ? ( + {senderName} + ) : ( +
+ {senderInitials(senderName)} +
+ ) + ) : ( +
+ ) + ) : null}
> + profiles: Record> ): string { const profile = profiles[senderId]; if (profile?.name?.trim()) { @@ -1526,3 +1553,15 @@ function senderNameColor(senderId: number): string { const hue = (senderId * 67) % 360; return `hsl(${hue} 85% 65%)`; } + +function senderInitials(name: string): string { + const words = name + .trim() + .split(/\s+/) + .filter(Boolean) + .slice(0, 2); + if (!words.length) { + return "?"; + } + return words.map((word) => word[0]?.toUpperCase() ?? "").join(""); +}