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)
|
5. Chat List - `DONE` (all/pinned/archive/sort/unread)
|
||||||
6. Chat Types - `DONE` (private/group/channel)
|
6. Chat Types - `DONE` (private/group/channel)
|
||||||
7. Chat Creation - `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)
|
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)
|
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)
|
11. Forwarding - `PARTIAL` (single + bulk; "without author" missing)
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export function MessageList() {
|
|||||||
const [threadLoading, setThreadLoading] = useState(false);
|
const [threadLoading, setThreadLoading] = useState(false);
|
||||||
const [threadError, setThreadError] = useState<string | null>(null);
|
const [threadError, setThreadError] = useState<string | null>(null);
|
||||||
const [showScrollToBottom, setShowScrollToBottom] = useState(false);
|
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 scrollContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const messages = useMemo(() => {
|
const messages = useMemo(() => {
|
||||||
@@ -200,10 +200,15 @@ export function MessageList() {
|
|||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
return;
|
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) {
|
for (const row of rows) {
|
||||||
if (row.status === "fulfilled") {
|
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) {
|
if (Object.keys(patch).length) {
|
||||||
@@ -522,6 +527,8 @@ export function MessageList() {
|
|||||||
const senderColor = senderNameColor(message.sender_id);
|
const senderColor = senderNameColor(message.sender_id);
|
||||||
const isSelected = selectedIds.has(message.id);
|
const isSelected = selectedIds.has(message.id);
|
||||||
const messageReactions = reactionsByMessage[message.id] ?? [];
|
const messageReactions = reactionsByMessage[message.id] ?? [];
|
||||||
|
const showSenderAvatar = !own && activeChat?.type === "group" && !groupedWithPrev;
|
||||||
|
const senderAvatarUrl = senderProfiles[message.sender_id]?.avatar_url ?? null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={`${message.id}-${message.client_message_id ?? ""}`}>
|
<div key={`${message.id}-${message.client_message_id ?? ""}`}>
|
||||||
@@ -535,7 +542,27 @@ export function MessageList() {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : 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
|
<div
|
||||||
id={`message-${message.id}`}
|
id={`message-${message.id}`}
|
||||||
className={`max-w-[90%] px-3 py-2.5 shadow-sm md:max-w-[70%] ${
|
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(
|
function formatSenderName(
|
||||||
senderId: number,
|
senderId: number,
|
||||||
profiles: Record<number, Pick<AuthUser, "id" | "name" | "username">>
|
profiles: Record<number, Pick<AuthUser, "id" | "name" | "username" | "avatar_url">>
|
||||||
): string {
|
): string {
|
||||||
const profile = profiles[senderId];
|
const profile = profiles[senderId];
|
||||||
if (profile?.name?.trim()) {
|
if (profile?.name?.trim()) {
|
||||||
@@ -1526,3 +1553,15 @@ function senderNameColor(senderId: number): string {
|
|||||||
const hue = (senderId * 67) % 360;
|
const hue = (senderId * 67) % 360;
|
||||||
return `hsl(${hue} 85% 65%)`;
|
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