2026-07-07 23:24:08 -04:00
|
|
|
import { useCallback } from 'react';
|
2026-06-04 10:26:08 -04:00
|
|
|
import { useMatrixClient } from './useMatrixClient';
|
2026-07-07 23:24:08 -04:00
|
|
|
import { createAccountDataListStore } from './createAccountDataListStore';
|
2026-06-04 10:26:08 -04:00
|
|
|
|
|
|
|
|
export type Bookmark = {
|
|
|
|
|
roomId: string;
|
|
|
|
|
eventId: string;
|
|
|
|
|
savedAt: number;
|
|
|
|
|
previewText: string;
|
|
|
|
|
roomName: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const BOOKMARKS_KEY = 'io.lotus.bookmarks';
|
|
|
|
|
const MAX_BOOKMARKS = 500;
|
|
|
|
|
|
|
|
|
|
type BookmarksContent = {
|
|
|
|
|
bookmarks: Bookmark[];
|
|
|
|
|
};
|
|
|
|
|
|
2026-07-07 23:24:08 -04:00
|
|
|
// Shared, concurrency-safe store. See createAccountDataListStore for why the
|
|
|
|
|
// snapshot + write queue must be module-scoped (writes are serialized to avoid
|
|
|
|
|
// lost updates, since setAccountData replaces the whole content with no merge).
|
|
|
|
|
const bookmarksStore = createAccountDataListStore<Bookmark[], BookmarksContent>({
|
|
|
|
|
eventType: BOOKMARKS_KEY,
|
|
|
|
|
read: (content) => content?.bookmarks ?? [],
|
|
|
|
|
write: (bookmarks) => ({ bookmarks }),
|
|
|
|
|
});
|
2026-07-02 20:56:27 -04:00
|
|
|
|
2026-06-04 10:26:08 -04:00
|
|
|
export function useBookmarks(): {
|
|
|
|
|
bookmarks: Bookmark[];
|
|
|
|
|
addBookmark: (b: Bookmark) => Promise<void>;
|
|
|
|
|
removeBookmark: (eventId: string) => Promise<void>;
|
|
|
|
|
isBookmarked: (eventId: string) => boolean;
|
|
|
|
|
} {
|
|
|
|
|
const mx = useMatrixClient();
|
2026-07-07 23:24:08 -04:00
|
|
|
const bookmarks = bookmarksStore.useValue(mx);
|
2026-06-04 10:26:08 -04:00
|
|
|
|
|
|
|
|
const addBookmark = useCallback(
|
2026-07-02 20:56:27 -04:00
|
|
|
(b: Bookmark) =>
|
2026-07-07 23:24:08 -04:00
|
|
|
bookmarksStore.enqueueWrite(mx, (current) => {
|
2026-07-02 20:56:27 -04:00
|
|
|
// Avoid duplicates
|
|
|
|
|
const filtered = current.filter((bk) => bk.eventId !== b.eventId);
|
|
|
|
|
let next = [b, ...filtered];
|
|
|
|
|
if (next.length > MAX_BOOKMARKS) {
|
|
|
|
|
next = next.slice(0, MAX_BOOKMARKS);
|
|
|
|
|
}
|
|
|
|
|
return next;
|
|
|
|
|
}),
|
2026-06-04 10:26:08 -04:00
|
|
|
[mx],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const removeBookmark = useCallback(
|
2026-07-02 20:56:27 -04:00
|
|
|
(eventId: string) =>
|
2026-07-07 23:24:08 -04:00
|
|
|
bookmarksStore.enqueueWrite(mx, (current) => current.filter((bk) => bk.eventId !== eventId)),
|
2026-06-04 10:26:08 -04:00
|
|
|
[mx],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const isBookmarked = useCallback(
|
|
|
|
|
(eventId: string) => bookmarks.some((bk) => bk.eventId === eventId),
|
|
|
|
|
[bookmarks],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return { bookmarks, addBookmark, removeBookmark, isBookmarked };
|
|
|
|
|
}
|