ui: show sender names in group bubbles with stable colors
All checks were successful
CI / test (push) Successful in 26s

This commit is contained in:
2026-03-08 18:49:20 +03:00
parent db700bcbcd
commit f186f12bde
2 changed files with 69 additions and 2 deletions

View File

@@ -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<string | null>(null);
const [showScrollToBottom, setShowScrollToBottom] = useState(false);
const [senderProfiles, setSenderProfiles] = useState<Record<number, Pick<AuthUser, "id" | "name" | "username">>>({});
const scrollContainerRef = useRef<HTMLDivElement | null>(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<number, Pick<AuthUser, "id" | "name" | "username">> = {};
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"
}`}
>
<p className="truncate font-semibold">{replySource.sender_id === me?.id ? "You" : "Reply"}</p>
<p
className="truncate font-semibold"
style={replySource.sender_id === me?.id ? undefined : activeChat?.type === "group" ? { color: senderNameColor(replySource.sender_id) } : undefined}
>
{replySource.sender_id === me?.id ? "You" : formatSenderName(replySource.sender_id, senderProfiles)}
</p>
<p className="truncate">{replySource.text || "[media]"}</p>
</div>
) : null}
{showSenderName ? (
<p className="mb-1 truncate text-[12px] font-semibold" style={{ color: senderColor }}>
{senderName}
</p>
) : 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<number, Pick<AuthUser, "id" | "name" | "username">>
): 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%)`;
}