mirror of
https://github.com/block/buzz.git
synced 2026-10-28 21:11:48 +00:00
feat(desktop): instant channel switching via data-layer caching (#133)
This commit is contained in:
@@ -33,7 +33,7 @@ const overrides = new Map([
|
||||
["src/app/AppShell.tsx", 750],
|
||||
["src/features/channels/hooks.ts", 525], // canvas query + mutation hooks
|
||||
["src/features/channels/ui/ChannelManagementSheet.tsx", 800],
|
||||
["src/features/messages/ui/MessageComposer.tsx", 650], // media upload handlers (paste, drop, dialog)
|
||||
["src/features/messages/ui/MessageComposer.tsx", 665], // media upload handlers (paste, drop, dialog) + channelId reset effect
|
||||
["src/features/settings/ui/SettingsView.tsx", 600],
|
||||
["src/features/sidebar/ui/AppSidebar.tsx", 850], // channels + forums creation forms
|
||||
["src/features/tokens/ui/TokenSettingsCard.tsx", 800],
|
||||
|
||||
@@ -217,14 +217,6 @@ export function AppShell() {
|
||||
.filter((value) => value && value.trim().length > 0)
|
||||
.join(" ") || "Channel details and activity."
|
||||
: "Connect to the relay to browse channels and read messages.";
|
||||
const contentPaneKey =
|
||||
selectedView === "home"
|
||||
? "home"
|
||||
: selectedView === "agents"
|
||||
? "agents"
|
||||
: selectedView === "settings"
|
||||
? "settings"
|
||||
: `channel:${activeChannel?.id ?? "none"}`;
|
||||
const shouldLoadTimeline =
|
||||
activeChannel !== null && activeChannel.channelType !== "forum";
|
||||
const isTimelineLoading =
|
||||
@@ -589,10 +581,7 @@ export function AppShell() {
|
||||
unreadChannelIds={unreadChannelIds}
|
||||
/>
|
||||
|
||||
<SidebarInset
|
||||
className="min-h-0 min-w-0 overflow-hidden"
|
||||
key={contentPaneKey}
|
||||
>
|
||||
<SidebarInset className="min-h-0 min-w-0 overflow-hidden">
|
||||
{selectedView === "home" ? (
|
||||
<ChatHeader
|
||||
description="Personalized feed for mentions, reminders, channel activity, and agent work."
|
||||
|
||||
@@ -51,8 +51,9 @@ export function ChannelPane({
|
||||
typingPubkeys,
|
||||
}: ChannelPaneProps) {
|
||||
return (
|
||||
<React.Fragment key={activeChannel?.id ?? "no-channel"}>
|
||||
<React.Fragment>
|
||||
<MessageTimeline
|
||||
channelId={activeChannel?.id}
|
||||
activeReplyTargetId={replyTargetId}
|
||||
currentPubkey={currentPubkey}
|
||||
profiles={profiles}
|
||||
|
||||
@@ -78,6 +78,20 @@ export function MessageComposer({
|
||||
}>({ status: "idle" });
|
||||
const [pendingImeta, setPendingImeta] = React.useState<BlobDescriptor[]>([]);
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: channelId is the sole trigger — reset all composer state on channel switch to prevent draft/upload/mention leaks
|
||||
React.useEffect(() => {
|
||||
setContent("");
|
||||
setPendingImeta([]);
|
||||
setUploadState({ status: "idle" });
|
||||
setMentionQuery(null);
|
||||
setMentionStartIndex(0);
|
||||
setMentionSelectedIndex(0);
|
||||
setIsEmojiPickerOpen(false);
|
||||
mentionMapRef.current.clear();
|
||||
draftSelectionRef.current = { end: 0, start: 0 };
|
||||
pendingSelectionRef.current = null;
|
||||
}, [channelId]);
|
||||
|
||||
const membersQuery = useChannelMembersQuery(channelId);
|
||||
const members = membersQuery.data ?? [];
|
||||
const managedAgentsQuery = useManagedAgentsQuery();
|
||||
|
||||
@@ -11,6 +11,7 @@ import { TimelineSkeleton } from "./TimelineSkeleton";
|
||||
import { useTimelineScrollManager } from "./useTimelineScrollManager";
|
||||
|
||||
type MessageTimelineProps = {
|
||||
channelId?: string | null;
|
||||
messages: TimelineMessage[];
|
||||
isLoading?: boolean;
|
||||
emptyTitle?: string;
|
||||
@@ -29,6 +30,7 @@ type MessageTimelineProps = {
|
||||
};
|
||||
|
||||
export function MessageTimeline({
|
||||
channelId,
|
||||
messages,
|
||||
isLoading = false,
|
||||
emptyTitle = "No messages yet",
|
||||
@@ -51,6 +53,7 @@ export function MessageTimeline({
|
||||
syncScrollState,
|
||||
timelineRef,
|
||||
} = useTimelineScrollManager({
|
||||
channelId,
|
||||
isLoading,
|
||||
messages,
|
||||
onTargetReached,
|
||||
|
||||
@@ -4,11 +4,13 @@ import type { TimelineMessage } from "@/features/messages/types";
|
||||
import { isNearBottom } from "./messageTimelineUtils";
|
||||
|
||||
export function useTimelineScrollManager({
|
||||
channelId,
|
||||
isLoading,
|
||||
messages,
|
||||
onTargetReached,
|
||||
targetMessageId,
|
||||
}: {
|
||||
channelId?: string | null;
|
||||
isLoading: boolean;
|
||||
messages: TimelineMessage[];
|
||||
onTargetReached?: (messageId: string) => void;
|
||||
@@ -32,6 +34,24 @@ export function useTimelineScrollManager({
|
||||
string | null
|
||||
>(null);
|
||||
const [newMessageCount, setNewMessageCount] = React.useState(0);
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: channelId is intentionally the sole trigger — we reset all scroll state when the channel changes
|
||||
React.useLayoutEffect(() => {
|
||||
hasInitializedRef.current = false;
|
||||
shouldStickToBottomRef.current = true;
|
||||
isAtBottomRef.current = true;
|
||||
isProgrammaticBottomScrollRef.current = false;
|
||||
previousTimelineHeightRef.current = null;
|
||||
previousScrollTopRef.current = 0;
|
||||
lockedScrollTopRef.current = null;
|
||||
previousLastMessageIdRef.current = undefined;
|
||||
previousMessageCountRef.current = 0;
|
||||
handledTargetMessageIdRef.current = null;
|
||||
setIsAtBottom(true);
|
||||
setHighlightedMessageId(null);
|
||||
setNewMessageCount(0);
|
||||
}, [channelId]);
|
||||
|
||||
const latestMessage =
|
||||
messages.length > 0 ? messages[messages.length - 1] : undefined;
|
||||
|
||||
|
||||
@@ -13,6 +13,10 @@ const queryClient = new QueryClient({
|
||||
queries: {
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false,
|
||||
networkMode: "always",
|
||||
},
|
||||
mutations: {
|
||||
networkMode: "always",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user