Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f438018400 | |||
| 89003f71a4 | |||
| b364c53c61 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@chat-app/desktop",
|
||||
"version": "0.21.10",
|
||||
"version": "0.21.11",
|
||||
"private": true,
|
||||
"description": "Electron desktop client (Windows / macOS / Linux)",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Reusable avatar that prefers an uploaded image and falls back to a coloured
|
||||
// letter circle. Use this everywhere the app needs to render a profile.
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useCachedAvatarUrl } from '../lib/avatarCache';
|
||||
|
||||
interface Props {
|
||||
@@ -27,7 +29,13 @@ export function Avatar({
|
||||
loading = 'lazy',
|
||||
}: Props) {
|
||||
const effectiveUrl = useCachedAvatarUrl(url);
|
||||
if (effectiveUrl) {
|
||||
// If the image URL is non-empty but unreachable (e.g. the storage object is
|
||||
// missing / 404s), the bare <img> would render broken with no fallback.
|
||||
// Track a load error and degrade to the letter circle instead. Reset on URL
|
||||
// change so a fresh, valid avatar is retried.
|
||||
const [failed, setFailed] = useState(false);
|
||||
useEffect(() => setFailed(false), [effectiveUrl]);
|
||||
if (effectiveUrl && !failed) {
|
||||
return (
|
||||
<img
|
||||
src={effectiveUrl}
|
||||
@@ -35,6 +43,7 @@ export function Avatar({
|
||||
className={'shrink-0 rounded-full object-cover ' + className}
|
||||
draggable={false}
|
||||
loading={loading}
|
||||
onError={() => setFailed(true)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -144,21 +144,18 @@ export function ConversationPage() {
|
||||
byMessage: reactionsByMessage,
|
||||
toggle: toggleReaction,
|
||||
voteExclusive: votePoll,
|
||||
ready: reactionsReady,
|
||||
} = useMessageReactions(messageIds, session?.user.id);
|
||||
|
||||
// Deferred-reveal gate for MessageList: keep the list hidden until messages
|
||||
// AND their reactions (the main post-paint height changer) are loaded, so the
|
||||
// chat opens already-stable instead of flickering through the load cascade.
|
||||
// A 300 ms max-timeout ensures a slow/empty reactions fetch never hangs it.
|
||||
const [revealTimedOut, setRevealTimedOut] = useState(false);
|
||||
useEffect(() => {
|
||||
setRevealTimedOut(false);
|
||||
if (!id || loading || messages.length === 0) return;
|
||||
const tmo = window.setTimeout(() => setRevealTimedOut(true), 300);
|
||||
return () => window.clearTimeout(tmo);
|
||||
}, [id, loading, messages.length]);
|
||||
const listReady = !loading && messages.length > 0 && (reactionsReady || revealTimedOut);
|
||||
// Reveal gate for MessageList: as soon as messages exist (cache hit = first
|
||||
// render, so no spinner and no wait), let the list reveal. We deliberately do
|
||||
// NOT gate on reactions readiness: on a cache-hit chat switch the messages are
|
||||
// already present, and gating on the async reactions fetch held the list at
|
||||
// opacity:0 for up to 300ms and then "popped" it in — that was the residual
|
||||
// chat-switch flicker. Reaction chips stream in a beat later; because the list
|
||||
// is pinned to the bottom, their height growth re-pins with no visible jump.
|
||||
// MessageList still defers its own reveal a few frames until the row-height
|
||||
// measurement settles, so the list still appears already at the final bottom.
|
||||
const listReady = !loading && messages.length > 0;
|
||||
|
||||
const myId = session?.user.id;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user