feat(bookmarks): show who wrote each saved message
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>
This commit is contained in:
@@ -733,6 +733,7 @@ Redacted events display "This message has been deleted" along with the redaction
|
||||
- Maximum of 500 bookmarked entries
|
||||
- `BookmarksPanel.tsx` is a sidebar panel accessible from the navigation rail
|
||||
- Live-renders edits/redactions, text search, jump-to-message, and remove
|
||||
- **Author attribution**: each saved-message card shows who wrote it (`{sender} · {time ago}`). The author is snapshotted at save time (`senderId`/`senderName` on the bookmark, optional for backward compatibility) and re-resolved live from the event when the room is joined; search also matches the author name.
|
||||
- **Sort & group**: a Newest / Oldest / By-room segmented control sorts the list; "By room" renders collapsible per-room sections (groups ordered by most-recent save). The chosen sort persists across panel opens (`cinny_bookmarks_sort_v1`). Ordering/grouping logic is pure and unit-tested in `src/app/utils/bookmarks.ts` (`bookmarks.test.ts`).
|
||||
- Hook: `src/app/hooks/useBookmarks.ts`
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ import { MessageDeletedContent } from '../../components/message/content/Fallback
|
||||
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||
import { RoomAvatar } from '../../components/room-avatar';
|
||||
import { getRoomAvatarUrl } from '../../utils/room';
|
||||
import { getRoomAvatarUrl, getMemberName } from '../../utils/room';
|
||||
import { nameInitials } from '../../utils/common';
|
||||
import { ContainerColor } from '../../styles/ContainerColor.css';
|
||||
import { stopPropagation } from '../../utils/keyboard';
|
||||
@@ -102,9 +102,11 @@ type BookmarkItemProps = {
|
||||
onRemove: (eventId: string) => void;
|
||||
// Optional live-rendered preview node; falls back to the stored snapshot when absent.
|
||||
preview?: ReactNode;
|
||||
// Live-resolved author name; falls back to the stored snapshot when absent.
|
||||
senderName?: string;
|
||||
};
|
||||
|
||||
function BookmarkItem({ bookmark, onJump, onRemove, preview }: BookmarkItemProps) {
|
||||
function BookmarkItem({ bookmark, onJump, onRemove, preview, senderName }: BookmarkItemProps) {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const room = mx.getRoom(bookmark.roomId) ?? undefined;
|
||||
@@ -112,6 +114,9 @@ function BookmarkItem({ bookmark, onJump, onRemove, preview }: BookmarkItemProps
|
||||
const avatarUrl = room
|
||||
? (getRoomAvatarUrl(mx, room, 96, useAuthentication) ?? undefined)
|
||||
: undefined;
|
||||
// Prefer a live-resolved author name, then the stored snapshot.
|
||||
const author = senderName ?? bookmark.senderName;
|
||||
const timeAgo = formatTimeAgo(bookmark.savedAt);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -137,8 +142,8 @@ function BookmarkItem({ bookmark, onJump, onRemove, preview }: BookmarkItemProps
|
||||
<Text size="T200" truncate style={{ fontWeight: config.fontWeight.W600 }}>
|
||||
{displayRoomName}
|
||||
</Text>
|
||||
<Text size="T200" priority="300">
|
||||
{formatTimeAgo(bookmark.savedAt)}
|
||||
<Text size="T200" priority="300" truncate>
|
||||
{author ? `${author} · ${timeAgo}` : timeAgo}
|
||||
</Text>
|
||||
</Box>
|
||||
<IconButton
|
||||
@@ -199,7 +204,19 @@ function LiveBookmarkItem({ room, bookmark, onJump, onRemove }: LiveBookmarkItem
|
||||
}
|
||||
}
|
||||
|
||||
return <BookmarkItem bookmark={bookmark} onJump={onJump} onRemove={onRemove} preview={preview} />;
|
||||
// Resolve the author's current display name from the live event when available.
|
||||
const liveSender = liveEvent?.getSender();
|
||||
const senderName = liveSender ? getMemberName(room, liveSender) : undefined;
|
||||
|
||||
return (
|
||||
<BookmarkItem
|
||||
bookmark={bookmark}
|
||||
onJump={onJump}
|
||||
onRemove={onRemove}
|
||||
preview={preview}
|
||||
senderName={senderName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
type RoomGroupHeaderProps = {
|
||||
@@ -320,7 +337,8 @@ export function BookmarksPanel({ onClose }: BookmarksPanelProps) {
|
||||
: bookmarks.filter(
|
||||
(bk) =>
|
||||
bk.previewText.toLowerCase().includes(query) ||
|
||||
bk.roomName.toLowerCase().includes(query),
|
||||
bk.roomName.toLowerCase().includes(query) ||
|
||||
(bk.senderName?.toLowerCase().includes(query) ?? false),
|
||||
),
|
||||
[bookmarks, query],
|
||||
);
|
||||
|
||||
@@ -1190,6 +1190,8 @@ export const Message = React.memo(
|
||||
savedAt: Date.now(),
|
||||
previewText: body.slice(0, 120),
|
||||
roomName: room.name,
|
||||
senderId,
|
||||
senderName: senderDisplayName,
|
||||
});
|
||||
}
|
||||
closeMenu();
|
||||
|
||||
@@ -8,6 +8,11 @@ export type Bookmark = {
|
||||
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';
|
||||
|
||||
Reference in New Issue
Block a user