fix(desktop): show emoji reactions in inbox threads

Render inbox reaction pills with the shared message reaction handler so emoji reactions appear the same way they do in channel threads.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Thomas Petersen
2026-05-18 07:51:25 -04:00
parent b46f70de78
commit 2228220156
2 changed files with 159 additions and 103 deletions

View File

@@ -12,12 +12,13 @@ import type {
InboxItem,
InboxReply,
} from "@/features/home/lib/inbox";
import {
type InboxDisplayMessage,
InboxMessageRow,
} from "@/features/home/ui/InboxMessageRow";
import type { TimelineMessage } from "@/features/messages/types";
import { MessageActionBar } from "@/features/messages/ui/MessageActionBar";
import { MessageComposer } from "@/features/messages/ui/MessageComposer";
import { cn } from "@/shared/lib/cn";
import { Button } from "@/shared/ui/button";
import { Markdown } from "@/shared/ui/markdown";
import {
DropdownMenu,
DropdownMenuContent,
@@ -30,7 +31,6 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/shared/ui/tooltip";
import { UserAvatar } from "@/shared/ui/UserAvatar";
type InboxDetailPaneProps = {
canDelete: boolean;
@@ -61,23 +61,6 @@ type InboxDetailPaneProps = {
) => Promise<void>;
};
type InboxDisplayMessage = InboxContextMessage & {
depth: number;
};
function toActionBarMessage(message: InboxDisplayMessage): TimelineMessage {
return {
id: message.id,
author: message.authorLabel,
avatarUrl: message.avatarUrl,
body: message.content,
createdAt: 0,
depth: message.depth,
reactions: message.reactions ?? [],
time: message.fullTimestampLabel,
};
}
export function InboxDetailPane({
canDelete,
canOpenChannel,
@@ -267,88 +250,14 @@ export function InboxDetailPane({
{index === 1 ? (
<div className="mx-6 my-3 border-t border-border/60" />
) : null}
<div className="px-6 py-2">
<article
className={cn(
"group/message relative flex items-start gap-2.5 px-2 py-1 transition-colors duration-1000",
message.isSelected
? cn(
isFocusHighlightVisible
? "bg-primary/[0.07]"
: "bg-transparent",
)
: "hover:bg-muted/20",
)}
data-testid={
message.isSelected
? "home-inbox-selected-message"
: "home-inbox-context-message"
}
>
{canReply || onToggleReaction ? (
<div className="absolute right-2 top-1 z-10">
<MessageActionBar
activeReplyTargetId={replyTargetId}
message={toActionBarMessage(message)}
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 ?? []}
/>
</div>
) : null}
<UserAvatar
avatarUrl={message.avatarUrl}
className="h-8 w-8 shrink-0 rounded-xl"
displayName={message.authorLabel}
size="md"
/>
<div className="-mt-1 min-w-0 flex-1">
<div className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0">
<p className="truncate text-sm font-semibold leading-none tracking-tight text-foreground">
{message.authorLabel}
</p>
<p className="shrink-0 text-xs font-normal leading-none tabular-nums text-muted-foreground/55">
{message.fullTimestampLabel}
</p>
{message.isSelected ? (
<span className="text-[10px] font-semibold uppercase leading-none tracking-[0.14em] text-muted-foreground/70">
Inbox item
</span>
) : null}
</div>
<div className="mt-1">
<Markdown
className="max-w-full text-left text-sm text-foreground"
content={message.content}
mentionNames={message.mentionNames}
tight
/>
</div>
</div>
</article>
</div>
<InboxMessageRow
activeReplyTargetId={replyTargetId}
canReply={canReply}
isFocusHighlightVisible={isFocusHighlightVisible}
message={message}
onSelectReplyTarget={handleSelectReplyTarget}
onToggleReaction={onToggleReaction}
/>
</React.Fragment>
))}
</div>

View File

@@ -0,0 +1,147 @@
import * as React from "react";
import type { InboxContextMessage } from "@/features/home/lib/inbox";
import type { TimelineMessage } from "@/features/messages/types";
import { MessageActionBar } from "@/features/messages/ui/MessageActionBar";
import { MessageReactions } from "@/features/messages/ui/MessageReactions";
import { useReactionHandler } from "@/features/messages/ui/useReactionHandler";
import { cn } from "@/shared/lib/cn";
import { Markdown } from "@/shared/ui/markdown";
import { UserAvatar } from "@/shared/ui/UserAvatar";
export type InboxDisplayMessage = InboxContextMessage & {
depth: number;
};
function toTimelineMessage(message: InboxDisplayMessage): TimelineMessage {
return {
id: message.id,
author: message.authorLabel,
avatarUrl: message.avatarUrl,
body: message.content,
createdAt: 0,
depth: message.depth,
reactions: message.reactions ?? [],
time: message.fullTimestampLabel,
};
}
type InboxMessageRowProps = {
activeReplyTargetId: string | null;
canReply: boolean;
isFocusHighlightVisible: boolean;
message: InboxDisplayMessage;
onSelectReplyTarget: (message: InboxDisplayMessage) => void;
onToggleReaction?: (
message: TimelineMessage,
emoji: string,
remove: boolean,
) => Promise<void>;
};
export function InboxMessageRow({
activeReplyTargetId,
canReply,
isFocusHighlightVisible,
message,
onSelectReplyTarget,
onToggleReaction,
}: InboxMessageRowProps) {
const timelineMessage = React.useMemo(
() => toTimelineMessage(message),
[message],
);
const {
reactions,
canToggle: canToggleReactions,
pending: reactionPending,
errorMessage: reactionErrorMessage,
select: handleReactionSelect,
} = useReactionHandler(timelineMessage, onToggleReaction);
return (
<div className="px-6 py-2">
<article
className={cn(
"group/message relative flex items-start gap-2.5 px-2 py-1 transition-colors duration-1000",
message.isSelected
? cn(
isFocusHighlightVisible
? "bg-primary/[0.07]"
: "bg-transparent",
)
: "hover:bg-muted/20",
)}
data-testid={
message.isSelected
? "home-inbox-selected-message"
: "home-inbox-context-message"
}
>
{canReply || canToggleReactions ? (
<div className="absolute right-2 top-1 z-10">
<MessageActionBar
activeReplyTargetId={activeReplyTargetId}
message={timelineMessage}
onReactionSelect={
canToggleReactions ? handleReactionSelect : undefined
}
onReply={
canReply ? () => onSelectReplyTarget(message) : undefined
}
reactionErrorMessage={reactionErrorMessage}
reactionPending={reactionPending}
reactions={reactions}
/>
</div>
) : null}
<UserAvatar
avatarUrl={message.avatarUrl}
className="h-8 w-8 shrink-0 rounded-xl"
displayName={message.authorLabel}
size="md"
/>
<div className="-mt-1 min-w-0 flex-1">
<div className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0">
<p className="truncate text-sm font-semibold leading-none tracking-tight text-foreground">
{message.authorLabel}
</p>
<p className="shrink-0 text-xs font-normal leading-none tabular-nums text-muted-foreground/55">
{message.fullTimestampLabel}
</p>
{message.isSelected ? (
<span className="text-[10px] font-semibold uppercase leading-none tracking-[0.14em] text-muted-foreground/70">
Inbox item
</span>
) : null}
</div>
<div className="mt-1">
<Markdown
className="max-w-full text-left text-sm text-foreground"
content={message.content}
mentionNames={message.mentionNames}
tight
/>
<MessageReactions
canToggle={canToggleReactions}
messageId={message.id}
onSelect={(emoji) => {
void handleReactionSelect(emoji);
}}
pending={reactionPending}
reactions={reactions}
/>
{reactionErrorMessage ? (
<p className="mt-1.5 text-xs text-destructive">
{reactionErrorMessage}
</p>
) : null}
</div>
</div>
</article>
</div>
);
}