Skip to content

Commit 96b3810

Browse files
committed
Tighten sidebar controller subscriptions
1 parent 6aea4a2 commit 96b3810

3 files changed

Lines changed: 175 additions & 79 deletions

File tree

apps/web/src/components/Sidebar.tsx

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,6 @@ import {
115115
resolveThreadRowClassName,
116116
resolveThreadStatusPill,
117117
orderItemsByPreferredIds,
118-
shouldClearThreadSelectionOnMouseDown,
119118
sortThreadsForSidebar,
120119
ThreadStatusPill,
121120
} from "./Sidebar.logic";
@@ -131,6 +130,7 @@ import { THREAD_PREVIEW_LIMIT } from "./sidebar/sidebarConstants";
131130
import {
132131
SidebarKeyboardController,
133132
SidebarProjectOrderingController,
133+
SidebarSelectionController,
134134
} from "./sidebar/sidebarControllers";
135135
import {
136136
collapseSidebarProjectThreadList,
@@ -2282,7 +2282,6 @@ export default function Sidebar() {
22822282
const suppressProjectClickAfterDragRef = useRef(false);
22832283
const suppressProjectClickForContextMenuRef = useRef(false);
22842284
const [desktopUpdateState, setDesktopUpdateState] = useState<DesktopUpdateState | null>(null);
2285-
const selectedThreadCount = useThreadSelectionStore((s) => s.selectedThreadKeys.size);
22862285
const clearSelection = useThreadSelectionStore((s) => s.clearSelection);
22872286
const setSelectionAnchor = useThreadSelectionStore((s) => s.setAnchor);
22882287
const isLinuxDesktop = isElectron && isLinuxPlatform(navigator.platform);
@@ -2547,20 +2546,6 @@ export default function Sidebar() {
25472546
};
25482547
}, []);
25492548

