diff --git a/desktop/src/features/messages/ui/MessageRow.tsx b/desktop/src/features/messages/ui/MessageRow.tsx
index afcb3e863c..fcb7983bdc 100644
--- a/desktop/src/features/messages/ui/MessageRow.tsx
+++ b/desktop/src/features/messages/ui/MessageRow.tsx
@@ -39,6 +39,10 @@ import { resolveSnapshotSharedBy } from "@/features/messages/lib/snapshotSharedB
import { resolveMentionProps } from "@/shared/lib/resolveMentionNames";
import { Markdown } from "@/shared/ui/markdown";
import type { VideoReviewContext } from "@/shared/ui/VideoPlayer";
+import {
+ messageBookmarkTarget,
+ useBookmarkActions,
+} from "@/features/bookmarks/lib/BookmarksContext";
import { MessageActionBar } from "./MessageActionBar";
import { MessageAgentOwner } from "./MessageAgentOwner";
import { MessageAuthorText, MessageHeaderRow } from "./MessageHeader";
@@ -179,6 +183,14 @@ export const MessageRow = React.memo(
},
[channelId, openReminder],
);
+ const bookmarks = useBookmarkActions();
+ const handleBookmark = React.useCallback(
+ (msg: TimelineMessage) => {
+ if (!channelId) return;
+ bookmarks.toggleBookmark(messageBookmarkTarget(msg, channelId));
+ },
+ [bookmarks, channelId],
+ );
const { mentionNames, mentionPubkeysByName } = React.useMemo(
() => resolveMentionProps(message.tags, profiles),
[profiles, message.tags],
@@ -467,6 +479,9 @@ export const MessageRow = React.memo(
/>
) : null;
+ const canBookmark =
+ bookmarks.enabled && Boolean(channelId) && !message.pending;
+
const actionBarNode = (
;
unreadChannelIds: ReadonlySet;
communities: Community[];
@@ -145,6 +146,7 @@ type AppSidebarProps = {
onSelectAgents: () => void;
onSelectProjects: () => void;
onSelectPulse: () => void;
+ onSelectSaved: () => void;
onSelectWorkflows: () => void;
onSelectHome: () => void;
onSelectChannel: (channelId: string) => void;
@@ -214,6 +216,7 @@ export function AppSidebar({
onSelectAgents,
onSelectProjects,
onSelectPulse,
+ onSelectSaved,
onSelectWorkflows,
onSelectHome,
onSelectChannel,
@@ -612,6 +615,7 @@ export function AppSidebar({
onSelectHome={onSelectHome}
onSelectProjects={onSelectProjects}
onSelectPulse={onSelectPulse}
+ onSelectSaved={onSelectSaved}
onSelectWorkflows={onSelectWorkflows}
selectedView={selectedView}
/>
diff --git a/desktop/src/features/sidebar/ui/AppSidebarPinnedHeader.tsx b/desktop/src/features/sidebar/ui/AppSidebarPinnedHeader.tsx
index a673492ef1..135b887dee 100644
--- a/desktop/src/features/sidebar/ui/AppSidebarPinnedHeader.tsx
+++ b/desktop/src/features/sidebar/ui/AppSidebarPinnedHeader.tsx
@@ -1,4 +1,4 @@
-import { Activity, Bot, FolderGit2, Inbox, Zap } from "lucide-react";
+import { Activity, Bookmark, Bot, FolderGit2, Inbox, Zap } from "lucide-react";
import { TopbarSearch } from "@/features/search/ui/TopbarSearch";
import { FeatureGate } from "@/shared/features";
@@ -19,7 +19,8 @@ type SidebarSelectedView =
| "agents"
| "workflows"
| "pulse"
- | "projects";
+ | "projects"
+ | "saved";
type AppSidebarPinnedHeaderProps = {
channelLabels: Record;
@@ -41,6 +42,7 @@ type AppSidebarPrimaryMenuProps = {
onSelectHome: () => void;
onSelectProjects: () => void;
onSelectPulse: () => void;
+ onSelectSaved: () => void;
onSelectWorkflows: () => void;
selectedView: SidebarSelectedView;
};
@@ -86,6 +88,7 @@ export function AppSidebarPrimaryMenu({
onSelectHome,
onSelectProjects,
onSelectPulse,
+ onSelectSaved,
onSelectWorkflows,
selectedView,
}: AppSidebarPrimaryMenuProps) {
@@ -115,6 +118,18 @@ export function AppSidebarPrimaryMenu({
) : null}
+
+
+
+ Saved
+
+
", "channel-sections", "channel-mutes", "channel-stars", "channel-sort").
+// differentiates them by d-tag ("read-state:", "channel-sections", "channel-mutes", "channel-stars", "channel-sort", "bookmarks").
export const KIND_READ_STATE = 30078;
export const KIND_CHANNEL_SECTIONS = 30078;
export const KIND_CHANNEL_MUTES = 30078;
export const KIND_CHANNEL_STARS = 30078;
export const KIND_CHANNEL_SORT = 30078;
+// Private, per-user "save for later" list (d-tag "bookmarks"), encrypted to self.
+export const KIND_BOOKMARKS = 30078;
// NIP-33 persona/team/managed-agent projection events (d-tag keyed). Published
// backend-side as secrets-stripped snapshots; the inbound sync hook subscribes
// to all three to patch local records. Mirror of buzz-core's KIND_PERSONA etc.