fix(messages): peer avatar visibility in chat bubbles
- ConversationsContext: subscribe to profiles UPDATE realtime so conversation members[].profile picks up peer avatar / displayName changes without a manual refresh - ConversationPage: skip call_event messages when computing run boundaries. Previously a peer bubble followed by a call event from the same sender was treated as mid-run -> avatar slot collapsed to a placeholder - DM peer-profile fallback already added in previous commit covers transient member-lookup misses
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { parseMessagePayload } from '@chat-app/shared/chat';
|
||||
import { extractErrorCode } from '@chat-app/shared/i18n';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -204,12 +205,26 @@ export function ConversationPage() {
|
||||
) : (
|
||||
<ul className="space-y-0.5">
|
||||
{messages.map((m, idx) => {
|
||||
const prev = messages[idx - 1];
|
||||
const next = messages[idx + 1];
|
||||
const grouped = idx > 0 && prev?.senderId === m.senderId;
|
||||
// Skip call_event messages when computing run boundaries — they
|
||||
// render as centred separators, not chat bubbles, so they
|
||||
// shouldn't count toward sender continuity. Without this,
|
||||
// a real bubble followed by a call event from the same sender
|
||||
// would be treated as "in the middle of a run" and lose its
|
||||
// avatar.
|
||||
const prev = findAdjacent(messages, idx, -1);
|
||||
const next = findAdjacent(messages, idx, +1);
|
||||
const grouped = !!prev && prev.senderId === m.senderId;
|
||||
// Anchor avatar on the LAST message of a run so it aligns with
|
||||
// the bubble's tail (bottom corner). Tail is bottom-left for
|
||||
// mine, bottom-right for peer — see rounded-[…_4px_…] above.
|
||||
const isLastOfRun = !next || next.senderId !== m.senderId;
|
||||
const senderProfile =
|
||||
// DM fallback: if member lookup fails (e.g. transient sync), fall
|
||||
// back to conversation.peer so the peer's avatar still resolves.
|
||||
const memberProfile =
|
||||
conversation?.members.find((mm) => mm.userId === m.senderId)?.profile ?? null;
|
||||
const senderProfile =
|
||||
memberProfile ??
|
||||
(m.senderId !== myId ? (conversation?.peer ?? null) : null);
|
||||
return (
|
||||
<li key={m.id}>
|
||||
<MessageBubble
|
||||
@@ -305,6 +320,25 @@ export function ConversationPage() {
|
||||
);
|
||||
}
|
||||
|
||||
// Walks `messages` from `idx + step` skipping call_event entries until a
|
||||
// regular bubble is found or the array boundary is reached. Used to decide
|
||||
// run-grouping for avatar placement so call separators don't bleed into
|
||||
// sender continuity.
|
||||
function findAdjacent<T extends { plaintext: string | null }>(
|
||||
messages: T[],
|
||||
idx: number,
|
||||
step: 1 | -1,
|
||||
): T | undefined {
|
||||
let i = idx + step;
|
||||
while (i >= 0 && i < messages.length) {
|
||||
const m = messages[i];
|
||||
if (!m) return undefined;
|
||||
if (parseMessagePayload(m.plaintext).kind !== 'call_event') return m;
|
||||
i += step;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function Banner({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user