|
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