mirror of
https://github.com/block/buzz.git
synced 2026-10-31 06:21:34 +00:00
fix(desktop): classify timeline prepends so history loads don't bump unread (#1416)
Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
classifyTimelineMessageDelta,
|
||||
BOTTOM_THRESHOLD_PX,
|
||||
buildDayGroupBoundaries,
|
||||
isDeferredTimelineSnapshotStale,
|
||||
@@ -339,6 +340,29 @@ test("no-tearing: stale snapshot keeps all three decisions internally consistent
|
||||
assert.equal(latestKey, "b");
|
||||
});
|
||||
|
||||
test("classifyTimelineMessageDelta: detects older-history prepends", () => {
|
||||
const previous = [message({ id: "a" }), message({ id: "b" })];
|
||||
const current = [message({ id: "older" }), ...previous];
|
||||
|
||||
assert.equal(classifyTimelineMessageDelta({ current, previous }), "prepend");
|
||||
});
|
||||
|
||||
test("classifyTimelineMessageDelta: detects latest-message appends", () => {
|
||||
const previous = [message({ id: "a" }), message({ id: "b" })];
|
||||
const current = [...previous, message({ id: "c" })];
|
||||
|
||||
assert.equal(classifyTimelineMessageDelta({ current, previous }), "append");
|
||||
});
|
||||
|
||||
test("classifyTimelineMessageDelta: unchanged snapshots do not count as arrivals", () => {
|
||||
const previous = [message({ id: "a" }), message({ id: "b" })];
|
||||
|
||||
assert.equal(
|
||||
classifyTimelineMessageDelta({ current: previous, previous }),
|
||||
"none",
|
||||
);
|
||||
});
|
||||
|
||||
// --- deferred reply-list render state (thread side pane) --------------------
|
||||
//
|
||||
// When MessageThreadPanel gates its reply render behind useDeferredValue, the
|
||||
|
||||
@@ -203,6 +203,46 @@ export function selectTimelineBodySurface({
|
||||
return renderState;
|
||||
}
|
||||
|
||||
export type TimelineMessageDelta = "prepend" | "append" | "replace" | "none";
|
||||
|
||||
export function classifyTimelineMessageDelta({
|
||||
current,
|
||||
previous,
|
||||
}: {
|
||||
current: readonly Pick<TimelineMessage, "id">[];
|
||||
previous: readonly Pick<TimelineMessage, "id">[];
|
||||
}): TimelineMessageDelta {
|
||||
if (previous.length === 0 || current.length === 0) {
|
||||
return previous.length === current.length ? "none" : "replace";
|
||||
}
|
||||
|
||||
const previousFirstId = previous[0]?.id;
|
||||
const previousLastId = previous[previous.length - 1]?.id;
|
||||
const currentFirstId = current[0]?.id;
|
||||
const currentLastId = current[current.length - 1]?.id;
|
||||
|
||||
if (previousFirstId === currentFirstId && previousLastId === currentLastId) {
|
||||
if (previous.length === current.length) {
|
||||
return "none";
|
||||
}
|
||||
return current.length > previous.length ? "append" : "replace";
|
||||
}
|
||||
|
||||
if (
|
||||
previousFirstId !== undefined &&
|
||||
currentFirstId !== previousFirstId &&
|
||||
current.some((message) => message.id === previousFirstId)
|
||||
) {
|
||||
return "prepend";
|
||||
}
|
||||
|
||||
if (previousLastId !== undefined && currentLastId !== previousLastId) {
|
||||
return "append";
|
||||
}
|
||||
|
||||
return "replace";
|
||||
}
|
||||
|
||||
export type TimelineSnapshotIdentity = {
|
||||
channelId: string | null;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { classifyTimelineMessageDelta } from "@/features/messages/lib/timelineSnapshot";
|
||||
import type { TimelineMessage } from "@/features/messages/types";
|
||||
|
||||
/**
|
||||
@@ -165,6 +166,7 @@ export function useAnchoredScroll({
|
||||
const prevLastMessageIdRef = React.useRef<string | undefined>(undefined);
|
||||
const prevFirstMessageIdRef = React.useRef<string | undefined>(undefined);
|
||||
const prevMessageCountRef = React.useRef(0);
|
||||
const prevMessagesRef = React.useRef<TimelineMessage[]>([]);
|
||||
const handledTargetIdRef = React.useRef<string | null>(null);
|
||||
const highlightTimeoutRef = React.useRef<number | null>(null);
|
||||
// Tracks a pending rAF queued by pinToBottomOnMount so it can be cancelled
|
||||
@@ -194,6 +196,7 @@ export function useAnchoredScroll({
|
||||
prevLastMessageIdRef.current = undefined;
|
||||
prevFirstMessageIdRef.current = undefined;
|
||||
prevMessageCountRef.current = 0;
|
||||
prevMessagesRef.current = [];
|
||||
handledTargetIdRef.current = null;
|
||||
forceBottomOnNextAppendRef.current = false;
|
||||
settlingRef.current = false;
|
||||
@@ -363,6 +366,7 @@ export function useAnchoredScroll({
|
||||
prevLastMessageIdRef.current = messages[messages.length - 1]?.id;
|
||||
prevFirstMessageIdRef.current = messages[0]?.id;
|
||||
prevMessageCountRef.current = messages.length;
|
||||
prevMessagesRef.current = messages;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -370,7 +374,6 @@ export function useAnchoredScroll({
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
const firstMessage = messages[0];
|
||||
const prevLastId = prevLastMessageIdRef.current;
|
||||
const prevFirstId = prevFirstMessageIdRef.current;
|
||||
const prevCount = prevMessageCountRef.current;
|
||||
const newLatestArrived =
|
||||
lastMessage !== undefined && lastMessage.id !== prevLastId;
|
||||
@@ -378,12 +381,13 @@ export function useAnchoredScroll({
|
||||
// signal. The relay can deliver a message that sorts ahead of an existing
|
||||
// same-second row, so the list grows without the *last* id changing —
|
||||
// `newLatestArrived` misses that case and the unread counter never bumps.
|
||||
const prevMessages = prevMessagesRef.current;
|
||||
const messagesArrived = messages.length - prevCount;
|
||||
const frontChanged =
|
||||
firstMessage !== undefined &&
|
||||
prevFirstId !== undefined &&
|
||||
firstMessage.id !== prevFirstId;
|
||||
const isPrepend = frontChanged && !newLatestArrived;
|
||||
const isPrepend =
|
||||
classifyTimelineMessageDelta({
|
||||
current: messages,
|
||||
previous: prevMessages,
|
||||
}) === "prepend";
|
||||
|
||||
// One-shot: an outbound send armed `scrollToBottomOnNextUpdate`. When the
|
||||
// resulting append lands, snap to bottom regardless of the current anchor,
|
||||
@@ -399,6 +403,7 @@ export function useAnchoredScroll({
|
||||
prevLastMessageIdRef.current = lastMessage?.id;
|
||||
prevFirstMessageIdRef.current = firstMessage?.id;
|
||||
prevMessageCountRef.current = messages.length;
|
||||
prevMessagesRef.current = messages;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -436,6 +441,7 @@ export function useAnchoredScroll({
|
||||
prevLastMessageIdRef.current = lastMessage?.id;
|
||||
prevFirstMessageIdRef.current = firstMessage?.id;
|
||||
prevMessageCountRef.current = messages.length;
|
||||
prevMessagesRef.current = messages;
|
||||
}, [
|
||||
isLoading,
|
||||
messages,
|
||||
|
||||
Reference in New Issue
Block a user