mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
desktop: open thread pane optimistically (#1143)
Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
co-authored by
npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf
parent
9c6852824e
commit
663b554c7c
@@ -139,6 +139,7 @@ type ChannelPaneProps = {
|
||||
personaLookup?: Map<string, string>;
|
||||
profiles?: UserProfileLookup;
|
||||
openThreadHeadId: string | null;
|
||||
shouldShowThreadSkeleton: boolean;
|
||||
openAgentSessionPubkey: string | null;
|
||||
onProfilePanelViewChange: (
|
||||
view: ProfilePanelView,
|
||||
@@ -225,6 +226,7 @@ export const ChannelPane = React.memo(function ChannelPane({
|
||||
personaLookup,
|
||||
profiles,
|
||||
openThreadHeadId,
|
||||
shouldShowThreadSkeleton,
|
||||
openAgentSessionPubkey,
|
||||
onProfilePanelViewChange,
|
||||
profilePanelPubkey,
|
||||
@@ -860,7 +862,7 @@ export const ChannelPane = React.memo(function ChannelPane({
|
||||
panel
|
||||
);
|
||||
})()
|
||||
: openThreadHeadId && activeChannel
|
||||
: shouldShowThreadSkeleton
|
||||
? (() => {
|
||||
const panel = (
|
||||
<MessageThreadPanelSkeleton
|
||||
|
||||
@@ -119,8 +119,6 @@ export function ChannelScreen({
|
||||
const [isAddBotOpen, setIsAddBotOpen] = React.useState(false);
|
||||
const [channelContentRef, channelContentWidthPx] =
|
||||
useElementWidth<HTMLDivElement>();
|
||||
const isNotifiedForCurrentThread =
|
||||
openThreadHeadId != null ? isNotifiedForThread(openThreadHeadId) : false;
|
||||
const [expandedThreadReplyIds, setExpandedThreadReplyIds] = React.useState(
|
||||
() => new Set<string>(),
|
||||
);
|
||||
@@ -131,9 +129,42 @@ export function ChannelScreen({
|
||||
string | null
|
||||
>(null);
|
||||
const [editTargetId, setEditTargetId] = React.useState<string | null>(null);
|
||||
// Thread panel state is URL-backed, but router navigation is intentionally
|
||||
// deferred out of the click handler. Keep a tiny optimistic override so the
|
||||
// auxiliary pane can open/close in the urgent render, then let the URL-backed
|
||||
// state hydrate the real thread contents when it catches up.
|
||||
const [optimisticOpenThreadHeadId, setOptimisticOpenThreadHeadId] =
|
||||
React.useState<string | null | undefined>(undefined);
|
||||
const clearOptimisticThreadOverride = React.useCallback(() => {
|
||||
setOptimisticOpenThreadHeadId(undefined);
|
||||
}, []);
|
||||
const mainInsetRef = useMainInsetRef();
|
||||
const currentPubkey = currentIdentity?.pubkey;
|
||||
const activeChannelId = activeChannel?.id ?? null;
|
||||
const effectiveOpenThreadHeadId =
|
||||
optimisticOpenThreadHeadId === undefined
|
||||
? openThreadHeadId
|
||||
: optimisticOpenThreadHeadId;
|
||||
const isNotifiedForOpenThread =
|
||||
openThreadHeadId != null ? isNotifiedForThread(openThreadHeadId) : false;
|
||||
const isNotifiedForEffectiveThread =
|
||||
effectiveOpenThreadHeadId != null
|
||||
? isNotifiedForThread(effectiveOpenThreadHeadId)
|
||||
: false;
|
||||
const previousActiveChannelIdRef = React.useRef(activeChannelId);
|
||||
React.useEffect(() => {
|
||||
const didChangeChannel =
|
||||
previousActiveChannelIdRef.current !== activeChannelId;
|
||||
previousActiveChannelIdRef.current = activeChannelId;
|
||||
setOptimisticOpenThreadHeadId((current) => {
|
||||
if (current === undefined) {
|
||||
return current;
|
||||
}
|
||||
return didChangeChannel || openThreadHeadId === current
|
||||
? undefined
|
||||
: current;
|
||||
});
|
||||
}, [activeChannelId, openThreadHeadId]);
|
||||
const messagesQuery = useChannelMessagesQuery(activeChannel);
|
||||
useChannelSubscription(activeChannel);
|
||||
const { fetchOlder, hasOlderMessages, isFetchingOlder } =
|
||||
@@ -288,7 +319,7 @@ export function ChannelScreen({
|
||||
activeChannelId,
|
||||
channelMembers,
|
||||
managedAgents,
|
||||
openThreadHeadId,
|
||||
openThreadHeadId: effectiveOpenThreadHeadId,
|
||||
relayAgents,
|
||||
typingEntries,
|
||||
});
|
||||
@@ -369,7 +400,7 @@ export function ChannelScreen({
|
||||
openThreadHeadId,
|
||||
threadReplyTargetId,
|
||||
expandedThreadReplyIds,
|
||||
isNotifiedForCurrentThread,
|
||||
isNotifiedForCurrentThread: isNotifiedForOpenThread,
|
||||
getChannelReadAt,
|
||||
getThreadReadAt,
|
||||
markChannelUnread,
|
||||
@@ -404,7 +435,8 @@ export function ChannelScreen({
|
||||
getReplyDescendantIdsForMessage,
|
||||
getSubtreeMaxCreatedAt,
|
||||
markThreadRead,
|
||||
openThreadHeadId,
|
||||
openThreadHeadId: effectiveOpenThreadHeadId,
|
||||
onOptimisticOpenThreadHeadIdChange: setOptimisticOpenThreadHeadId,
|
||||
sendMessageMutation,
|
||||
setExpandedThreadReplyIds,
|
||||
setEditTargetId,
|
||||
@@ -540,6 +572,7 @@ export function ChannelScreen({
|
||||
if (isTimelineLoading) {
|
||||
return;
|
||||
}
|
||||
clearOptimisticThreadOverride();
|
||||
setOpenThreadHeadId(null, { replace: true });
|
||||
setExpandedThreadReplyIds(new Set());
|
||||
setThreadScrollTargetId(null);
|
||||
@@ -558,6 +591,7 @@ export function ChannelScreen({
|
||||
setEditTargetId(null);
|
||||
}
|
||||
}, [
|
||||
clearOptimisticThreadOverride,
|
||||
editTargetId,
|
||||
editTargetMessage,
|
||||
isTimelineLoading,
|
||||
@@ -570,7 +604,23 @@ export function ChannelScreen({
|
||||
|
||||
useLoadMissingAncestors(activeChannel, resolvedMessages);
|
||||
const hasAuxiliaryPanel = Boolean(
|
||||
openThreadHeadMessage || openAgentSessionPubkey || profilePanelPubkey,
|
||||
effectiveOpenThreadHeadId || openAgentSessionPubkey || profilePanelPubkey,
|
||||
);
|
||||
const displayedThreadHeadMessage =
|
||||
openThreadHeadMessage?.id === effectiveOpenThreadHeadId
|
||||
? openThreadHeadMessage
|
||||
: null;
|
||||
const displayedThreadMessages = displayedThreadHeadMessage
|
||||
? threadMessages
|
||||
: [];
|
||||
const displayedThreadReplyTargetMessage = displayedThreadHeadMessage
|
||||
? threadReplyTargetMessage
|
||||
: null;
|
||||
const displayedThreadFirstUnreadReplyId = displayedThreadHeadMessage
|
||||
? threadFirstUnreadReplyId
|
||||
: null;
|
||||
const shouldShowThreadSkeleton = Boolean(
|
||||
effectiveOpenThreadHeadId && activeChannel && !displayedThreadHeadMessage,
|
||||
);
|
||||
const isNarrowPanelViewport =
|
||||
channelContentWidthPx > 0 &&
|
||||
@@ -673,7 +723,7 @@ export function ChannelScreen({
|
||||
followThreadById={followThread}
|
||||
unfollowThreadById={unfollowThread}
|
||||
isFollowingThreadById={isFollowingThread}
|
||||
isFollowingThread={isNotifiedForCurrentThread}
|
||||
isFollowingThread={isNotifiedForEffectiveThread}
|
||||
isSending={sendMessageMutation.isPending}
|
||||
isSinglePanelView={isSinglePanelView}
|
||||
isTimelineLoading={isTimelineLoading}
|
||||
@@ -681,13 +731,15 @@ export function ChannelScreen({
|
||||
onCancelEdit={handleCancelEdit}
|
||||
onCancelThreadReply={handleCancelThreadReply}
|
||||
onFollowThread={
|
||||
openThreadHeadId != null && !isNotifiedForCurrentThread
|
||||
? () => followThread(openThreadHeadId)
|
||||
effectiveOpenThreadHeadId != null &&
|
||||
!isNotifiedForEffectiveThread
|
||||
? () => followThread(effectiveOpenThreadHeadId)
|
||||
: undefined
|
||||
}
|
||||
onUnfollowThread={
|
||||
openThreadHeadId != null && isNotifiedForCurrentThread
|
||||
? () => unfollowThread(openThreadHeadId)
|
||||
effectiveOpenThreadHeadId != null &&
|
||||
isNotifiedForEffectiveThread
|
||||
? () => unfollowThread(effectiveOpenThreadHeadId)
|
||||
: undefined
|
||||
}
|
||||
onCloseAgentSession={handleCloseAgentSession}
|
||||
@@ -718,7 +770,8 @@ export function ChannelScreen({
|
||||
onTargetReached={handleTargetReached}
|
||||
onToggleReaction={effectiveToggleReaction}
|
||||
openAgentSessionPubkey={openAgentSessionPubkey}
|
||||
openThreadHeadId={openThreadHeadId}
|
||||
openThreadHeadId={effectiveOpenThreadHeadId}
|
||||
shouldShowThreadSkeleton={shouldShowThreadSkeleton}
|
||||
onProfilePanelViewChange={setProfilePanelView}
|
||||
profilePanelPubkey={profilePanelPubkey}
|
||||
profilePanelView={profilePanelView}
|
||||
@@ -727,15 +780,15 @@ export function ChannelScreen({
|
||||
firstUnreadMessageId={firstUnreadMessageId}
|
||||
unreadCount={unreadCount}
|
||||
targetMessageId={mainTimelineTargetMessageId}
|
||||
threadHeadMessage={openThreadHeadMessage}
|
||||
threadMessages={threadMessages}
|
||||
threadHeadMessage={displayedThreadHeadMessage}
|
||||
threadMessages={displayedThreadMessages}
|
||||
threadPanelWidthPx={threadPanelWidthPx}
|
||||
threadTypingPubkeys={threadTypingPubkeys}
|
||||
threadReplyTargetMessage={threadReplyTargetMessage}
|
||||
threadReplyTargetMessage={displayedThreadReplyTargetMessage}
|
||||
threadScrollTargetId={threadScrollTargetId}
|
||||
threadUnreadCounts={threadUnreadCounts}
|
||||
threadReplyUnreadCounts={threadReplyUnreadCounts}
|
||||
threadFirstUnreadReplyId={threadFirstUnreadReplyId}
|
||||
threadFirstUnreadReplyId={displayedThreadFirstUnreadReplyId}
|
||||
isJoining={joinChannelMutation.isPending}
|
||||
onJoinChannel={joinChannelMutation.mutateAsync}
|
||||
typingPubkeys={humanTypingPubkeys}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { flushSync } from "react-dom";
|
||||
|
||||
import type {
|
||||
useDeleteMessageMutation,
|
||||
useEditMessageMutation,
|
||||
@@ -24,6 +26,7 @@ export function useChannelPaneHandlers({
|
||||
getReplyDescendantIdsForMessage,
|
||||
getSubtreeMaxCreatedAt,
|
||||
markThreadRead,
|
||||
onOptimisticOpenThreadHeadIdChange,
|
||||
openThreadHeadId,
|
||||
sendMessageMutation,
|
||||
setExpandedThreadReplyIds,
|
||||
@@ -42,6 +45,9 @@ export function useChannelPaneHandlers({
|
||||
getReplyDescendantIdsForMessage: (messageId: string) => string[];
|
||||
getSubtreeMaxCreatedAt: (messageId: string) => number | null;
|
||||
markThreadRead: (rootId: string, timestamp: number) => void;
|
||||
onOptimisticOpenThreadHeadIdChange: React.Dispatch<
|
||||
React.SetStateAction<string | null | undefined>
|
||||
>;
|
||||
openThreadHeadId: string | null;
|
||||
sendMessageMutation: ReturnType<typeof useSendMessageMutation>;
|
||||
setExpandedThreadReplyIds: React.Dispatch<React.SetStateAction<Set<string>>>;
|
||||
@@ -82,11 +88,15 @@ export function useChannelPaneHandlers({
|
||||
}, [setThreadReplyTargetId]);
|
||||
|
||||
const handleCloseThread = React.useCallback(() => {
|
||||
flushSync(() => {
|
||||
onOptimisticOpenThreadHeadIdChange(null);
|
||||
});
|
||||
setOpenThreadHeadId(null);
|
||||
setThreadReplyTargetId(null);
|
||||
setThreadScrollTargetId(null);
|
||||
setExpandedThreadReplyIds(new Set());
|
||||
}, [
|
||||
onOptimisticOpenThreadHeadIdChange,
|
||||
setExpandedThreadReplyIds,
|
||||
setOpenThreadHeadId,
|
||||
setThreadReplyTargetId,
|
||||
@@ -127,6 +137,9 @@ export function useChannelPaneHandlers({
|
||||
const handleOpenThread = React.useCallback(
|
||||
(message: { id: string }) => {
|
||||
if (openThreadHeadIdRef.current === message.id) {
|
||||
flushSync(() => {
|
||||
onOptimisticOpenThreadHeadIdChange(null);
|
||||
});
|
||||
setOpenThreadHeadId(null);
|
||||
setThreadReplyTargetId(null);
|
||||
setThreadScrollTargetId(null);
|
||||
@@ -135,6 +148,9 @@ export function useChannelPaneHandlers({
|
||||
return;
|
||||
}
|
||||
|
||||
flushSync(() => {
|
||||
onOptimisticOpenThreadHeadIdChange(message.id);
|
||||
});
|
||||
setOpenThreadHeadId(message.id);
|
||||
setThreadReplyTargetId(message.id);
|
||||
setThreadScrollTargetId(null);
|
||||
@@ -142,6 +158,7 @@ export function useChannelPaneHandlers({
|
||||
setEditTargetId(null);
|
||||
},
|
||||
[
|
||||
onOptimisticOpenThreadHeadIdChange,
|
||||
setEditTargetId,
|
||||
setExpandedThreadReplyIds,
|
||||
setOpenThreadHeadId,
|
||||
|
||||
Reference in New Issue
Block a user