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
|
- Maximum of 500 bookmarked entries
|
||||||
- `BookmarksPanel.tsx` is a sidebar panel accessible from the navigation rail
|
- `BookmarksPanel.tsx` is a sidebar panel accessible from the navigation rail
|
||||||
- Live-renders edits/redactions, text search, jump-to-message, and remove
|
- 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`).
|
- **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`
|
- Hook: `src/app/hooks/useBookmarks.ts`
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import { MessageDeletedContent } from '../../components/message/content/Fallback
|
|||||||
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
|
||||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||||
import { RoomAvatar } from '../../components/room-avatar';
|
import { RoomAvatar } from '../../components/room-avatar';
|
||||||
import { getRoomAvatarUrl } from '../../utils/room';
|
import { getRoomAvatarUrl, getMemberName } from '../../utils/room';
|
||||||
import { nameInitials } from '../../utils/common';
|
import { nameInitials } from '../../utils/common';
|
||||||
import { ContainerColor } from '../../styles/ContainerColor.css';
|
import { ContainerColor } from '../../styles/ContainerColor.css';
|
||||||
import { stopPropagation } from '../../utils/keyboard';
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
@@ -102,9 +102,11 @@ type BookmarkItemProps = {
|
|||||||
onRemove: (eventId: string) => void;
|
onRemove: (eventId: string) => void;
|
||||||
// Optional live-rendered preview node; falls back to the stored snapshot when absent.
|
// Optional live-rendered preview node; falls back to the stored snapshot when absent.
|
||||||
preview?: ReactNode;
|
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 mx = useMatrixClient();
|
||||||
const useAuthentication = useMediaAuthentication();
|
const useAuthentication = useMediaAuthentication();
|
||||||
const room = mx.getRoom(bookmark.roomId) ?? undefined;
|
const room = mx.getRoom(bookmark.roomId) ?? undefined;
|
||||||
@@ -112,6 +114,9 @@ function BookmarkItem({ bookmark, onJump, onRemove, preview }: BookmarkItemProps
|
|||||||
const avatarUrl = room
|
const avatarUrl = room
|
||||||
? (getRoomAvatarUrl(mx, room, 96, useAuthentication) ?? undefined)
|
? (getRoomAvatarUrl(mx, room, 96, useAuthentication) ?? undefined)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
// Prefer a live-resolved author name, then the stored snapshot.
|
||||||
|
const author = senderName ?? bookmark.senderName;
|
||||||
|
const timeAgo = formatTimeAgo(bookmark.savedAt);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -137,8 +142,8 @@ function BookmarkItem({ bookmark, onJump, onRemove, preview }: BookmarkItemProps
|
|||||||
<Text size="T200" truncate style={{ fontWeight: config.fontWeight.W600 }}>
|
<Text size="T200" truncate style={{ fontWeight: config.fontWeight.W600 }}>
|
||||||
{displayRoomName}
|
{displayRoomName}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="T200" priority="300">
|
<Text size="T200" priority="300" truncate>
|
||||||
{formatTimeAgo(bookmark.savedAt)}
|
{author ? `${author} · ${timeAgo}` : timeAgo}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton
|
<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 = {
|
type RoomGroupHeaderProps = {
|
||||||
@@ -320,7 +337,8 @@ export function BookmarksPanel({ onClose }: BookmarksPanelProps) {
|
|||||||
: bookmarks.filter(
|
: bookmarks.filter(
|
||||||
(bk) =>
|
(bk) =>
|
||||||
bk.previewText.toLowerCase().includes(query) ||
|
bk.previewText.toLowerCase().includes(query) ||
|
||||||
bk.roomName.toLowerCase().includes(query),
|
bk.roomName.toLowerCase().includes(query) ||
|
||||||
|
(bk.senderName?.toLowerCase().includes(query) ?? false),
|
||||||
),
|
),
|
||||||
[bookmarks, query],
|
[bookmarks, query],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1190,6 +1190,8 @@ export const Message = React.memo(
|
|||||||
savedAt: Date.now(),
|
savedAt: Date.now(),
|
||||||
previewText: body.slice(0, 120),
|
previewText: body.slice(0, 120),
|
||||||
roomName: room.name,
|
roomName: room.name,
|
||||||
|
senderId,
|
||||||
|
senderName: senderDisplayName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
closeMenu();
|
closeMenu();
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ export type Bookmark = {
|
|||||||
savedAt: number;
|
savedAt: number;
|
||||||
previewText: string;
|
previewText: string;
|
||||||
roomName: 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 BOOKMARKS_KEY = 'io.lotus.bookmarks';
|
||||||
|
|||||||
Reference in New Issue
Block a user