mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): show skeleton during channel switch (#1141)
Signed-off-by: Tyler Longwell <tlongwell@block.xyz> Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
This commit is contained in:
co-authored by
npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr
Tyler Longwell
parent
959f25982b
commit
d1c3574ba7
@@ -4,6 +4,7 @@ import test from "node:test";
|
||||
import {
|
||||
BOTTOM_THRESHOLD_PX,
|
||||
buildDayGroupBoundaries,
|
||||
isDeferredTimelineSnapshotStale,
|
||||
isNearBottomMetrics,
|
||||
resolveDeepLinkTarget,
|
||||
selectDeferredListRenderState,
|
||||
@@ -414,6 +415,42 @@ test("timeline-body-surface: empty only when live and deferred rows are empty",
|
||||
);
|
||||
});
|
||||
|
||||
test("deferred-snapshot: stale when channel ids diverge during channel switch", () => {
|
||||
assert.equal(
|
||||
isDeferredTimelineSnapshotStale({
|
||||
deferredSnapshot: { channelId: "chan-a" },
|
||||
liveSnapshot: { channelId: "chan-b" },
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("deferred-snapshot: fresh when channel ids match", () => {
|
||||
assert.equal(
|
||||
isDeferredTimelineSnapshotStale({
|
||||
deferredSnapshot: { channelId: "chan-a" },
|
||||
liveSnapshot: { channelId: "chan-a" },
|
||||
}),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("timeline-body-surface: stale deferred channel snapshot paints skeleton instead of old list", () => {
|
||||
const isStale = isDeferredTimelineSnapshotStale({
|
||||
deferredSnapshot: { channelId: "chan-a" },
|
||||
liveSnapshot: { channelId: "chan-b" },
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
selectTimelineBodySurface({
|
||||
deferredCount: 4,
|
||||
isLoading: isStale,
|
||||
liveCount: 0,
|
||||
}),
|
||||
"skeleton",
|
||||
);
|
||||
});
|
||||
|
||||
test("timeline-intro-surface: skeleton suppresses intro while loading", () => {
|
||||
assert.equal(
|
||||
selectTimelineIntroSurface({
|
||||
|
||||
@@ -203,6 +203,20 @@ export function selectTimelineBodySurface({
|
||||
return renderState;
|
||||
}
|
||||
|
||||
export type TimelineSnapshotIdentity = {
|
||||
channelId: string | null;
|
||||
};
|
||||
|
||||
export function isDeferredTimelineSnapshotStale({
|
||||
deferredSnapshot,
|
||||
liveSnapshot,
|
||||
}: {
|
||||
deferredSnapshot: TimelineSnapshotIdentity;
|
||||
liveSnapshot: TimelineSnapshotIdentity;
|
||||
}): boolean {
|
||||
return deferredSnapshot.channelId !== liveSnapshot.channelId;
|
||||
}
|
||||
|
||||
export type TimelineIntroSurface =
|
||||
| "direct-message-intro"
|
||||
| "channel-intro"
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as React from "react";
|
||||
import { Hash } from "lucide-react";
|
||||
|
||||
import {
|
||||
isDeferredTimelineSnapshotStale,
|
||||
selectTimelineBodySurface,
|
||||
selectTimelineIntroSurface,
|
||||
} from "@/features/messages/lib/timelineSnapshot";
|
||||
@@ -113,6 +114,16 @@ type DirectMessageIntroParticipant = {
|
||||
pubkey: string;
|
||||
};
|
||||
|
||||
type TimelineSnapshot = {
|
||||
channelId: string | null;
|
||||
messages: TimelineMessage[];
|
||||
};
|
||||
|
||||
const EMPTY_TIMELINE_SNAPSHOT: TimelineSnapshot = {
|
||||
channelId: null,
|
||||
messages: EMPTY_MESSAGES,
|
||||
};
|
||||
|
||||
const MessageTimelineBase = React.forwardRef<
|
||||
MessageTimelineHandle,
|
||||
MessageTimelineProps
|
||||
@@ -174,15 +185,30 @@ const MessageTimelineBase = React.forwardRef<
|
||||
// scroll/autoscroll/deep-link logic reads the DOM (`scrollIntoView`,
|
||||
// ResizeObserver on the content), so it must stay consistent with what's
|
||||
// actually painted. You can't scroll to a row that hasn't committed yet.
|
||||
const deferredMessages = React.useDeferredValue(messages, EMPTY_MESSAGES);
|
||||
const isRenderPending = deferredMessages !== messages;
|
||||
// Channel id travels with the deferred message snapshot. Without that guard, a
|
||||
// route change can paint the previous channel's deferred rows for a frame even
|
||||
// though the sidebar/header already moved to the new channel.
|
||||
const liveSnapshot = React.useMemo<TimelineSnapshot>(
|
||||
() => ({ channelId: channelId ?? null, messages }),
|
||||
[channelId, messages],
|
||||
);
|
||||
const deferredSnapshot = React.useDeferredValue(
|
||||
liveSnapshot,
|
||||
EMPTY_TIMELINE_SNAPSHOT,
|
||||
);
|
||||
const deferredMessages = deferredSnapshot.messages;
|
||||
const isDeferredSnapshotStale = isDeferredTimelineSnapshotStale({
|
||||
deferredSnapshot,
|
||||
liveSnapshot,
|
||||
});
|
||||
const isRenderPending = deferredSnapshot !== liveSnapshot;
|
||||
const scrollRestorationId = targetMessageId
|
||||
? `message-timeline:${channelId ?? "none"}:target:${targetMessageId}`
|
||||
: `message-timeline:${channelId ?? "none"}`;
|
||||
|
||||
const timelineBodySurface = selectTimelineBodySurface({
|
||||
deferredCount: deferredMessages.length,
|
||||
isLoading,
|
||||
isLoading: isLoading || isDeferredSnapshotStale,
|
||||
liveCount: messages.length,
|
||||
});
|
||||
const showTimelineSkeleton = timelineBodySurface === "skeleton";
|
||||
|
||||
Reference in New Issue
Block a user