mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
227 lines
8.6 KiB
TypeScript
227 lines
8.6 KiB
TypeScript
import * as React from "react";
|
|
|
|
import { useKnownAgentPubkeys } from "@/features/agents/useKnownAgentPubkeys";
|
|
import type { InboxContextMessage } from "@/features/home/lib/inbox";
|
|
import { toTimelineMessage } from "@/features/home/lib/inboxViewHelpers";
|
|
import { formatTimeWithoutDayPeriod } from "@/features/messages/lib/dateFormatters";
|
|
import type { TimelineMessage } from "@/features/messages/types";
|
|
import { getConfigNudgeAuthorPubkey } from "@/features/messages/ui/configNudgeAuthPubkey";
|
|
import { MessageActionBar } from "@/features/messages/ui/MessageActionBar";
|
|
import { MessageReactions } from "@/features/messages/ui/MessageReactions";
|
|
import { useReactionHandler } from "@/features/messages/ui/useReactionHandler";
|
|
import { useMessageEmoji } from "@/features/messages/lib/useMessageEmoji";
|
|
import { UserProfilePopover } from "@/features/profile/ui/UserProfilePopover";
|
|
import { cn } from "@/shared/lib/cn";
|
|
import { normalizePubkey } from "@/shared/lib/pubkey";
|
|
import { Markdown } from "@/shared/ui/markdown";
|
|
import { UserAvatar } from "@/shared/ui/UserAvatar";
|
|
|
|
export type InboxDisplayMessage = InboxContextMessage & {
|
|
depth: number;
|
|
};
|
|
|
|
type InboxMessageRowProps = {
|
|
agentPubkeys?: ReadonlySet<string>;
|
|
canReply: boolean;
|
|
/** Channel UUID for "Copy link" — passed straight through to MessageActionBar. */
|
|
channelId?: string | null;
|
|
isContinuation?: boolean;
|
|
isFocusHighlightVisible: boolean;
|
|
message: InboxDisplayMessage;
|
|
onSelectReplyTarget: (message: InboxDisplayMessage) => void;
|
|
onToggleReaction?: (
|
|
message: TimelineMessage,
|
|
emoji: string,
|
|
remove: boolean,
|
|
) => Promise<void>;
|
|
};
|
|
|
|
export function InboxMessageRow({
|
|
agentPubkeys,
|
|
canReply,
|
|
channelId = null,
|
|
isContinuation = false,
|
|
isFocusHighlightVisible,
|
|
message,
|
|
onSelectReplyTarget,
|
|
onToggleReaction,
|
|
}: InboxMessageRowProps) {
|
|
const timelineMessage = React.useMemo(
|
|
() => toTimelineMessage(message),
|
|
[message],
|
|
);
|
|
const { customEmoji, emojiOnly } = useMessageEmoji(
|
|
message.content,
|
|
message.tags,
|
|
);
|
|
const [badgeBurstEmoji, setBadgeBurstEmoji] = React.useState<string | null>(
|
|
null,
|
|
);
|
|
const {
|
|
reactions,
|
|
canToggle: canToggleReactions,
|
|
pending: reactionPending,
|
|
errorMessage: reactionErrorMessage,
|
|
select: handleReactionSelect,
|
|
} = useReactionHandler(timelineMessage, onToggleReaction);
|
|
// "Is this pubkey an agent" = the community-scoped baseline every surface
|
|
// shares plus this surface's extras passed via `agentPubkeys` (HomeView
|
|
// folds feed-profile `isAgent` flags in). Mirrors MessageRow's predicate.
|
|
const knownAgentPubkeys = useKnownAgentPubkeys();
|
|
const isKnownAgentPubkey = React.useCallback(
|
|
(pubkey: string) => {
|
|
const normalized = normalizePubkey(pubkey);
|
|
return (
|
|
knownAgentPubkeys.has(normalized) ||
|
|
agentPubkeys?.has(normalized) === true
|
|
);
|
|
},
|
|
[agentPubkeys, knownAgentPubkeys],
|
|
);
|
|
const isAuthorAgent = isKnownAgentPubkey(message.authorPubkey);
|
|
const profileRole = isAuthorAgent ? "bot" : undefined;
|
|
const hoverTimestampLabel = formatTimeWithoutDayPeriod(
|
|
message.timeLabel ?? message.fullTimestampLabel,
|
|
);
|
|
|
|
return (
|
|
<div className="relative px-2">
|
|
{message.isSelected ? (
|
|
<div
|
|
aria-hidden="true"
|
|
className={cn(
|
|
"pointer-events-none absolute inset-x-3 inset-y-1 rounded-2xl transition-opacity duration-1000",
|
|
isFocusHighlightVisible
|
|
? "bg-primary/[0.07] opacity-100"
|
|
: "bg-primary/[0.07] opacity-0",
|
|
)}
|
|
/>
|
|
) : null}
|
|
<article
|
|
className={cn(
|
|
"group/message relative z-10 mx-1 flex gap-2.5 rounded-2xl px-2 py-1 transition-colors hover:bg-muted/50 focus-within:bg-muted/50",
|
|
isContinuation ? "items-center" : "items-start",
|
|
)}
|
|
data-message-id={message.id}
|
|
data-testid={
|
|
message.isSelected
|
|
? "home-inbox-selected-message"
|
|
: "home-inbox-context-message"
|
|
}
|
|
>
|
|
{canReply || canToggleReactions ? (
|
|
<div className="absolute right-2 top-1 z-10 sm:top-0 sm:-translate-y-1/2">
|
|
<MessageActionBar
|
|
channelId={channelId}
|
|
message={timelineMessage}
|
|
onReactionSelect={
|
|
canToggleReactions ? handleReactionSelect : undefined
|
|
}
|
|
onReactionBadgeBurstRequest={
|
|
reactionPending ? undefined : setBadgeBurstEmoji
|
|
}
|
|
onReply={
|
|
canReply ? () => onSelectReplyTarget(message) : undefined
|
|
}
|
|
reactionErrorMessage={reactionErrorMessage}
|
|
reactions={reactions}
|
|
/>
|
|
</div>
|
|
) : null}
|
|
|
|
{isContinuation ? (
|
|
<div
|
|
aria-hidden="true"
|
|
className="flex w-9 shrink-0 self-stretch items-start justify-end pt-0.5"
|
|
title={message.fullTimestampLabel}
|
|
>
|
|
<p className="shrink-0 cursor-default whitespace-nowrap text-xs font-normal leading-4 tabular-nums text-muted-foreground/55 opacity-0 transition-opacity group-hover/message:opacity-100 group-focus-within/message:opacity-100">
|
|
{hoverTimestampLabel}
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="relative shrink-0">
|
|
<UserProfilePopover
|
|
botIdenticonValue={message.authorLabel}
|
|
pubkey={message.authorPubkey}
|
|
role={profileRole}
|
|
triggerElement="span"
|
|
>
|
|
<span className="inline-flex shrink-0 rounded-full focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring">
|
|
<UserAvatar
|
|
avatarUrl={message.avatarUrl}
|
|
className="h-9 w-9 shrink-0"
|
|
displayName={message.authorLabel}
|
|
size="md"
|
|
/>
|
|
</span>
|
|
</UserProfilePopover>
|
|
</div>
|
|
)}
|
|
|
|
<div className="min-w-0 flex-1">
|
|
{isContinuation ? null : (
|
|
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-0">
|
|
<UserProfilePopover
|
|
botIdenticonValue={message.authorLabel}
|
|
pubkey={message.authorPubkey}
|
|
role={profileRole}
|
|
triggerElement="span"
|
|
>
|
|
<span className="block max-w-full truncate rounded text-sm font-semibold text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring">
|
|
{message.authorLabel}
|
|
</span>
|
|
</UserProfilePopover>
|
|
<p className="shrink-0 text-xs font-normal tabular-nums text-muted-foreground/55">
|
|
{message.fullTimestampLabel}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className={isContinuation ? "mt-0" : "mt-0.5"}>
|
|
<Markdown
|
|
className={cn(
|
|
"max-w-full text-left text-sm text-foreground",
|
|
emojiOnly &&
|
|
"text-4xl leading-tight [&_p]:leading-tight [&_img[data-custom-emoji]]:h-[1.45em] [&_img[data-custom-emoji]]:align-middle [&_button:has(img[data-custom-emoji])]:align-middle",
|
|
)}
|
|
// Only pass the author pubkey for agent-authored messages so
|
|
// config-nudge cards can authenticate the sender. Uses the
|
|
// raw event signer (signerPubkey) — not the tag-attributed
|
|
// display author — to prevent actor/p-tag spoofing.
|
|
configNudgeAuthorPubkey={getConfigNudgeAuthorPubkey(
|
|
timelineMessage,
|
|
isKnownAgentPubkey,
|
|
)}
|
|
content={message.content}
|
|
customEmoji={customEmoji}
|
|
mentionNames={message.mentionNames}
|
|
mentionPubkeysByName={message.mentionPubkeysByName}
|
|
/>
|
|
<MessageReactions
|
|
canToggle={canToggleReactions}
|
|
messageId={message.id}
|
|
onSelect={(emoji) => {
|
|
void handleReactionSelect(emoji);
|
|
}}
|
|
burstEmojiOnRender={badgeBurstEmoji}
|
|
onBurstEmojiRendered={(emoji) => {
|
|
setBadgeBurstEmoji((current) =>
|
|
current === emoji ? null : current,
|
|
);
|
|
}}
|
|
pending={reactionPending}
|
|
reactions={reactions}
|
|
/>
|
|
{reactionErrorMessage ? (
|
|
<p className="mt-1.5 text-xs text-destructive">
|
|
{reactionErrorMessage}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</article>
|
|
</div>
|
|
);
|
|
}
|