web(group-ui): show sender avatars on incoming clusters
All checks were successful
CI / test (push) Successful in 28s
All checks were successful
CI / test (push) Successful in 28s
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -80,7 +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 [senderProfiles, setSenderProfiles] = useState<Record<number, Pick<AuthUser, "id" | "name" | "username" | "avatar_url">>>({});
|
||||
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const messages = useMemo(() => {
|
||||
@@ -200,10 +200,15 @@ export function MessageList() {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
const patch: Record<number, Pick<AuthUser, "id" | "name" | "username">> = {};
|
||||
const patch: Record<number, Pick<AuthUser, "id" | "name" | "username" | "avatar_url">> = {};
|
||||
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 (
|
||||
<div key={`${message.id}-${message.client_message_id ?? ""}`}>
|
||||
@@ -535,7 +542,27 @@ export function MessageList() {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className={`${groupedWithPrev ? "mb-1" : "mb-2"} flex ${own ? "justify-end" : "justify-start"}`}>
|
||||
<div className={`${groupedWithPrev ? "mb-1" : "mb-2"} flex items-end gap-2 ${own ? "justify-end" : "justify-start"}`}>
|
||||
{!own && activeChat?.type === "group" ? (
|
||||
showSenderAvatar ? (
|
||||
senderAvatarUrl ? (
|
||||
<img
|
||||
alt={senderName}
|
||||
className="h-8 w-8 rounded-full border border-slate-700/70 object-cover"
|
||||
src={senderAvatarUrl}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full border border-slate-700/70 bg-slate-800 text-[11px] font-semibold text-slate-200"
|
||||
title={senderName}
|
||||
>
|
||||
{senderInitials(senderName)}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="h-8 w-8 shrink-0" />
|
||||
)
|
||||
) : null}
|
||||
<div
|
||||
id={`message-${message.id}`}
|
||||
className={`max-w-[90%] px-3 py-2.5 shadow-sm md:max-w-[70%] ${
|
||||
@@ -1510,7 +1537,7 @@ function formatAudioTime(seconds: number): string {
|
||||
|
||||
function formatSenderName(
|
||||
senderId: number,
|
||||
profiles: Record<number, Pick<AuthUser, "id" | "name" | "username">>
|
||||
profiles: Record<number, Pick<AuthUser, "id" | "name" | "username" | "avatar_url">>
|
||||
): 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("");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user