mirror of
https://github.com/block/buzz.git
synced 2026-11-15 22:02:04 +00:00
perf(desktop): paint thread feedback immediately
Commit the existing thread skeleton before the deferred thread-panel mount, even when the root is already cached. Keep the expensive panel work transitional and cover the one-frame loading state with a mutation-observer regression test. Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
This commit is contained in:
@@ -782,6 +782,20 @@ export const ChannelPane = React.memo(function ChannelPane({
|
||||
useSplitAuxiliaryPane={useSplitAuxiliaryPane}
|
||||
transparentChrome={hasSplitAuxiliaryPane}
|
||||
/>
|
||||
) : shouldShowThreadSkeleton ? (
|
||||
(() => {
|
||||
if (isHuddleTranscript) {
|
||||
return wrapThreadPanel(<HuddleStartingView />);
|
||||
}
|
||||
const panel = (
|
||||
<MessageThreadPanelSkeleton
|
||||
{...threadLayoutProps}
|
||||
onClose={onCloseThread}
|
||||
widthPx={threadPanelWidthPx}
|
||||
/>
|
||||
);
|
||||
return wrapThreadPanel(panel);
|
||||
})()
|
||||
) : threadHeadMessage ? (
|
||||
(() => {
|
||||
const panel = (
|
||||
@@ -854,20 +868,6 @@ export const ChannelPane = React.memo(function ChannelPane({
|
||||
);
|
||||
return wrapThreadPanel(panel);
|
||||
})()
|
||||
) : shouldShowThreadSkeleton ? (
|
||||
(() => {
|
||||
if (isHuddleTranscript) {
|
||||
return wrapThreadPanel(<HuddleStartingView />);
|
||||
}
|
||||
const panel = (
|
||||
<MessageThreadPanelSkeleton
|
||||
{...threadLayoutProps}
|
||||
onClose={onCloseThread}
|
||||
widthPx={threadPanelWidthPx}
|
||||
/>
|
||||
);
|
||||
return wrapThreadPanel(panel);
|
||||
})()
|
||||
) : activeChannel && selectedAgent ? (
|
||||
(() => {
|
||||
// When the panel was opened from a different channel than the
|
||||
|
||||
@@ -157,6 +157,9 @@ export function ChannelScreen({
|
||||
const [editTargetId, setEditTargetId] = React.useState<string | null>(null);
|
||||
const [optimisticOpenThreadHeadId, setOptimisticOpenThreadHeadId] =
|
||||
React.useState<string | null | undefined>(undefined);
|
||||
const [pendingOpenThreadHeadId, setPendingOpenThreadHeadId] = React.useState<
|
||||
string | null
|
||||
>(null);
|
||||
const clearOptimisticThreadOverride = React.useCallback(() => {
|
||||
setOptimisticOpenThreadHeadId(undefined);
|
||||
}, []);
|
||||
@@ -171,6 +174,11 @@ export function ChannelScreen({
|
||||
openThreadHeadId,
|
||||
optimisticOpenThreadHeadId,
|
||||
});
|
||||
React.useEffect(() => {
|
||||
if (pendingOpenThreadHeadId === effectiveOpenThreadHeadId) {
|
||||
setPendingOpenThreadHeadId(null);
|
||||
}
|
||||
}, [effectiveOpenThreadHeadId, pendingOpenThreadHeadId]);
|
||||
const isNotifiedForEffectiveThread =
|
||||
effectiveOpenThreadHeadId != null
|
||||
? isNotifiedForThread(effectiveOpenThreadHeadId)
|
||||
@@ -489,6 +497,7 @@ export function ChannelScreen({
|
||||
recordThreadInteraction,
|
||||
openThreadHeadId: effectiveOpenThreadHeadId,
|
||||
onOptimisticOpenThreadHeadIdChange: setOptimisticOpenThreadHeadId,
|
||||
onPendingOpenThreadHeadIdChange: setPendingOpenThreadHeadId,
|
||||
onRequestEmptyEditDelete: setEmptyDeleteId,
|
||||
sendMessageMutation,
|
||||
setExpandedThreadReplyIds,
|
||||
@@ -683,7 +692,10 @@ export function ChannelScreen({
|
||||
? threadFirstUnreadReplyId
|
||||
: null;
|
||||
const shouldShowThreadSkeleton = Boolean(
|
||||
effectiveOpenThreadHeadId && activeChannel && !displayedThreadHeadMessage,
|
||||
pendingOpenThreadHeadId ||
|
||||
(effectiveOpenThreadHeadId &&
|
||||
activeChannel &&
|
||||
!displayedThreadHeadMessage),
|
||||
);
|
||||
const isNarrowPanelViewport =
|
||||
channelContentWidthPx > 0 &&
|
||||
|
||||
@@ -31,6 +31,7 @@ export function useChannelPaneHandlers({
|
||||
profiles,
|
||||
recordThreadInteraction,
|
||||
onOptimisticOpenThreadHeadIdChange,
|
||||
onPendingOpenThreadHeadIdChange,
|
||||
onRequestEmptyEditDelete,
|
||||
openThreadHeadId,
|
||||
sendMessageMutation,
|
||||
@@ -54,6 +55,7 @@ export function useChannelPaneHandlers({
|
||||
onOptimisticOpenThreadHeadIdChange: React.Dispatch<
|
||||
React.SetStateAction<string | null | undefined>
|
||||
>;
|
||||
onPendingOpenThreadHeadIdChange: (value: string | null) => void;
|
||||
onRequestEmptyEditDelete: (eventId: string) => void;
|
||||
openThreadHeadId: string | null;
|
||||
sendMessageMutation: ReturnType<typeof useSendMessageMutation>;
|
||||
@@ -199,6 +201,7 @@ export function useChannelPaneHandlers({
|
||||
const handleOpenThread = React.useCallback(
|
||||
(message: { id: string }) => {
|
||||
if (openThreadHeadIdRef.current === message.id) {
|
||||
onPendingOpenThreadHeadIdChange(null);
|
||||
deferPanelState(() => {
|
||||
onOptimisticOpenThreadHeadIdChange(null);
|
||||
setOpenThreadHeadId(null);
|
||||
@@ -210,6 +213,7 @@ export function useChannelPaneHandlers({
|
||||
return;
|
||||
}
|
||||
|
||||
onPendingOpenThreadHeadIdChange(message.id);
|
||||
deferPanelState(() => {
|
||||
onOptimisticOpenThreadHeadIdChange(message.id);
|
||||
setOpenThreadHeadId(message.id);
|
||||
@@ -222,6 +226,7 @@ export function useChannelPaneHandlers({
|
||||
[
|
||||
deferPanelState,
|
||||
onOptimisticOpenThreadHeadIdChange,
|
||||
onPendingOpenThreadHeadIdChange,
|
||||
setEditTargetId,
|
||||
setExpandedThreadReplyIds,
|
||||
setOpenThreadHeadId,
|
||||
|
||||
@@ -2781,6 +2781,47 @@ test("composer is focused after switching to a different channel", async ({
|
||||
await expect(input).toBeFocused();
|
||||
});
|
||||
|
||||
test("thread open paints immediate feedback before deferred panel work", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installMockBridge(page, { threadRepliesDelayMs: 800 });
|
||||
await page.goto("/");
|
||||
await page.getByTestId("channel-general").click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||||
|
||||
const rootMessage = page
|
||||
.getByTestId("message-timeline")
|
||||
.getByTestId("message-row")
|
||||
.first();
|
||||
await rootMessage.hover();
|
||||
await page.evaluate(() => {
|
||||
const observed = { loading: false };
|
||||
Object.assign(window, { __BUZZ_THREAD_OPEN_OBSERVED__: observed });
|
||||
const observer = new MutationObserver(() => {
|
||||
if (document.querySelector('[data-testid="message-thread-loading"]')) {
|
||||
observed.loading = true;
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
});
|
||||
|
||||
await rootMessage.getByRole("button", { name: "Reply" }).click();
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(
|
||||
() =>
|
||||
(
|
||||
window as Window & {
|
||||
__BUZZ_THREAD_OPEN_OBSERVED__?: { loading: boolean };
|
||||
}
|
||||
).__BUZZ_THREAD_OPEN_OBSERVED__?.loading ?? false,
|
||||
),
|
||||
)
|
||||
.toBe(true);
|
||||
await expect(page.getByTestId("message-thread-panel")).toBeVisible();
|
||||
});
|
||||
|
||||
test("thread composer is focused after clicking the reply icon", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
Reference in New Issue
Block a user