Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions desktop/src/features/channels/ui/ChannelPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export const ChannelPane = React.memo(function ChannelPane({
unfollowThreadById,
personaLookup,
profiles,
ownerProfiles,
openThreadHeadId,
shouldShowThreadSkeleton,
openAgentSessionChannelId,
Expand Down Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions desktop/src/features/channels/ui/ChannelPane.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ export type ChannelPaneProps = {
) => void;
personaLookup?: Map<string, string>;
profiles?: UserProfileLookup;
ownerProfiles?: UserProfileLookup;
openThreadHeadId: string | null;
shouldShowThreadSkeleton: boolean;
openAgentSessionChannelId: string | null;
Expand Down
6 changes: 6 additions & 0 deletions desktop/src/features/channels/ui/ChannelScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -393,13 +395,15 @@ export function ChannelScreen({
personaLookup,
respondToLookup,
relaySelfPubkey,
messageOwnerProfiles,
),
[
activeChannel,
channelMembers,
currentProfile?.avatarUrl,
currentPubkey,
messageProfiles,
messageOwnerProfiles,
personaLookup,
relaySelfPubkey,
respondToLookup,
Expand Down Expand Up @@ -437,6 +441,7 @@ export function ChannelScreen({
currentPubkey,
currentAvatarUrl: currentProfile?.avatarUrl ?? null,
profiles: messageProfiles,
ownerProfiles: messageOwnerProfiles,
members: channelMembers,
personaLookup,
respondToLookup,
Expand Down Expand Up @@ -934,6 +939,7 @@ export function ChannelScreen({
profilePanelView={profilePanelView}
personaLookup={personaLookup}
profiles={messageProfiles}
ownerProfiles={messageOwnerProfiles}
firstUnreadMessageId={firstUnreadMessageId}
unreadCount={unreadCount}
targetMessageId={mainTimelineTargetMessageId}
Expand Down
22 changes: 22 additions & 0 deletions desktop/src/features/channels/useMessageOwnerProfiles.ts
Original file line number Diff line number Diff line change
@@ -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;
}
3 changes: 3 additions & 0 deletions desktop/src/features/home/lib/inbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions desktop/src/features/home/lib/inboxViewHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
16 changes: 16 additions & 0 deletions desktop/src/features/home/ui/HomeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -477,6 +491,7 @@ export function HomeView({
undefined,
undefined,
relaySelfPubkey,
feedOwnerProfiles,
);

return timelineMessages.map((message) =>
Expand All @@ -491,6 +506,7 @@ export function HomeView({
channelMessages,
currentPubkey,
feedProfiles,
feedOwnerProfiles,
relaySelfPubkey,
selectedChannel,
selectedEventId,
Expand Down
7 changes: 7 additions & 0 deletions desktop/src/features/home/ui/InboxMessageRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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>
Expand Down
11 changes: 11 additions & 0 deletions desktop/src/features/messages/lib/formatTimelineMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
isBroadcastReply,
} from "@/features/messages/lib/threading";
import {
formatOwnerLabel,
resolveUserLabel,
type UserProfileLookup,
} from "@/features/profile/lib/identity";
Expand Down Expand Up @@ -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>();
Expand Down Expand Up @@ -423,13 +426,21 @@ 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,
createdAt: event.created_at,
pubkey: authorPubkey,
signerPubkey: normalizePubkey(event.pubkey),
author,
isAgent,
ownerPubkey,
ownerLabel: isAgent
? formatOwnerLabel(ownerPubkey, currentPubkey, ownerProfiles)
: null,
avatarUrl: getAuthorAvatarUrl({
authorPubkey,
currentPubkey,
Expand Down
6 changes: 6 additions & 0 deletions desktop/src/features/messages/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
50 changes: 50 additions & 0 deletions desktop/src/features/messages/ui/MessageAgentOwner.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
}
11 changes: 11 additions & 0 deletions desktop/src/features/messages/ui/MessageRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -545,6 +552,7 @@ export const MessageRow = React.memo(
) : (
authorNode
)}
{agentOwnerNode}
{inlineMetadataNode}
{message.personaDisplayName &&
message.personaDisplayName !== message.author ? (
Expand Down Expand Up @@ -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 &&
Expand Down
3 changes: 3 additions & 0 deletions desktop/src/features/messages/ui/MessageTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -170,6 +171,7 @@ const MessageTimelineBase = React.forwardRef<
messageFooters,
personaLookup,
profiles,
ownerProfiles,
onDelete,
onEdit,
onMarkUnread,
Expand Down Expand Up @@ -636,6 +638,7 @@ const MessageTimelineBase = React.forwardRef<
onAtBottomStateChange={handleVirtualizerAtBottomStateChange}
personaLookup={personaLookup}
profiles={profiles}
ownerProfiles={ownerProfiles}
searchActiveMessageId={searchActiveMessageId}
searchMatchingMessageIds={searchMatchingMessageIds}
searchQuery={searchQuery}
Expand Down
Loading
Loading