ui: show sender names in group bubbles with stable colors
All checks were successful
CI / test (push) Successful in 26s
All checks were successful
CI / test (push) Successful in 26s
This commit is contained in:
@@ -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%)`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user