mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(desktop): show agent owners in messages (#2080)
Signed-off-by: npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n <03271c724b51c287eb37fc1633105ae90417e975e63c3a6b4c339c81b155ba3e@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n <03271c724b51c287eb37fc1633105ae90417e975e63c3a6b4c339c81b155ba3e@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
co-authored by
npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n
parent
b18a79476b
commit
463db6de3e
@@ -132,6 +132,7 @@ export const ChannelPane = React.memo(function ChannelPane({
|
||||
unfollowThreadById,
|
||||
personaLookup,
|
||||
profiles,
|
||||
ownerProfiles,
|
||||
openThreadHeadId,
|
||||
shouldShowThreadSkeleton,
|
||||
openAgentSessionChannelId,
|
||||
@@ -605,6 +606,7 @@ export const ChannelPane = React.memo(function ChannelPane({
|
||||
isMessageUnreadById={isMessageUnreadById}
|
||||
personaLookup={personaLookup}
|
||||
profiles={profiles}
|
||||
ownerProfiles={ownerProfiles}
|
||||
unfollowThreadById={unfollowThreadById}
|
||||
emptyDescription={
|
||||
activeChannel?.channelType === "forum"
|
||||
|
||||
@@ -134,6 +134,7 @@ export type ChannelPaneProps = {
|
||||
) => void;
|
||||
personaLookup?: Map<string, string>;
|
||||
profiles?: UserProfileLookup;
|
||||
ownerProfiles?: UserProfileLookup;
|
||||
openThreadHeadId: string | null;
|
||||
shouldShowThreadSkeleton: boolean;
|
||||
openAgentSessionChannelId: string | null;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useAppNavigation } from "@/app/navigation/useAppNavigation";
|
||||
import { useActiveChannelHeader } from "@/features/channels/useActiveChannelHeader";
|
||||
import { useChannelPaneHandlers } from "@/features/channels/useChannelPaneHandlers";
|
||||
import { useMessageEventProfilePubkeys } from "@/features/channels/useMessageEventProfilePubkeys";
|
||||
import { useMessageOwnerProfiles } from "@/features/channels/useMessageOwnerProfiles";
|
||||
import { useThreadTargetSync } from "@/features/channels/useThreadTargetSync";
|
||||
import {
|
||||
useChannelMembersQuery,
|
||||
@@ -350,6 +351,7 @@ export function ChannelScreen({
|
||||
profiles: messageProfilesQuery.data?.profiles,
|
||||
relayAgents,
|
||||
});
|
||||
const messageOwnerProfiles = useMessageOwnerProfiles(messageProfiles);
|
||||
// Agent set for ChannelPane's own consumers (DM huddle member resolution,
|
||||
// the agents list): the community-scoped baseline shared by every surface,
|
||||
// widened with channel-member roles and this screen's profile lookup.
|
||||
@@ -393,6 +395,7 @@ export function ChannelScreen({
|
||||
personaLookup,
|
||||
respondToLookup,
|
||||
relaySelfPubkey,
|
||||
messageOwnerProfiles,
|
||||
),
|
||||
[
|
||||
activeChannel,
|
||||
@@ -400,6 +403,7 @@ export function ChannelScreen({
|
||||
currentProfile?.avatarUrl,
|
||||
currentPubkey,
|
||||
messageProfiles,
|
||||
messageOwnerProfiles,
|
||||
personaLookup,
|
||||
relaySelfPubkey,
|
||||
respondToLookup,
|
||||
@@ -437,6 +441,7 @@ export function ChannelScreen({
|
||||
currentPubkey,
|
||||
currentAvatarUrl: currentProfile?.avatarUrl ?? null,
|
||||
profiles: messageProfiles,
|
||||
ownerProfiles: messageOwnerProfiles,
|
||||
members: channelMembers,
|
||||
personaLookup,
|
||||
respondToLookup,
|
||||
@@ -934,6 +939,7 @@ export function ChannelScreen({
|
||||
profilePanelView={profilePanelView}
|
||||
personaLookup={personaLookup}
|
||||
profiles={messageProfiles}
|
||||
ownerProfiles={messageOwnerProfiles}
|
||||
firstUnreadMessageId={firstUnreadMessageId}
|
||||
unreadCount={unreadCount}
|
||||
targetMessageId={mainTimelineTargetMessageId}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { useUsersBatchQuery } from "@/features/profile/hooks";
|
||||
import type { UserProfileLookup } from "@/features/profile/lib/identity";
|
||||
|
||||
/** Fetches verified agent-owner profiles in one batch for message surfaces. */
|
||||
export function useMessageOwnerProfiles(profiles: UserProfileLookup) {
|
||||
const ownerPubkeys = React.useMemo(
|
||||
() => [
|
||||
...new Set(
|
||||
Object.values(profiles)
|
||||
.map((profile) => profile.ownerPubkey)
|
||||
.filter((pubkey): pubkey is string => Boolean(pubkey)),
|
||||
),
|
||||
],
|
||||
[profiles],
|
||||
);
|
||||
const ownerProfilesQuery = useUsersBatchQuery(ownerPubkeys, {
|
||||
enabled: ownerPubkeys.length > 0,
|
||||
});
|
||||
return ownerProfilesQuery.data?.profiles;
|
||||
}
|
||||
@@ -60,6 +60,9 @@ export type InboxTypeLabel = {
|
||||
export type InboxReply = {
|
||||
authorLabel: string;
|
||||
authorPubkey: string;
|
||||
isAgent?: boolean;
|
||||
ownerLabel?: string | null;
|
||||
ownerPubkey?: string | null;
|
||||
avatarUrl: string | null;
|
||||
content: string;
|
||||
createdAt: number;
|
||||
|
||||
@@ -139,6 +139,9 @@ export function toInboxContextMessage(
|
||||
id: message.id,
|
||||
authorLabel: message.author,
|
||||
authorPubkey,
|
||||
isAgent: message.isAgent,
|
||||
ownerLabel: message.ownerLabel,
|
||||
ownerPubkey: message.ownerPubkey,
|
||||
avatarUrl: message.avatarUrl ?? null,
|
||||
content: message.body,
|
||||
createdAt: message.createdAt,
|
||||
@@ -169,6 +172,9 @@ export function toTimelineMessage(
|
||||
return {
|
||||
id: message.id,
|
||||
author: message.authorLabel,
|
||||
isAgent: message.isAgent,
|
||||
ownerLabel: message.ownerLabel,
|
||||
ownerPubkey: message.ownerPubkey,
|
||||
avatarUrl: message.avatarUrl,
|
||||
body: message.content,
|
||||
createdAt: message.createdAt,
|
||||
|
||||
@@ -333,6 +333,20 @@ export function HomeView({
|
||||
enabled: feedProfilePubkeys.length > 0,
|
||||
});
|
||||
const feedProfiles = feedProfilesQuery.data?.profiles;
|
||||
const feedOwnerPubkeys = React.useMemo(
|
||||
() => [
|
||||
...new Set(
|
||||
Object.values(feedProfiles ?? {})
|
||||
.map((profile) => profile.ownerPubkey)
|
||||
.filter((pubkey): pubkey is string => Boolean(pubkey)),
|
||||
),
|
||||
],
|
||||
[feedProfiles],
|
||||
);
|
||||
const feedOwnerProfilesQuery = useUsersBatchQuery(feedOwnerPubkeys, {
|
||||
enabled: feedOwnerPubkeys.length > 0,
|
||||
});
|
||||
const feedOwnerProfiles = feedOwnerProfilesQuery.data?.profiles;
|
||||
// Agent set for the inbox list/detail bot badges: the community-scoped
|
||||
// baseline widened with this surface's profile lookup.
|
||||
const communityAgentPubkeys = useKnownAgentPubkeys();
|
||||
@@ -477,6 +491,7 @@ export function HomeView({
|
||||
undefined,
|
||||
undefined,
|
||||
relaySelfPubkey,
|
||||
feedOwnerProfiles,
|
||||
);
|
||||
|
||||
return timelineMessages.map((message) =>
|
||||
@@ -491,6 +506,7 @@ export function HomeView({
|
||||
channelMessages,
|
||||
currentPubkey,
|
||||
feedProfiles,
|
||||
feedOwnerProfiles,
|
||||
relaySelfPubkey,
|
||||
selectedChannel,
|
||||
selectedEventId,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { formatTimeWithoutDayPeriod } from "@/features/messages/lib/dateFormatte
|
||||
import type { TimelineMessage } from "@/features/messages/types";
|
||||
import { getConfigNudgeAuthorPubkey } from "@/features/messages/ui/configNudgeAuthPubkey";
|
||||
import { MessageActionBar } from "@/features/messages/ui/MessageActionBar";
|
||||
import { MessageAgentOwner } from "@/features/messages/ui/MessageAgentOwner";
|
||||
import { MessageReactions } from "@/features/messages/ui/MessageReactions";
|
||||
import { useReactionHandler } from "@/features/messages/ui/useReactionHandler";
|
||||
import { useMessageEmoji } from "@/features/messages/lib/useMessageEmoji";
|
||||
@@ -172,6 +173,12 @@ export function InboxMessageRow({
|
||||
{message.authorLabel}
|
||||
</span>
|
||||
</UserProfilePopover>
|
||||
{message.isAgent ? (
|
||||
<MessageAgentOwner
|
||||
ownerLabel={message.ownerLabel}
|
||||
ownerPubkey={message.ownerPubkey}
|
||||
/>
|
||||
) : null}
|
||||
<p className="shrink-0 text-xs font-normal tabular-nums text-muted-foreground/55">
|
||||
{message.fullTimestampLabel}
|
||||
</p>
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
isBroadcastReply,
|
||||
} from "@/features/messages/lib/threading";
|
||||
import {
|
||||
formatOwnerLabel,
|
||||
resolveUserLabel,
|
||||
type UserProfileLookup,
|
||||
} from "@/features/profile/lib/identity";
|
||||
@@ -193,6 +194,8 @@ export function formatTimelineMessages(
|
||||
respondToLookup?: Map<string, RespondToMode>,
|
||||
/** Active relay identity from NIP-11 `self`; absent or malformed fails closed to the signer. */
|
||||
relaySelfPubkey?: string | null,
|
||||
/** Profiles for verified agent owners, fetched in one batch by the surface. */
|
||||
ownerProfiles?: UserProfileLookup,
|
||||
): TimelineMessage[] {
|
||||
const currentPubkeyLower = currentPubkey?.toLowerCase();
|
||||
const roleByPubkey = new Map<string, string>();
|
||||
@@ -423,6 +426,9 @@ export function formatTimelineMessages(
|
||||
const thread = getThreadReference(event.tags);
|
||||
const edit = editsByTargetId.get(event.id);
|
||||
const role = roleByPubkey.get(authorPubkey.toLowerCase());
|
||||
const authorProfile = profiles?.[authorPubkey.toLowerCase()];
|
||||
const isAgent = role === "bot" || authorProfile?.isAgent === true;
|
||||
const ownerPubkey = isAgent ? (authorProfile?.ownerPubkey ?? null) : null;
|
||||
return {
|
||||
id: event.id,
|
||||
renderKey: event.localKey ?? event.id,
|
||||
@@ -430,6 +436,11 @@ export function formatTimelineMessages(
|
||||
pubkey: authorPubkey,
|
||||
signerPubkey: normalizePubkey(event.pubkey),
|
||||
author,
|
||||
isAgent,
|
||||
ownerPubkey,
|
||||
ownerLabel: isAgent
|
||||
? formatOwnerLabel(ownerPubkey, currentPubkey, ownerProfiles)
|
||||
: null,
|
||||
avatarUrl: getAuthorAvatarUrl({
|
||||
authorPubkey,
|
||||
currentPubkey,
|
||||
|
||||
@@ -25,6 +25,12 @@ export type TimelineMessage = {
|
||||
*/
|
||||
signerPubkey?: string;
|
||||
author: string;
|
||||
/** True when the displayed author is known to be an agent. */
|
||||
isAgent?: boolean;
|
||||
/** Verified owner pubkey for an agent author, when available. */
|
||||
ownerPubkey?: string | null;
|
||||
/** Viewer-relative owner label (for example, "you" or "baxen"). */
|
||||
ownerLabel?: string | null;
|
||||
avatarUrl?: string | null;
|
||||
role?: string;
|
||||
/** For bot messages, the display name of the persona this bot was created from. */
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Bot } from "lucide-react";
|
||||
|
||||
import { UserProfilePopover } from "@/features/profile/ui/UserProfilePopover";
|
||||
|
||||
export function MessageAgentOwner({
|
||||
ownerLabel,
|
||||
ownerPubkey,
|
||||
}: {
|
||||
ownerLabel?: string | null;
|
||||
ownerPubkey?: string | null;
|
||||
}) {
|
||||
return (
|
||||
<span
|
||||
className="inline-flex min-w-0 max-w-56 items-baseline gap-1 text-xs leading-4 text-muted-foreground/65"
|
||||
data-testid="message-agent-owner"
|
||||
>
|
||||
<span className="sr-only">
|
||||
{ownerLabel ? "Agent owned by" : "Agent; owner unavailable"}
|
||||
</span>
|
||||
{ownerPubkey && ownerLabel ? (
|
||||
<>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="inline-flex shrink-0 items-baseline gap-1 leading-4"
|
||||
>
|
||||
<Bot className="relative -top-px h-3.5 w-3.5 self-center" />
|
||||
<span>owned by</span>
|
||||
</span>
|
||||
<UserProfilePopover
|
||||
pubkey={ownerPubkey}
|
||||
triggerAriaLabel={ownerLabel}
|
||||
triggerElement="span"
|
||||
>
|
||||
<span className="min-w-0 truncate rounded font-semibold text-foreground/85 hover:text-foreground hover:underline focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring">
|
||||
{ownerLabel}
|
||||
</span>
|
||||
</UserProfilePopover>
|
||||
</>
|
||||
) : (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="inline-flex min-w-0 items-center gap-1"
|
||||
>
|
||||
<Bot className="h-3.5 w-3.5 shrink-0" />
|
||||
<span className="truncate">owner unavailable</span>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -40,6 +40,7 @@ import { resolveMentionProps } from "@/shared/lib/resolveMentionNames";
|
||||
import { Markdown } from "@/shared/ui/markdown";
|
||||
import type { VideoReviewContext } from "@/shared/ui/VideoPlayer";
|
||||
import { MessageActionBar } from "./MessageActionBar";
|
||||
import { MessageAgentOwner } from "./MessageAgentOwner";
|
||||
import { MessageAuthorText, MessageHeaderRow } from "./MessageHeader";
|
||||
import { MessageTimestamp } from "./MessageTimestamp";
|
||||
import { WaveMessageAttachment } from "./WaveMessageAttachment";
|
||||
@@ -459,6 +460,12 @@ export const MessageRow = React.memo(
|
||||
) : (
|
||||
<MessageAuthorText as="h3">{message.author}</MessageAuthorText>
|
||||
);
|
||||
const agentOwnerNode = message.isAgent ? (
|
||||
<MessageAgentOwner
|
||||
ownerLabel={message.ownerLabel}
|
||||
ownerPubkey={message.ownerPubkey}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const actionBarNode = (
|
||||
<div
|
||||
@@ -545,6 +552,7 @@ export const MessageRow = React.memo(
|
||||
) : (
|
||||
authorNode
|
||||
)}
|
||||
{agentOwnerNode}
|
||||
{inlineMetadataNode}
|
||||
{message.personaDisplayName &&
|
||||
message.personaDisplayName !== message.author ? (
|
||||
@@ -811,6 +819,9 @@ export const MessageRow = React.memo(
|
||||
prev.message.pubkey === next.message.pubkey &&
|
||||
prev.message.body === next.message.body &&
|
||||
prev.message.author === next.message.author &&
|
||||
prev.message.isAgent === next.message.isAgent &&
|
||||
prev.message.ownerPubkey === next.message.ownerPubkey &&
|
||||
prev.message.ownerLabel === next.message.ownerLabel &&
|
||||
prev.message.avatarUrl === next.message.avatarUrl &&
|
||||
prev.message.accent === next.message.accent &&
|
||||
prev.message.time === next.message.time &&
|
||||
|
||||
@@ -74,6 +74,7 @@ type MessageTimelineProps = {
|
||||
/** Map from lowercase pubkey → persona display name for bot members. */
|
||||
personaLookup?: Map<string, string>;
|
||||
profiles?: UserProfileLookup;
|
||||
ownerProfiles?: UserProfileLookup;
|
||||
followThreadById?: (rootId: string) => void;
|
||||
isFollowingThreadById?: (rootId: string) => boolean;
|
||||
isMessageUnreadById?: (messageId: string) => boolean;
|
||||
@@ -170,6 +171,7 @@ const MessageTimelineBase = React.forwardRef<
|
||||
messageFooters,
|
||||
personaLookup,
|
||||
profiles,
|
||||
ownerProfiles,
|
||||
onDelete,
|
||||
onEdit,
|
||||
onMarkUnread,
|
||||
@@ -636,6 +638,7 @@ const MessageTimelineBase = React.forwardRef<
|
||||
onAtBottomStateChange={handleVirtualizerAtBottomStateChange}
|
||||
personaLookup={personaLookup}
|
||||
profiles={profiles}
|
||||
ownerProfiles={ownerProfiles}
|
||||
searchActiveMessageId={searchActiveMessageId}
|
||||
searchMatchingMessageIds={searchMatchingMessageIds}
|
||||
searchQuery={searchQuery}
|
||||
|
||||
@@ -9,7 +9,10 @@ import type {
|
||||
import { MessageReactions } from "@/features/messages/ui/MessageReactions";
|
||||
import { useReactionHandler } from "@/features/messages/ui/useReactionHandler";
|
||||
import { recordQuickReactionEmoji } from "@/features/messages/ui/useQuickReactionEmojis";
|
||||
import type { UserProfileLookup } from "@/features/profile/lib/identity";
|
||||
import {
|
||||
formatOwnerLabel,
|
||||
type UserProfileLookup,
|
||||
} from "@/features/profile/lib/identity";
|
||||
import { resolveUserLabel } from "@/features/profile/lib/identity";
|
||||
import { UserProfilePopover } from "@/features/profile/ui/UserProfilePopover";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
@@ -25,6 +28,7 @@ import {
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
|
||||
import { UserAvatar } from "@/shared/ui/UserAvatar";
|
||||
import { MessageAgentOwner } from "./MessageAgentOwner";
|
||||
import { MessageAuthorText, MessageHeaderRow } from "./MessageHeader";
|
||||
import { MessageTimestamp } from "./MessageTimestamp";
|
||||
|
||||
@@ -631,6 +635,7 @@ export const SystemMessageRow = React.memo(function SystemMessageRow({
|
||||
currentPubkey,
|
||||
agentPubkeys,
|
||||
profiles,
|
||||
ownerProfiles,
|
||||
personaLookup,
|
||||
onToggleReaction,
|
||||
}: {
|
||||
@@ -639,6 +644,7 @@ export const SystemMessageRow = React.memo(function SystemMessageRow({
|
||||
currentPubkey?: string;
|
||||
agentPubkeys?: ReadonlySet<string>;
|
||||
profiles?: UserProfileLookup;
|
||||
ownerProfiles?: UserProfileLookup;
|
||||
/** Map from lowercase pubkey → persona display name for bot members. */
|
||||
personaLookup?: Map<string, string>;
|
||||
onToggleReaction?: (
|
||||
@@ -720,6 +726,33 @@ export const SystemMessageRow = React.memo(function SystemMessageRow({
|
||||
payload.type === "member_joined" ||
|
||||
payload.type === "members_added" ||
|
||||
payload.type === "members_joined";
|
||||
const displayedIdentityPubkey = isMembershipArrival
|
||||
? payload.target
|
||||
: payload.actor;
|
||||
const displayedIdentityProfile = displayedIdentityPubkey
|
||||
? profiles?.[normalizePubkey(displayedIdentityPubkey)]
|
||||
: undefined;
|
||||
const displayedTimelineIdentity = displayedIdentityPubkey
|
||||
? sourceMessages.find(
|
||||
(source) =>
|
||||
source.pubkey &&
|
||||
normalizePubkey(source.pubkey) ===
|
||||
normalizePubkey(displayedIdentityPubkey),
|
||||
)
|
||||
: undefined;
|
||||
const displayedIdentityIsAgent = Boolean(
|
||||
displayedIdentityProfile?.isAgent ||
|
||||
displayedTimelineIdentity?.isAgent ||
|
||||
(displayedIdentityPubkey &&
|
||||
agentPubkeys?.has(normalizePubkey(displayedIdentityPubkey))),
|
||||
);
|
||||
const displayedOwnerPubkey =
|
||||
displayedIdentityProfile?.ownerPubkey ??
|
||||
displayedTimelineIdentity?.ownerPubkey ??
|
||||
null;
|
||||
const displayedOwnerLabel =
|
||||
displayedTimelineIdentity?.ownerLabel ??
|
||||
formatOwnerLabel(displayedOwnerPubkey, currentPubkey, ownerProfiles);
|
||||
|
||||
const wouldAddReaction = (emoji: string) =>
|
||||
!reactions.some(
|
||||
@@ -750,6 +783,12 @@ export const SystemMessageRow = React.memo(function SystemMessageRow({
|
||||
<MessageAuthorText as="div" className="text-foreground">
|
||||
{description.title}
|
||||
</MessageAuthorText>
|
||||
{displayedIdentityIsAgent ? (
|
||||
<MessageAgentOwner
|
||||
ownerLabel={displayedOwnerLabel}
|
||||
ownerPubkey={displayedOwnerPubkey}
|
||||
/>
|
||||
) : null}
|
||||
<MessageTimestamp
|
||||
createdAt={message.createdAt}
|
||||
time={message.time}
|
||||
|
||||
@@ -96,6 +96,7 @@ type TimelineMessageListProps = {
|
||||
/** Map from lowercase pubkey → persona display name for bot members. */
|
||||
personaLookup?: Map<string, string>;
|
||||
profiles?: UserProfileLookup;
|
||||
ownerProfiles?: UserProfileLookup;
|
||||
/** The message ID of the currently active find-in-channel match. */
|
||||
searchActiveMessageId?: string | null;
|
||||
/** Set of message IDs that match the current find-in-channel query. */
|
||||
@@ -147,6 +148,7 @@ export const TimelineMessageList = React.memo(function TimelineMessageList({
|
||||
onSendVideoReviewComment,
|
||||
onToggleReaction,
|
||||
profiles,
|
||||
ownerProfiles,
|
||||
searchActiveMessageId = null,
|
||||
searchMatchingMessageIds,
|
||||
searchQuery,
|
||||
@@ -237,6 +239,7 @@ export const TimelineMessageList = React.memo(function TimelineMessageList({
|
||||
footer={messageFooters?.[item.entry.message.id] ?? null}
|
||||
onToggleReaction={onToggleReaction}
|
||||
profiles={profiles}
|
||||
ownerProfiles={ownerProfiles}
|
||||
/>
|
||||
);
|
||||
case "system-group":
|
||||
@@ -249,6 +252,7 @@ export const TimelineMessageList = React.memo(function TimelineMessageList({
|
||||
)}
|
||||
onToggleReaction={onToggleReaction}
|
||||
profiles={profiles}
|
||||
ownerProfiles={ownerProfiles}
|
||||
/>
|
||||
);
|
||||
case "message":
|
||||
@@ -306,6 +310,7 @@ export const TimelineMessageList = React.memo(function TimelineMessageList({
|
||||
onReply,
|
||||
onToggleReaction,
|
||||
profiles,
|
||||
ownerProfiles,
|
||||
searchActiveMessageId,
|
||||
searchMatchingMessageIds,
|
||||
searchQuery,
|
||||
@@ -782,6 +787,7 @@ function SystemRow({
|
||||
footer,
|
||||
onToggleReaction,
|
||||
profiles,
|
||||
ownerProfiles,
|
||||
}: {
|
||||
currentPubkey?: string;
|
||||
entries?: MainTimelineEntry[];
|
||||
@@ -789,6 +795,7 @@ function SystemRow({
|
||||
footer: React.ReactNode;
|
||||
onToggleReaction?: TimelineMessageListProps["onToggleReaction"];
|
||||
profiles?: UserProfileLookup;
|
||||
ownerProfiles?: UserProfileLookup;
|
||||
}) {
|
||||
const systemEntries = entries ?? (entry ? [entry] : []);
|
||||
const firstEntry = systemEntries[0];
|
||||
@@ -806,6 +813,7 @@ function SystemRow({
|
||||
currentPubkey={currentPubkey}
|
||||
onToggleReaction={onToggleReaction}
|
||||
profiles={profiles}
|
||||
ownerProfiles={ownerProfiles}
|
||||
/>
|
||||
{footer}
|
||||
</div>
|
||||
|
||||
@@ -19,6 +19,7 @@ export function useIndependentThreadPanel(args: {
|
||||
currentPubkey: string | undefined;
|
||||
currentAvatarUrl: string | null;
|
||||
profiles: UserProfileLookup | undefined;
|
||||
ownerProfiles: UserProfileLookup | undefined;
|
||||
members: ChannelMember[] | undefined;
|
||||
personaLookup: Map<string, string>;
|
||||
respondToLookup: Map<string, RespondToMode>;
|
||||
@@ -48,6 +49,7 @@ export function useIndependentThreadPanel(args: {
|
||||
args.personaLookup,
|
||||
args.respondToLookup,
|
||||
args.relaySelfPubkey,
|
||||
args.ownerProfiles,
|
||||
),
|
||||
[
|
||||
args.channelEvents,
|
||||
@@ -59,6 +61,7 @@ export function useIndependentThreadPanel(args: {
|
||||
args.currentPubkey,
|
||||
args.currentAvatarUrl,
|
||||
args.profiles,
|
||||
args.ownerProfiles,
|
||||
args.members,
|
||||
args.personaLookup,
|
||||
args.respondToLookup,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { profileLookupsEqual } from "./identity.ts";
|
||||
import { formatOwnerLabel, profileLookupsEqual } from "./identity.ts";
|
||||
|
||||
const OWNER_PUBKEY =
|
||||
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
|
||||
|
||||
const summary = (over = {}) => ({
|
||||
displayName: "Ada",
|
||||
@@ -12,6 +15,23 @@ const summary = (over = {}) => ({
|
||||
...over,
|
||||
});
|
||||
|
||||
test("formatOwnerLabel resolves a known owner's display name", () => {
|
||||
assert.equal(
|
||||
formatOwnerLabel(OWNER_PUBKEY, null, {
|
||||
[OWNER_PUBKEY]: summary({ displayName: "baxen" }),
|
||||
}),
|
||||
"baxen",
|
||||
);
|
||||
});
|
||||
|
||||
test("formatOwnerLabel calls the viewer-owned agent's owner you", () => {
|
||||
assert.equal(formatOwnerLabel(OWNER_PUBKEY, OWNER_PUBKEY, {}), "you");
|
||||
});
|
||||
|
||||
test("formatOwnerLabel returns null when verified ownership is absent", () => {
|
||||
assert.equal(formatOwnerLabel(null, OWNER_PUBKEY, {}), null);
|
||||
});
|
||||
|
||||
test("profileLookupsEqual: same reference is equal", () => {
|
||||
const a = { p1: summary() };
|
||||
assert.equal(profileLookupsEqual(a, a), true);
|
||||
|
||||
@@ -55,6 +55,8 @@ type UserProfilePopoverProps = {
|
||||
children: React.ReactNode;
|
||||
pubkey: string;
|
||||
triggerElement?: "div" | "span";
|
||||
/** Accessible name for interactive trigger content that is visually hidden. */
|
||||
triggerAriaLabel?: string;
|
||||
/** Set false when the trigger is inside another interactive control. */
|
||||
enableProfilePanel?: boolean;
|
||||
/** When set to "bot", a BotIdenticon badge renders next to the display name. */
|
||||
@@ -169,6 +171,7 @@ export function UserProfilePopover({
|
||||
children,
|
||||
pubkey,
|
||||
triggerElement = "div",
|
||||
triggerAriaLabel,
|
||||
enableProfilePanel = true,
|
||||
role,
|
||||
botIdenticonValue,
|
||||
@@ -550,6 +553,7 @@ export function UserProfilePopover({
|
||||
<Popover onOpenChange={setOpen} open={open}>
|
||||
<PopoverAnchor asChild>
|
||||
<TriggerElement
|
||||
aria-label={triggerAriaLabel}
|
||||
role={canOpenProfilePanel ? "button" : undefined}
|
||||
tabIndex={canOpenProfilePanel ? 0 : undefined}
|
||||
onClick={handleTriggerClick}
|
||||
|
||||
@@ -92,8 +92,50 @@ async function measureThreadSummaryGeometry(summaryRow: Locator) {
|
||||
});
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await installMockBridge(page);
|
||||
test.beforeEach(async ({ page }, testInfo) => {
|
||||
const mock = testInfo.title.includes("agent owner label")
|
||||
? {
|
||||
searchProfiles: [
|
||||
{
|
||||
pubkey: TEST_IDENTITIES.alice.pubkey,
|
||||
displayName: "alice",
|
||||
ownerPubkey: TEST_IDENTITIES.bob.pubkey,
|
||||
isAgent: true,
|
||||
},
|
||||
{
|
||||
pubkey: TEST_IDENTITIES.bob.pubkey,
|
||||
displayName: "bob",
|
||||
},
|
||||
],
|
||||
}
|
||||
: undefined;
|
||||
await installMockBridge(page, mock);
|
||||
});
|
||||
|
||||
test("agent owner label identifies the agent and owner", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("channel-general").click();
|
||||
|
||||
const aliceMessage = page
|
||||
.getByTestId("message-row")
|
||||
.filter({ hasText: "Hey team — checking in." });
|
||||
const ownerTreatment = aliceMessage.getByTestId("message-agent-owner");
|
||||
|
||||
await expect(ownerTreatment.locator("svg")).toBeVisible();
|
||||
await expect(
|
||||
ownerTreatment.getByText("owned by", { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(ownerTreatment.locator(".font-semibold")).toHaveText("bob");
|
||||
await expect(ownerTreatment.getByRole("button")).toHaveAccessibleName("bob");
|
||||
await expect(ownerTreatment.locator(".sr-only")).toHaveText("Agent owned by");
|
||||
|
||||
const joinedRow = page
|
||||
.getByTestId("system-message-row")
|
||||
.filter({ hasText: "alice" })
|
||||
.filter({ hasText: "joined the channel" });
|
||||
await expect(joinedRow.getByTestId("message-agent-owner")).toContainText(
|
||||
"owned bybob",
|
||||
);
|
||||
});
|
||||
|
||||
test("send a message and see it in timeline", async ({ page }) => {
|
||||
|
||||
Reference in New Issue
Block a user