From 50ccef6980bc22ee2ca223bc92864e5036266003 Mon Sep 17 00:00:00 2001 From: Thomas Petersen Date: Wed, 13 May 2026 17:32:38 -0400 Subject: [PATCH] fix(desktop): add inbox reactions and clean header Co-authored-by: Cursor --- desktop/src/features/home/lib/inbox.ts | 2 + desktop/src/features/home/ui/HomeView.tsx | 95 +++++++++++++++---- .../src/features/home/ui/InboxDetailPane.tsx | 39 +++++++- .../src/features/home/ui/InboxListPane.tsx | 2 +- 4 files changed, 114 insertions(+), 24 deletions(-) diff --git a/desktop/src/features/home/lib/inbox.ts b/desktop/src/features/home/lib/inbox.ts index 6130c5ed28..0d6e7a99a9 100644 --- a/desktop/src/features/home/lib/inbox.ts +++ b/desktop/src/features/home/lib/inbox.ts @@ -3,6 +3,7 @@ import { type UserProfileLookup, } from "@/features/profile/lib/identity"; import { getThreadReference } from "@/features/messages/lib/threading"; +import type { TimelineReaction } from "@/features/messages/types"; import type { FeedItem, FeedItemCategory, @@ -43,6 +44,7 @@ export type InboxReply = { fullTimestampLabel: string; id: string; parentId?: string | null; + reactions?: TimelineReaction[]; rootId?: string | null; }; diff --git a/desktop/src/features/home/ui/HomeView.tsx b/desktop/src/features/home/ui/HomeView.tsx index 1aa58c4e54..f6de6dfa2a 100644 --- a/desktop/src/features/home/ui/HomeView.tsx +++ b/desktop/src/features/home/ui/HomeView.tsx @@ -13,12 +13,17 @@ import { useFeedItemState } from "@/features/home/useFeedItemState"; import { useInboxThreadContext } from "@/features/home/useInboxThreadContext"; import { InboxDetailPane } from "@/features/home/ui/InboxDetailPane"; import { InboxListPane } from "@/features/home/ui/InboxListPane"; -import { useChannelMessagesQuery } from "@/features/messages/hooks"; +import { + useChannelMessagesQuery, + useToggleReactionMutation, +} from "@/features/messages/hooks"; +import { formatTimelineMessages } from "@/features/messages/lib/formatTimelineMessages"; import { getThreadReference } from "@/features/messages/lib/threading"; import { useUsersBatchQuery } from "@/features/profile/hooks"; import { resolveUserLabel } from "@/features/profile/lib/identity"; import { deleteMessage, sendChannelMessage } from "@/shared/api/tauri"; import type { HomeFeedResponse, RelayEvent } from "@/shared/api/types"; +import { KIND_REACTION } from "@/shared/constants/kinds"; import { resolveMentionNames } from "@/shared/lib/resolveMentionNames"; import { Button } from "@/shared/ui/button"; import { Skeleton } from "@/shared/ui/skeleton"; @@ -82,6 +87,17 @@ function getContextMessageDepth( return depth; } +function getReactionTargetId(tags: string[][]) { + for (let index = tags.length - 1; index >= 0; index -= 1) { + const tag = tags[index]; + if (tag?.[0] === "e" && typeof tag[1] === "string") { + return tag[1]; + } + } + + return null; +} + type HomeViewProps = { feed?: HomeFeedResponse; isLoading?: boolean; @@ -140,6 +156,7 @@ export function HomeView({ }, [channels, selectedChannelIdCandidate]); const channelMessagesQuery = useChannelMessagesQuery(selectedChannel); + const toggleReactionMutation = useToggleReactionMutation(); const channelMessages = channelMessagesQuery.data; const threadContext = useInboxThreadContext( selectedFeedItem, @@ -151,10 +168,13 @@ export function HomeView({ ...new Set([ ...feedItems.map((item) => item.pubkey), ...threadContext.events.map((event) => event.pubkey), + ...(channelMessages ?? []) + .filter((event) => event.kind === KIND_REACTION) + .map((event) => event.pubkey), ...(currentPubkey ? [currentPubkey] : []), ]), ], - [currentPubkey, feedItems, threadContext.events], + [channelMessages, currentPubkey, feedItems, threadContext.events], ); const feedProfilesQuery = useUsersBatchQuery(feedProfilePubkeys, { enabled: feedProfilePubkeys.length > 0, @@ -182,23 +202,49 @@ export function HomeView({ const eventById = new Map( threadContext.events.map((event) => [event.id, event]), ); + const contextEventIds = new Set(eventById.keys()); + const reactionEvents = (channelMessages ?? []).filter((event) => { + if (event.kind !== KIND_REACTION) { + return false; + } - return threadContext.events.map((event) => ({ - id: event.id, - authorLabel: resolveUserLabel({ - pubkey: event.pubkey, - currentPubkey, - profiles: feedProfiles, - preferResolvedSelfLabel: true, - }), - avatarUrl: feedProfiles?.[event.pubkey.toLowerCase()]?.avatarUrl ?? null, - content: event.content, - depth: getContextMessageDepth(event, eventById), - fullTimestampLabel: formatInboxFullTimestamp(event.created_at), - isSelected: event.id === selectedItem.id, - mentionNames: resolveMentionNames(event.tags, feedProfiles) ?? [], - })); - }, [currentPubkey, feedProfiles, selectedItem, threadContext.events]); + const targetId = getReactionTargetId(event.tags); + return Boolean(targetId && contextEventIds.has(targetId)); + }); + const currentUserAvatarUrl = currentPubkey + ? (feedProfiles?.[currentPubkey.toLowerCase()]?.avatarUrl ?? null) + : null; + const timelineMessages = formatTimelineMessages( + [...threadContext.events, ...reactionEvents], + selectedChannel, + currentPubkey, + currentUserAvatarUrl, + feedProfiles, + ); + + return timelineMessages.map((message) => { + const event = eventById.get(message.id); + return { + id: message.id, + authorLabel: message.author, + avatarUrl: message.avatarUrl ?? null, + content: message.body, + depth: event ? getContextMessageDepth(event, eventById) : message.depth, + fullTimestampLabel: formatInboxFullTimestamp(message.createdAt), + isSelected: message.id === selectedItem.id, + mentionNames: + resolveMentionNames(message.tags ?? [], feedProfiles) ?? [], + reactions: message.reactions, + }; + }); + }, [ + channelMessages, + currentPubkey, + feedProfiles, + selectedChannel, + selectedItem, + threadContext.events, + ]); const selectedItemReplies = React.useMemo(() => { if (!selectedItem) return []; const localReplies = localRepliesByItemId[selectedItem.id] ?? []; @@ -382,6 +428,19 @@ export function HomeView({ handleToggleDone(selectedItem.id); } }} + onToggleReaction={ + canReply + ? async (message, emoji, remove) => { + await toggleReactionMutation.mutateAsync({ + emoji, + eventId: message.id, + remove, + }); + await channelMessagesQuery.refetch(); + onRefresh(); + } + : undefined + } /> diff --git a/desktop/src/features/home/ui/InboxDetailPane.tsx b/desktop/src/features/home/ui/InboxDetailPane.tsx index 9b3a3aac84..cb02be3b90 100644 --- a/desktop/src/features/home/ui/InboxDetailPane.tsx +++ b/desktop/src/features/home/ui/InboxDetailPane.tsx @@ -56,6 +56,11 @@ type InboxDetailPaneProps = { parentEventId: string; }) => Promise; onToggleDone: () => void; + onToggleReaction?: ( + message: TimelineMessage, + emoji: string, + remove: boolean, + ) => Promise; }; type InboxDisplayMessage = InboxContextMessage & { @@ -70,7 +75,7 @@ function toActionBarMessage(message: InboxDisplayMessage): TimelineMessage { body: message.content, createdAt: 0, depth: message.depth, - reactions: [], + reactions: message.reactions ?? [], time: message.fullTimestampLabel, }; } @@ -91,6 +96,7 @@ export function InboxDetailPane({ onOpenChannel, onSendReply, onToggleDone, + onToggleReaction, }: InboxDetailPaneProps) { const detailPaneRef = React.useRef(null); const [replyTargetId, setReplyTargetId] = React.useState(null); @@ -180,7 +186,7 @@ export function InboxDetailPane({ data-testid="home-inbox-detail" ref={detailPaneRef} > -
+
{message.fullTimestampLabel}

- {canReply ? ( + {canReply || onToggleReaction ? (
handleSelectReplyTarget(message)} - reactions={[]} + onReactionSelect={ + onToggleReaction + ? (emoji) => { + const actionBarMessage = + toActionBarMessage(message); + const remove = + actionBarMessage.reactions?.some( + (reaction) => + reaction.emoji === emoji && + reaction.reactedByCurrentUser, + ) ?? false; + return onToggleReaction( + actionBarMessage, + emoji, + remove, + ); + } + : undefined + } + onReply={ + canReply + ? () => handleSelectReplyTarget(message) + : undefined + } + reactions={message.reactions ?? []} />
diff --git a/desktop/src/features/home/ui/InboxListPane.tsx b/desktop/src/features/home/ui/InboxListPane.tsx index ac94f0aa8c..1e2409908b 100644 --- a/desktop/src/features/home/ui/InboxListPane.tsx +++ b/desktop/src/features/home/ui/InboxListPane.tsx @@ -30,7 +30,7 @@ export function InboxListPane({ }: InboxListPaneProps) { return (
-
+
{FILTER_OPTIONS.map((option) => (