|
3 | 3 | * License: MS-RSL – see LICENSE.md for details |
4 | 4 | */ |
5 | 5 |
|
6 | | -import { merge, sortBy, throttle, uniq, xor } from "lodash"; |
| 6 | +/** |
| 7 | +// To debug starred files in the browser console: |
| 8 | +c = cc.client.conat_client |
| 9 | +bm = await c.dkv({account_id: cc.client.account_id, name: 'bookmark-starred-files'}) |
| 10 | +// Check all bookmark data |
| 11 | +console.log('All bookmarks:', bm.getAll()) |
| 12 | +// Check specific project bookmarks |
| 13 | +console.log('Project bookmarks (get):', bm.get("[project_id]")) |
| 14 | +// Set starred files for a project |
| 15 | +bm.set(project_id, ['file1.txt', 'folder/file2.md']) |
| 16 | +// Listen to changes |
| 17 | +bm.on('change', (e) => console.log('Bookmark change:', e)) |
| 18 | + */ |
| 19 | + |
| 20 | +import { sortBy, uniq } from "lodash"; |
7 | 21 | import { useState } from "react"; |
8 | 22 | import useAsyncEffect from "use-async-effect"; |
9 | 23 |
|
10 | | -import api from "@cocalc/frontend/client/api"; |
11 | | -import { STARRED_FILES } from "@cocalc/util/consts/bookmarks"; |
12 | | -import { |
13 | | - GetStarredBookmarks, |
14 | | - GetStarredBookmarksPayload, |
15 | | - SetStarredBookmarks, |
16 | | -} from "@cocalc/util/types/bookmarks"; |
17 | | -import { |
18 | | - FlyoutActiveStarred, |
19 | | - getFlyoutActiveStarred, |
20 | | - storeFlyoutState, |
21 | | -} from "./state"; |
22 | | - |
23 | | -// Additionally to local storage, we back the state of the starred files in the database. |
24 | | -// Errors with the API are ignored, because we primarily rely on local storage. |
25 | | -// The only really important situation to think of are when there is nothing in local storage but in the database, |
26 | | -// or when there is |
| 24 | +import { redux } from "@cocalc/frontend/app-framework"; |
| 25 | +import { webapp_client } from "@cocalc/frontend/webapp-client"; |
| 26 | +import { CONAT_BOOKMARKS_KEY } from "@cocalc/util/consts/bookmarks"; |
| 27 | +import type { FlyoutActiveStarred } from "./state"; |
| 28 | + |
| 29 | +// Starred files are now managed entirely through conat with in-memory state. |
| 30 | +// No local storage dependency - conat handles synchronization and persistence. |
27 | 31 | export function useStarredFilesManager(project_id: string) { |
28 | | - const [starred, setStarred] = useState<FlyoutActiveStarred>( |
29 | | - getFlyoutActiveStarred(project_id), |
30 | | - ); |
| 32 | + const [starred, setStarred] = useState<FlyoutActiveStarred>([]); |
| 33 | + const [bookmarks, setBookmarks] = useState<any>(null); |
| 34 | + const [isInitialized, setIsInitialized] = useState(false); |
31 | 35 |
|
32 | | - // once after mounting this, we update the starred bookmarks (which merges with what we have) and then stores it |
| 36 | + // Initialize conat bookmarks once on mount, waiting for authentication |
33 | 37 | useAsyncEffect(async () => { |
34 | | - await updateStarred(); |
| 38 | + // Wait until account is authenticated |
| 39 | + const store = redux.getStore("account"); |
| 40 | + await store.async_wait({ |
| 41 | + until: () => store.get_account_id() != null, |
| 42 | + timeout: 0, // indefinite timeout |
| 43 | + }); |
| 44 | + |
| 45 | + const account_id = store.get_account_id(); |
| 46 | + await initializeConatBookmarks(account_id); |
35 | 47 | }, []); |
36 | 48 |
|
37 | | - function setStarredLS(starred: string[]) { |
38 | | - setStarred(starred); |
39 | | - storeFlyoutState(project_id, "active", { starred: starred }); |
| 49 | + async function initializeConatBookmarks(account_id: string) { |
| 50 | + try { |
| 51 | + const conatBookmarks = await webapp_client.conat_client.dkv<string[]>({ |
| 52 | + account_id, |
| 53 | + name: CONAT_BOOKMARKS_KEY, |
| 54 | + }); |
| 55 | + |
| 56 | + setBookmarks(conatBookmarks); |
| 57 | + |
| 58 | + // Listen for changes from other clients |
| 59 | + conatBookmarks.on( |
| 60 | + "change", |
| 61 | + (changeEvent: { key: string; value?: string[]; prev?: string[] }) => { |
| 62 | + if (changeEvent.key === project_id) { |
| 63 | + const remoteStars = changeEvent.value || []; |
| 64 | + setStarred(sortBy(uniq(remoteStars))); |
| 65 | + } |
| 66 | + }, |
| 67 | + ); |
| 68 | + |
| 69 | + // Load initial data from conat |
| 70 | + const initialStars = conatBookmarks.get(project_id) || []; |
| 71 | + if (Array.isArray(initialStars)) { |
| 72 | + setStarred(sortBy(uniq(initialStars))); |
| 73 | + } |
| 74 | + |
| 75 | + setIsInitialized(true); |
| 76 | + } catch (err) { |
| 77 | + console.warn(`conat bookmark initialization warning -- ${err}`); |
| 78 | + setIsInitialized(true); // Set initialized even on error to avoid infinite loading |
| 79 | + } |
40 | 80 | } |
41 | 81 |
|
42 | | - // TODO: there are also add/remove API endpoints, but for now we stick with set. Hardly worth optimizing. |
43 | 82 | function setStarredPath(path: string, starState: boolean) { |
| 83 | + if (!bookmarks || !isInitialized) { |
| 84 | + console.warn("Conat bookmarks not yet initialized"); |
| 85 | + return; |
| 86 | + } |
| 87 | + |
44 | 88 | const next = starState |
45 | | - ? [...starred, path] |
| 89 | + ? sortBy(uniq([...starred, path])) |
46 | 90 | : starred.filter((p) => p !== path); |
47 | | - setStarredLS(next); |
48 | | - storeStarred(next); |
49 | | - } |
50 | 91 |
|
51 | | - async function storeStarred(stars: string[]) { |
| 92 | + // Update local state immediately for responsive UI |
| 93 | + setStarred(next); |
| 94 | + |
| 95 | + // Store to conat (this will also trigger the change event for other clients) |
52 | 96 | try { |
53 | | - const payload: SetStarredBookmarks = { |
54 | | - type: STARRED_FILES, |
55 | | - project_id, |
56 | | - stars, |
57 | | - }; |
58 | | - await api("bookmarks/set", payload); |
| 97 | + bookmarks.set(project_id, next); |
59 | 98 | } catch (err) { |
60 | | - console.warn(`bookmark: warning -- ${err}`); |
| 99 | + console.warn(`conat bookmark storage warning -- ${err}`); |
| 100 | + // Revert local state on error |
| 101 | + setStarred(starred); |
61 | 102 | } |
62 | 103 | } |
63 | 104 |
|
64 | | - // this is called once, when the flyout/tabs component is mounted |
65 | | - // throtteled, to usually take 1 sec from opening the panel to loading the stars |
66 | | - const updateStarred = throttle( |
67 | | - async () => { |
68 | | - try { |
69 | | - const payload: GetStarredBookmarksPayload = { |
70 | | - type: STARRED_FILES, |
71 | | - project_id, |
72 | | - }; |
73 | | - const data: GetStarredBookmarks = await api("bookmarks/get", payload); |
74 | | - |
75 | | - const { type, status } = data; |
76 | | - |
77 | | - if (type !== STARRED_FILES) { |
78 | | - console.error( |
79 | | - `flyout/store/starred type must be ${STARRED_FILES} but we got`, |
80 | | - type, |
81 | | - ); |
82 | | - return; |
83 | | - } |
84 | | - |
85 | | - if (status === "success") { |
86 | | - const { stars } = data; |
87 | | - if ( |
88 | | - Array.isArray(stars) && |
89 | | - stars.every((x) => typeof x === "string") |
90 | | - ) { |
91 | | - stars.sort(); // sorted for the xor check below |
92 | | - const next = sortBy(uniq(merge(starred, stars))); |
93 | | - setStarredLS(next); |
94 | | - if (xor(stars, next).length > 0) { |
95 | | - // if there is a change (e.g. nothing in the database stored yet), store the stars |
96 | | - await storeStarred(next); |
97 | | - } |
98 | | - } else { |
99 | | - console.error("flyout/store/starred invalid payload", stars); |
100 | | - } |
101 | | - } else if (status === "error") { |
102 | | - const { error } = data; |
103 | | - console.error("flyout/store/starred error", error); |
104 | | - } else { |
105 | | - console.error("flyout/store/starred error: unknown status", status); |
106 | | - } |
107 | | - } catch (err) { |
108 | | - console.warn(`bookmark: warning -- ${err}`); |
109 | | - } |
110 | | - }, |
111 | | - 1000, |
112 | | - { trailing: true, leading: false }, |
113 | | - ); |
114 | | - |
115 | 105 | return { |
116 | 106 | starred, |
117 | 107 | setStarredPath, |
|
0 commit comments