feat(bookmarks): sort & group saved messages by room
The Saved Messages panel showed bookmarks in one fixed order (newest save first) with no way to reorganize. Add a Newest / Oldest / By-room segmented sort control to the panel toolbar. In "By room" mode the list renders collapsible per-room sections, with groups ordered by their most recently saved message so active rooms float to the top. The chosen sort persists across panel opens via a localStorage-backed atom. Ordering and grouping are pure functions in utils/bookmarks.ts (sortBookmarks, groupBookmarksByRoom) with deterministic eventId tie-breaks, covered by bookmarks.test.ts (9 tests). No change to the bookmark data model, account-data schema, useBookmarks, or how bookmarks are created; search still feeds the sorter/grouper unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import { Bookmark } from '../hooks/useBookmarks';
|
||||
|
||||
export type BookmarkSort = 'newest' | 'oldest' | 'room';
|
||||
|
||||
export type BookmarkRoomGroup = {
|
||||
roomId: string;
|
||||
roomName: string;
|
||||
items: Bookmark[];
|
||||
};
|
||||
|
||||
// Deterministic newest-first comparator: most recently saved first, ties broken
|
||||
// by eventId so the order is stable regardless of input order (keeps tests and
|
||||
// re-renders from shuffling equal-timestamp entries).
|
||||
const byNewest = (a: Bookmark, b: Bookmark): number =>
|
||||
b.savedAt - a.savedAt || (a.eventId < b.eventId ? -1 : a.eventId > b.eventId ? 1 : 0);
|
||||
|
||||
/**
|
||||
* Return a new array of bookmarks ordered per the selected sort. `room` has no
|
||||
* flat ordering of its own (grouping is a render concern) and falls back to
|
||||
* newest-first so callers can still render a sensible flat list if they want.
|
||||
*/
|
||||
export function sortBookmarks(bookmarks: Bookmark[], sort: BookmarkSort): Bookmark[] {
|
||||
const copy = [...bookmarks];
|
||||
if (sort === 'oldest') {
|
||||
// Oldest-first is the reverse ordering; keep the same eventId tie-break shape.
|
||||
return copy.sort(
|
||||
(a, b) => a.savedAt - b.savedAt || (a.eventId < b.eventId ? -1 : a.eventId > b.eventId ? 1 : 0),
|
||||
);
|
||||
}
|
||||
return copy.sort(byNewest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bucket bookmarks by room. Items within a group are newest-first; groups are
|
||||
* ordered by each group's most-recently-saved bookmark (active rooms float up),
|
||||
* with the room's stored name (the panel overrides with the live name when the
|
||||
* room is joined). Deterministic tie-breaks keep the output stable.
|
||||
*/
|
||||
export function groupBookmarksByRoom(bookmarks: Bookmark[]): BookmarkRoomGroup[] {
|
||||
const groups = new Map<string, BookmarkRoomGroup>();
|
||||
bookmarks.forEach((bk) => {
|
||||
const existing = groups.get(bk.roomId);
|
||||
if (existing) {
|
||||
existing.items.push(bk);
|
||||
} else {
|
||||
groups.set(bk.roomId, { roomId: bk.roomId, roomName: bk.roomName, items: [bk] });
|
||||
}
|
||||
});
|
||||
|
||||
const result = [...groups.values()];
|
||||
result.forEach((group) => group.items.sort(byNewest));
|
||||
// Order groups by their newest item's savedAt (desc); tie-break by roomId.
|
||||
result.sort((a, b) => {
|
||||
const diff = b.items[0].savedAt - a.items[0].savedAt;
|
||||
if (diff !== 0) return diff;
|
||||
return a.roomId < b.roomId ? -1 : a.roomId > b.roomId ? 1 : 0;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user