diff --git a/docs/core-checklist-status.md b/docs/core-checklist-status.md index 9885200..f919181 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) +8. Messages (base) - `DONE` (send/read/edit/delete/delete for all; group UI shows sender names over bubbles) 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 a5dec92..5aeeeb2 100644 --- a/web/src/components/MessageList.tsx +++ b/web/src/components/MessageList.tsx @@ -17,6 +17,8 @@ import { useUiStore } from "../store/uiStore"; import { formatTime } from "../utils/format"; import { formatMessageHtml } from "../utils/formatMessage"; import { MediaViewer } from "./MediaViewer"; +import { getUserById } from "../api/users"; +import type { AuthUser } from "../chat/types"; type ContextMenuState = { x: number; @@ -78,6 +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 scrollContainerRef = useRef(null); const messages = useMemo(() => { @@ -179,8 +182,39 @@ export function MessageList() { setThreadError(null); setReactionsByMessage({}); setAttachmentsByMessage({}); + setSenderProfiles({}); }, [activeChatId, setEditingMessage]); + useEffect(() => { + if (!activeChatId || activeChat?.type !== "group") { + return; + } + const senderIds = [...new Set(messages.map((message) => message.sender_id).filter((id) => id !== me?.id))]; + const missing = senderIds.filter((id) => !senderProfiles[id]); + if (!missing.length) { + return; + } + let cancelled = false; + void (async () => { + const rows = await Promise.allSettled(missing.map((id) => getUserById(id))); + if (cancelled) { + return; + } + 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 }; + } + } + if (Object.keys(patch).length) { + setSenderProfiles((prev) => ({ ...prev, ...patch })); + } + })(); + return () => { + cancelled = true; + }; + }, [activeChat?.type, activeChatId, me?.id, messages, senderProfiles]); + useEffect(() => { if (!activeChatId) { setAttachmentsByMessage({}); @@ -483,6 +517,9 @@ export function MessageList() { Math.abs(new Date(message.created_at).getTime() - new Date(prev.created_at).getTime()) < 4 * 60 * 1000 ); const replySource = message.reply_to_message_id ? messagesMap.get(message.reply_to_message_id) : null; + const showSenderName = !own && activeChat?.type === "group" && !groupedWithPrev; + const senderName = formatSenderName(message.sender_id, senderProfiles); + const senderColor = senderNameColor(message.sender_id); const isSelected = selectedIds.has(message.id); const messageReactions = reactionsByMessage[message.id] ?? []; @@ -553,11 +590,22 @@ export function MessageList() { : "border-sky-400 bg-slate-800/60 text-slate-300" }`} > -

{replySource.sender_id === me?.id ? "You" : "Reply"}

+

+ {replySource.sender_id === me?.id ? "You" : formatSenderName(replySource.sender_id, senderProfiles)} +

{replySource.text || "[media]"}

) : null} + {showSenderName ? ( +

+ {senderName} +

+ ) : null} + {renderMessageContent(message, { attachments: attachmentsByMessage[message.id] ?? [], onAttachmentContextMenu: (event, url) => { @@ -1441,3 +1489,22 @@ function formatAudioTime(seconds: number): string { const rem = total % 60; return `${minutes}:${String(rem).padStart(2, "0")}`; } + +function formatSenderName( + senderId: number, + profiles: Record> +): string { + const profile = profiles[senderId]; + if (profile?.name?.trim()) { + return profile.name.trim(); + } + if (profile?.username?.trim()) { + return `@${profile.username.trim()}`; + } + return `User #${senderId}`; +} + +function senderNameColor(senderId: number): string { + const hue = (senderId * 67) % 360; + return `hsl(${hue} 85% 65%)`; +}