Files
buzz/desktop/src/features/sidebar/useDmSidebarMetadata.ts
Arjun Mahanti bc006f6708 feat: render agent avatars as squircles (#7106)
## Summary

- render every agent/AI identity as a 30% squircle across desktop and
mobile while keeping human avatars circular
- propagate agent identity through message, thread, profile, reaction,
member, DM, search, workflow, project, huddle, forum, pulse, and
agent-management surfaces
- preserve squircle geometry for fallbacks, focus/status treatments,
add-agent controls, and overlapping avatar outlines (`calc(30% + 2px)`
for the outer background)

### Related issue

None found. This change was requested and visually reviewed in the
originating Buzz thread.

### Testing

- `just desktop-test` — 5,799 passed
- `just mobile-test` — 2,008 passed
- pre-push gates passed at `0d59d77b120dcb90aac2f918e422c11c9fa5353b`:
desktop check, TypeScript typecheck, desktop full test suite, mobile
format/analyze and full test suite, Rust tests, Tauri checks, and
differential file-size gate
- deterministic desktop visual sweep covered channel messages/thread
summaries; thread, subthread, and sub-subthread depths; reactions and
reactor popovers; hover/full profiles; added-to-channel activity;
channel members/settings; agent library/team overlaps; agent creation;
mention autocomplete; and DM header/sidebar/settings

### UI evidence

The complete labeled visual matrix is available in the originating Buzz
review thread. GitHub-hosted copies will be added in a follow-up PR
comment using the repository screenshot script.

---------

Signed-off-by: Fizz <dae5f6af70b8695a8b83c8deae555f63be41630ec2b8cd493e41a439c9527dd8@buzz.block.builderlab.xyz>
Co-authored-by: Fizz <dae5f6af70b8695a8b83c8deae555f63be41630ec2b8cd493e41a439c9527dd8@buzz.block.builderlab.xyz>
2026-08-31 13:31:33 -04:00

151 lines
5.0 KiB
TypeScript

import * as React from "react";
import { usePresenceQuery } from "@/features/presence/hooks";
import { resolveUserLabel } from "@/features/profile/lib/identity";
import { useUsersBatchQuery } from "@/features/profile/hooks";
import { resolveChannelDisplayLabel } from "@/features/sidebar/lib/channelLabels";
import type { SidebarDmParticipant } from "@/features/sidebar/ui/SidebarSection";
import type { Channel, PresenceStatus } from "@/shared/api/types";
export function useDmSidebarMetadata({
currentPubkey,
directMessages,
fallbackDisplayName,
profileDisplayName,
enabled = true,
}: {
currentPubkey?: string;
directMessages: Channel[];
fallbackDisplayName?: string;
profileDisplayName?: string | null;
enabled?: boolean;
}) {
const selfDmLabels = React.useMemo(
() =>
new Set(
[profileDisplayName, fallbackDisplayName]
.map((value) => value?.trim().toLowerCase())
.filter((value): value is string => Boolean(value)),
),
[fallbackDisplayName, profileDisplayName],
);
const dmParticipantPubkeys = React.useMemo(
() =>
directMessages.flatMap((channel) =>
channel.participantPubkeys.filter((pubkey, index) => {
const normalizedPubkey = pubkey.toLowerCase();
if (normalizedPubkey === currentPubkey?.toLowerCase()) {
return false;
}
const participantLabel =
channel.participants[index]?.trim().toLowerCase() ?? null;
return !participantLabel || !selfDmLabels.has(participantLabel);
}),
),
[currentPubkey, directMessages, selfDmLabels],
);
const dmPresenceQuery = usePresenceQuery(dmParticipantPubkeys, {
enabled: enabled && directMessages.length > 0,
});
const dmProfilesQuery = useUsersBatchQuery(dmParticipantPubkeys, {
enabled: enabled && directMessages.length > 0,
});
const dmProfiles = dmProfilesQuery.data?.profiles;
const dmPresenceByChannelId = React.useMemo(
() =>
Object.fromEntries(
directMessages.map((channel) => {
const otherParticipantPubkey = channel.participantPubkeys.find(
(pubkey, index) => {
const normalizedPubkey = pubkey.toLowerCase();
if (normalizedPubkey === currentPubkey?.toLowerCase()) {
return false;
}
const participantLabel =
channel.participants[index]?.trim().toLowerCase() ?? null;
return !participantLabel || !selfDmLabels.has(participantLabel);
},
);
return [
channel.id,
otherParticipantPubkey
? (dmPresenceQuery.data?.[otherParticipantPubkey.toLowerCase()] ??
"offline")
: "offline",
];
}),
) satisfies Record<string, PresenceStatus>,
[currentPubkey, directMessages, dmPresenceQuery.data, selfDmLabels],
);
const dmChannelLabels = React.useMemo(
() =>
Object.fromEntries(
directMessages.map((channel) => [
channel.id,
resolveChannelDisplayLabel(
channel,
currentPubkey,
dmProfilesQuery.data?.profiles,
),
]),
),
[currentPubkey, directMessages, dmProfilesQuery.data],
);
const dmParticipantsByChannelId = React.useMemo(
() =>
Object.fromEntries(
directMessages.map((channel) => {
const participants = channel.participantPubkeys.map(
(pubkey, index) => ({
fallbackName: channel.participants[index] ?? null,
pubkey,
}),
);
const otherParticipants = participants.filter((participant) => {
if (
participant.pubkey.toLowerCase() === currentPubkey?.toLowerCase()
) {
return false;
}
const participantLabel =
participant.fallbackName?.trim().toLowerCase() ?? null;
return !participantLabel || !selfDmLabels.has(participantLabel);
});
const visibleParticipants =
otherParticipants.length > 0 ? otherParticipants : participants;
return [
channel.id,
visibleParticipants.map((participant) => ({
avatarUrl:
dmProfiles?.[participant.pubkey.toLowerCase()]?.avatarUrl ??
null,
label: resolveUserLabel({
currentPubkey,
fallbackName: participant.fallbackName,
profiles: dmProfiles,
pubkey: participant.pubkey,
}),
...(dmProfiles?.[participant.pubkey.toLowerCase()]?.isAgent ===
true
? { isAgent: true }
: {}),
pubkey: participant.pubkey,
})),
];
}),
) satisfies Record<string, SidebarDmParticipant[]>,
[currentPubkey, directMessages, dmProfiles, selfDmLabels],
);
return {
dmChannelLabels,
dmParticipantsByChannelId,
dmPresenceByChannelId,
};
}