2550-
useEffect(() => {
2551-
const onMouseDown = (event: globalThis.MouseEvent) => {
2552-
if (selectedThreadCount === 0) return;
2553-
const target = event.target instanceof HTMLElement ? event.target : null;
2554-
if (!shouldClearThreadSelectionOnMouseDown(target)) return;
2555-
clearSelection();
2556-
};
2557-
2558-
window.addEventListener("mousedown", onMouseDown);
2559-
return () => {
2560-
window.removeEventListener("mousedown", onMouseDown);
2561-
};
2562-
}, [clearSelection, selectedThreadCount]);
2563-
25642549
useEffect(() => {
25652550
if (!isElectron) return;
25662551
const bridge = window.desktopBridge;
@@ -2674,6 +2659,7 @@ export default function Sidebar() {
26742659
physicalToLogicalKey={physicalToLogicalKey}
26752660
sidebarProjectSortOrder={sidebarProjectSortOrder}
26762661
/>
2662+
<SidebarSelectionController />
26772663
<SidebarKeyboardController
26782664
navigateToThread={navigateToThread}
26792665
physicalToLogicalKey={physicalToLogicalKey}

apps/web/src/components/sidebar/sidebarControllers.tsx

Lines changed: 69 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { parseScopedThreadKey, scopedThreadKey } from "@t3tools/client-runtime";
33
import { useParams } from "@tanstack/react-router";
44
import type { ScopedThreadRef } from "@t3tools/contracts";
55
import type { SidebarProjectSortOrder, SidebarThreadSortOrder } from "@t3tools/contracts/settings";
6+
import { useShallow } from "zustand/react/shallow";
67
import { isTerminalFocused } from "../../lib/terminalFocus";
78
import { resolveThreadRouteRef } from "../../threadRoutes";
89
import {
@@ -17,15 +18,14 @@ import { selectThreadTerminalState, useTerminalStateStore } from "../../terminal
1718
import { useUiStateStore } from "../../uiStateStore";
1819
import {
1920
resolveAdjacentThreadId,
20-
sortProjectsForSidebar,
21+
shouldClearThreadSelectionOnMouseDown,
2122
useThreadJumpHintVisibility,
2223
} from "../Sidebar.logic";
2324
import {
2425
createSidebarActiveRouteProjectKeySelectorByRef,
25-
createSidebarProjectOrderingThreadSnapshotsSelector,
26-
createSidebarSortedThreadKeysByLogicalProjectSelector,
26+
createSidebarSortedProjectKeysSelector,
27+
createSidebarVisibleThreadKeysSelector,
2728
} from "./sidebarSelectors";
28-
import { THREAD_PREVIEW_LIMIT } from "./sidebarConstants";
2929
import type { LogicalProjectKey } from "../../logicalProject";
3030
import {
3131
setSidebarKeyboardState,
@@ -36,6 +36,7 @@ import {
3636
import type { SidebarProjectSnapshot } from "./sidebarProjectSnapshots";
3737
import { useServerKeybindings } from "../../rpc/serverState";
3838
import { useStore } from "../../store";
39+
import { useThreadSelectionStore } from "../../threadSelectionStore";
3940

4041
const EMPTY_THREAD_JUMP_LABELS = new Map<string, string>();
4142

@@ -70,56 +71,53 @@ function buildThreadJumpLabelMap(input: {
7071
}
7172

7273
function useSidebarKeyboardController(input: {
74+
physicalToLogicalKey: ReadonlyMap<string, LogicalProjectKey>;
7375
sortedProjectKeys: readonly LogicalProjectKey[];
74-
sortedThreadKeysByLogicalProject: ReadonlyMap<LogicalProjectKey, readonly string[]>;
7576
expandedThreadListsByProject: ReadonlySet<LogicalProjectKey>;
7677
routeThreadRef: ScopedThreadRef | null;
7778
routeThreadKey: string | null;
7879
platform: string;
7980
keybindings: ReturnType<typeof useServerKeybindings>;
8081
navigateToThread: (threadRef: ScopedThreadRef) => void;
82+
threadSortOrder: SidebarThreadSortOrder;
8183
}) {
8284
const {
85+
physicalToLogicalKey,
8386
sortedProjectKeys,
84-
sortedThreadKeysByLogicalProject,
8587
expandedThreadListsByProject,
8688
routeThreadRef,
8789
routeThreadKey,
8890
platform,
8991
keybindings,
9092
navigateToThread,
93+
threadSortOrder,
9194
} = input;
92-
const projectExpandedById = useUiStateStore((store) => store.projectExpandedById);
95+
const projectExpandedStates = useUiStateStore(
96+
useShallow((store) =>
97+
sortedProjectKeys.map((projectKey) => store.projectExpandedById[projectKey] ?? true),
98+
),
99+
);
93100
const { showThreadJumpHints, updateThreadJumpHintsVisibility } = useThreadJumpHintVisibility();
94-
const visibleSidebarThreadKeys = useMemo(
95-
() =>
96-
sortedProjectKeys.flatMap((projectKey) => {
97-
const projectThreadKeys = sortedThreadKeysByLogicalProject.get(projectKey) ?? [];
98-
const projectExpanded = projectExpandedById[projectKey] ?? true;
99-
const activeThreadKey = routeThreadKey ?? undefined;
100-
const pinnedCollapsedThread =
101-
!projectExpanded && activeThreadKey
102-
? (projectThreadKeys.find((threadKey) => threadKey === activeThreadKey) ?? null)
103-
: null;
104-
const shouldShowThreadPanel = projectExpanded || pinnedCollapsedThread !== null;
105-
if (!shouldShowThreadPanel) {
106-
return [];
107-
}
108-
const isThreadListExpanded = expandedThreadListsByProject.has(projectKey);
109-
const hasOverflowingThreads = projectThreadKeys.length > THREAD_PREVIEW_LIMIT;
110-
const previewThreads =
111-
isThreadListExpanded || !hasOverflowingThreads
112-
? projectThreadKeys
113-
: projectThreadKeys.slice(0, THREAD_PREVIEW_LIMIT);
114-
return pinnedCollapsedThread ? [pinnedCollapsedThread] : previewThreads;
115-
}),
116-
[
117-
expandedThreadListsByProject,
118-
projectExpandedById,
119-
routeThreadKey,
120-
sortedProjectKeys,
121-
sortedThreadKeysByLogicalProject,
122-
],
101+
const visibleSidebarThreadKeys = useStore(
102+
useMemo(
103+
() =>
104+
createSidebarVisibleThreadKeysSelector({
105+
expandedThreadListsByProject,
106+
physicalToLogicalKey,
107+
projectExpandedStates,
108+
routeThreadKey,
109+
sortedProjectKeys,
110+
threadSortOrder,
111+
}),
112+
[
113+
expandedThreadListsByProject,
114+
physicalToLogicalKey,
115+
projectExpandedStates,
116+
routeThreadKey,
117+
sortedProjectKeys,
118+
threadSortOrder,
119+
],
120+
),
123121
);
124122
const threadJumpCommandByKey = useMemo(() => {
125123
const mapping = new Map<string, NonNullable<ReturnType<typeof threadJumpCommandForIndex>>>();
@@ -333,36 +331,53 @@ function useSidebarKeyboardController(input: {
333331
return threadJumpLabelByKey;
334332
}
335333

334+
export const SidebarSelectionController = memo(function SidebarSelectionController() {
335+
const selectedThreadCount = useThreadSelectionStore((state) => state.selectedThreadKeys.size);
336+
const clearSelection = useThreadSelectionStore((state) => state.clearSelection);
337+
const selectedThreadCountRef = useRef(selectedThreadCount);
338+
selectedThreadCountRef.current = selectedThreadCount;
339+
const clearSelectionRef = useRef(clearSelection);
340+
clearSelectionRef.current = clearSelection;
341+
342+
useEffect(() => {
343+
const onMouseDown = (event: globalThis.MouseEvent) => {
344+
if (selectedThreadCountRef.current === 0) {
345+
return;
346+
}
347+
const target = event.target instanceof HTMLElement ? event.target : null;
348+
if (!shouldClearThreadSelectionOnMouseDown(target)) {
349+
return;
350+
}
351+
clearSelectionRef.current();
352+
};
353+
354+
window.addEventListener("mousedown", onMouseDown);
355+
return () => {
356+
window.removeEventListener("mousedown", onMouseDown);
357+
};
358+
}, []);
359+
360+
return null;
361+
});
362+
336363
export const SidebarProjectOrderingController = memo(
337364
function SidebarProjectOrderingController(props: {
338365
sidebarProjects: readonly SidebarProjectSnapshot[];
339366
physicalToLogicalKey: ReadonlyMap<string, LogicalProjectKey>;
340367
sidebarProjectSortOrder: SidebarProjectSortOrder;
341368
}) {
342369
const { sidebarProjects, physicalToLogicalKey, sidebarProjectSortOrder } = props;
343-
const orderingThreads = useStore(
370+
const sortedProjectKeys = useStore(
344371
useMemo(
345372
() =>
346-
createSidebarProjectOrderingThreadSnapshotsSelector({
373+
createSidebarSortedProjectKeysSelector({
347374
physicalToLogicalKey,
375+
projects: sidebarProjects,
348376
sortOrder: sidebarProjectSortOrder,
349377
}),
350-
[physicalToLogicalKey, sidebarProjectSortOrder],
378+
[physicalToLogicalKey, sidebarProjectSortOrder, sidebarProjects],
351379
),
352380
);
353-
const sortedProjectKeys = useMemo(() => {
354-
if (sidebarProjectSortOrder === "manual") {
355-
return sidebarProjects.map((project) => project.projectKey);
356-
}
357-
358-
const sortableProjects = sidebarProjects.map((project) => ({
359-
...project,
360-
id: project.projectKey,
361-
}));
362-
return sortProjectsForSidebar(sortableProjects, orderingThreads, sidebarProjectSortOrder).map(
363-
(project) => project.id,
364-
);
365-
}, [orderingThreads, sidebarProjectSortOrder, sidebarProjects]);
366381

367382
useEffect(() => {
368383
setSidebarProjectOrdering(sortedProjectKeys);
@@ -380,16 +395,6 @@ export const SidebarKeyboardController = memo(function SidebarKeyboardController
380395
const { navigateToThread, physicalToLogicalKey, sidebarThreadSortOrder } = props;
381396
const sortedProjectKeys = useSidebarProjectKeys();
382397
const expandedThreadListsByProject = useSidebarExpandedThreadListsByProject();
383-
const sortedThreadKeysByLogicalProject = useStore(
384-
useMemo(
385-
() =>
386-
createSidebarSortedThreadKeysByLogicalProjectSelector({
387-
physicalToLogicalKey,
388-
threadSortOrder: sidebarThreadSortOrder,
389-
}),
390-
[physicalToLogicalKey, sidebarThreadSortOrder],
391-
),
392-
);
393398
const routeThreadRef = useParams({
394399
strict: false,
395400
select: (params) => resolveThreadRouteRef(params),
@@ -404,14 +409,15 @@ export const SidebarKeyboardController = memo(function SidebarKeyboardController
404409
const keybindings = useServerKeybindings();
405410
const platform = navigator.platform;
406411
const threadJumpLabelByKey = useSidebarKeyboardController({
412+
physicalToLogicalKey,
407413
sortedProjectKeys,
408-
sortedThreadKeysByLogicalProject,
409414
expandedThreadListsByProject,
410415
routeThreadRef,
411416
routeThreadKey,
412417
platform,
413418
keybindings,
414419
navigateToThread,
420+
threadSortOrder: sidebarThreadSortOrder,
415421
});
416422

417423
useEffect(() => {

0 commit comments

Comments
 (0)