Skip to content
Source
Sign inSign up

threadSelectionStore.ts

110 lines3.4 KB

1import { create } from "zustand";
2​
3export interface ThreadSelectionState {
4 selectedThreadKeys: ReadonlySet<string>;
5 anchorThreadKey: string | null;
6}
7​
8interface ThreadSelectionStore extends ThreadSelectionState {
9 toggleThread: (threadKey: string) => void;
10 /** Requires ordered scoped thread keys from the same project. */
11 rangeSelectTo: (threadKey: string, orderedThreadKeys: readonly string[]) => void;
12 clearSelection: () => void;
13 removeFromSelection: (threadKeys: readonly string[]) => void;
14 setAnchor: (threadKey: string) => void;
15 hasSelection: () => boolean;
17​
18const EMPTY_SET = new Set<string>();
19​
20export function getThreadKeysToDeselectAfterDelete(
21 selectedThreadKeys: readonly string[],
22 deletedThreadKeys: ReadonlySet<string>,
23 hasThread: (threadKey: string) => boolean,
24): string[] {
25 return selectedThreadKeys.filter((key) => deletedThreadKeys.has(key) || !hasThread(key));
27​
28export const useThreadSelectionStore = create<ThreadSelectionStore>((set, get) => ({
29 selectedThreadKeys: EMPTY_SET,
30 anchorThreadKey: null,
31​
32 toggleThread: (threadKey) => {
33 set((state) => {
34 const next = new Set(state.selectedThreadKeys);
35 if (next.has(threadKey)) {
36 next.delete(threadKey);
37 } else {
38 next.add(threadKey);
39 }
40 return {
41 selectedThreadKeys: next,
42 anchorThreadKey: next.has(threadKey) ? threadKey : state.anchorThreadKey,
43 };
44 });
45 },
46​
47 rangeSelectTo: (threadKey, orderedThreadKeys) => {
48 set((state) => {
49 const anchor = state.anchorThreadKey;
50 if (anchor === null) {
51 const next = new Set(state.selectedThreadKeys);
52 next.add(threadKey);
53 return { selectedThreadKeys: next, anchorThreadKey: threadKey };
54 }
55​
56 const anchorIndex = orderedThreadKeys.indexOf(anchor);
57 const targetIndex = orderedThreadKeys.indexOf(threadKey);
58 if (anchorIndex === -1 || targetIndex === -1) {
59 const next = new Set(state.selectedThreadKeys);
60 next.add(threadKey);
61 return { selectedThreadKeys: next, anchorThreadKey: threadKey };
62 }
63​
64 const start = Math.min(anchorIndex, targetIndex);
65 const end = Math.max(anchorIndex, targetIndex);
66 const next = new Set(state.selectedThreadKeys);
67 for (let i = start; i <= end; i++) {
68 const key = orderedThreadKeys[i];
69 if (key !== undefined) {
70 next.add(key);
71 }
72 }
73 return { selectedThreadKeys: next, anchorThreadKey: anchor };
74 });
75 },
76​
77 clearSelection: () => {
78 const state = get();
79 if (state.selectedThreadKeys.size === 0 && state.anchorThreadKey === null) return;
80 set({ selectedThreadKeys: EMPTY_SET, anchorThreadKey: null });
81 },
82​
83 setAnchor: (threadKey) => {
84 if (get().anchorThreadKey === threadKey) return;
85 set({ anchorThreadKey: threadKey });
86 },
87​
88 removeFromSelection: (threadKeys) => {
89 set((state) => {
90 const toRemove = new Set(threadKeys);
91 let changed = false;
92 const next = new Set<string>();
93 for (const key of state.selectedThreadKeys) {
94 if (toRemove.has(key)) {
95 changed = true;
96 } else {
97 next.add(key);
98 }
99 }
100 if (!changed) return state;
101 const newAnchor =
102 state.anchorThreadKey !== null && toRemove.has(state.anchorThreadKey)
103 ? null
104 : state.anchorThreadKey;
105 return { selectedThreadKeys: next, anchorThreadKey: newAnchor };
106 });
107 },
108​
109 hasSelection: () => get().selectedThreadKeys.size > 0,
110}));