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:
@@ -151,6 +151,15 @@ export function ConversationsProvider({ children }: { children: ReactNode }) {
|
|||||||
.on('postgres_changes', { event: '*', schema: 'public', table: 'conversations' }, () => {
|
.on('postgres_changes', { event: '*', schema: 'public', table: 'conversations' }, () => {
|
||||||
void refresh();
|
void refresh();
|
||||||
})
|
})
|
||||||
|
.on(
|
||||||
|
'postgres_changes',
|
||||||
|
{ event: 'UPDATE', schema: 'public', table: 'profiles' },
|
||||||
|
() => {
|
||||||
|
// Peer updated their profile (e.g. uploaded avatar / changed name).
|
||||||
|
// Re-pull conversations so members[].profile picks up the new data.
|
||||||
|
void refresh();
|
||||||
|
},
|
||||||
|
)
|
||||||
.on(
|
.on(
|
||||||
'postgres_changes',
|
'postgres_changes',
|
||||||
{ event: 'INSERT', schema: 'public', table: 'messages' },
|
{ event: 'INSERT', schema: 'public', table: 'messages' },
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { parseMessagePayload } from '@chat-app/shared/chat';
|
||||||
import { extractErrorCode } from '@chat-app/shared/i18n';
|
import { extractErrorCode } from '@chat-app/shared/i18n';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -204,12 +205,26 @@ export function ConversationPage() {
|
|||||||
) : (
|
) : (
|
||||||
<ul className="space-y-0.5">
|
<ul className="space-y-0.5">
|
||||||
{messages.map((m, idx) => {
|
{messages.map((m, idx) => {
|
||||||
const prev = messages[idx - 1];
|
// Skip call_event messages when computing run boundaries — they
|
||||||
const next = messages[idx + 1];
|
// render as centred separators, not chat bubbles, so they
|
||||||
const grouped = idx > 0 && prev?.senderId === m.senderId;
|
// 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 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;
|
conversation?.members.find((mm) => mm.userId === m.senderId)?.profile ?? null;
|
||||||
|
const senderProfile =
|
||||||
|
memberProfile ??
|
||||||
|
(m.senderId !== myId ? (conversation?.peer ?? null) : null);
|
||||||
return (
|
return (
|
||||||
<li key={m.id}>
|
<li key={m.id}>
|
||||||
<MessageBubble
|
<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 }) {
|
function Banner({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
Reference in New Issue
Block a user