Saved-message cards showed the room and time but not the author, so in a busy
room you couldn't tell who said it without jumping. Now each card shows
"{sender} - {time ago}":
- Bookmark gains optional senderId/senderName (snapshotted at save time in
Message.tsx from the already-computed sender display name); optional so
existing stored bookmarks stay valid.
- The panel re-resolves the author's current display name live from the event
when the room is joined, falling back to the stored snapshot for left rooms.
- Search now also matches the author name.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
70 lines
2.2 KiB
TypeScript
70 lines
2.2 KiB
TypeScript
import { useCallback } from 'react';
|
|
import { useMatrixClient } from './useMatrixClient';
|
|
import { createAccountDataListStore } from './createAccountDataListStore';
|
|
|
|
export type Bookmark = {
|
|
roomId: string;
|
|
eventId: string;
|
|
savedAt: number;
|
|
previewText: string;
|
|
roomName: string;
|
|
// Author of the saved message. Optional for backward compatibility with
|
|
// bookmarks stored before attribution was added; the panel resolves a live
|
|
// name when the room is joined and falls back to these snapshots otherwise.
|
|
senderId?: string;
|
|
senderName?: string;
|
|
};
|
|
|
|
const BOOKMARKS_KEY = 'io.lotus.bookmarks';
|
|
const MAX_BOOKMARKS = 500;
|
|
|
|
type BookmarksContent = {
|
|
bookmarks: Bookmark[];
|
|
};
|
|
|
|
// 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 }),
|
|
});
|
|
|
|
export function useBookmarks(): {
|
|
bookmarks: Bookmark[];
|
|
addBookmark: (b: Bookmark) => Promise<void>;
|
|
removeBookmark: (eventId: string) => Promise<void>;
|
|
isBookmarked: (eventId: string) => boolean;
|
|
} {
|
|
const mx = useMatrixClient();
|
|
const bookmarks = bookmarksStore.useValue(mx);
|
|
|
|
const addBookmark = useCallback(
|
|
(b: Bookmark) =>
|
|
bookmarksStore.enqueueWrite(mx, (current) => {
|
|
// 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;
|
|
}),
|
|
[mx],
|
|
);
|
|
|
|
const removeBookmark = useCallback(
|
|
(eventId: string) =>
|
|
bookmarksStore.enqueueWrite(mx, (current) => current.filter((bk) => bk.eventId !== eventId)),
|
|
[mx],
|
|
);
|
|
|
|
const isBookmarked = useCallback(
|
|
(eventId: string) => bookmarks.some((bk) => bk.eventId === eventId),
|
|
[bookmarks],
|
|
);
|
|
|
|
return { bookmarks, addBookmark, removeBookmark, isBookmarked };
|
|
}
|