diff --git a/desktop/src/features/channels/lib/dmParticipantDisplay.ts b/desktop/src/features/channels/lib/dmParticipantDisplay.ts new file mode 100644 index 00000000000..25f8bd89931 --- /dev/null +++ b/desktop/src/features/channels/lib/dmParticipantDisplay.ts @@ -0,0 +1,106 @@ +import { + resolveUserLabel, + type UserProfileLookup, +} from "@/features/profile/lib/identity"; +import type { Channel } from "@/shared/api/types"; +import { normalizePubkey } from "@/shared/lib/pubkey"; + +export const DM_PARTICIPANT_PREVIEW_LIMIT = 3; + +export type DmParticipantDisplay = { + displayName: string; +}; + +export type DirectMessageIntroParticipant = { + avatarUrl: string | null; + displayName: string; + pubkey: string; +}; + +export type DirectMessageIntro = { + displayName: string; + participants: DirectMessageIntroParticipant[]; +}; + +export function getDmParticipantPreview(participants: readonly T[]) { + const visibleParticipants = participants.slice( + 0, + DM_PARTICIPANT_PREVIEW_LIMIT, + ); + + return { + hiddenCount: Math.max( + 0, + participants.length - DM_PARTICIPANT_PREVIEW_LIMIT, + ), + visibleParticipants, + }; +} + +export function formatDmParticipantDisplayName( + participants: readonly DmParticipantDisplay[], +) { + const { hiddenCount, visibleParticipants } = + getDmParticipantPreview(participants); + const names = visibleParticipants.map( + (participant) => participant.displayName, + ); + + return hiddenCount > 0 + ? [...names, `+${hiddenCount} more`].join(", ") + : names.join(", "); +} + +export function buildDirectMessageIntro({ + channel, + currentPubkey, + profiles, +}: { + channel: Channel | null; + currentPubkey?: string; + profiles?: UserProfileLookup; +}): DirectMessageIntro | null { + if (channel?.channelType !== "dm") { + return null; + } + + const participants = channel.participantPubkeys.map((pubkey, index) => ({ + fallbackName: channel.participants[index] ?? null, + pubkey, + })); + const normalizedCurrentPubkey = currentPubkey + ? normalizePubkey(currentPubkey) + : null; + const otherParticipants = normalizedCurrentPubkey + ? participants.filter( + (participant) => + normalizePubkey(participant.pubkey) !== normalizedCurrentPubkey, + ) + : participants; + const displayParticipants = + otherParticipants.length > 0 ? otherParticipants : participants; + + if (displayParticipants.length === 0) { + return null; + } + + const introParticipants = displayParticipants.map((participant) => { + const profile = profiles?.[normalizePubkey(participant.pubkey)] ?? null; + + return { + avatarUrl: profile?.avatarUrl ?? null, + displayName: resolveUserLabel({ + currentPubkey, + fallbackName: participant.fallbackName, + profiles, + pubkey: participant.pubkey, + }), + pubkey: participant.pubkey, + }; + }); + + return { + displayName: formatDmParticipantDisplayName(introParticipants), + participants: introParticipants, + }; +} diff --git a/desktop/src/features/channels/ui/ChannelMemberInviteCard.tsx b/desktop/src/features/channels/ui/ChannelMemberInviteCard.tsx index 4a06ff83ec9..e787a319203 100644 --- a/desktop/src/features/channels/ui/ChannelMemberInviteCard.tsx +++ b/desktop/src/features/channels/ui/ChannelMemberInviteCard.tsx @@ -1,4 +1,4 @@ -import { ChevronDown, Search, UserPlus, X } from "lucide-react"; +import { Search, UserPlus, X } from "lucide-react"; import * as React from "react"; import { formatPubkey } from "@/features/channels/lib/memberUtils"; @@ -9,10 +9,8 @@ import type { ChannelMember, UserSearchResult, } from "@/shared/api/types"; -import { cn } from "@/shared/lib/cn"; import { Button } from "@/shared/ui/button"; import { Input } from "@/shared/ui/input"; -import { Textarea } from "@/shared/ui/textarea"; import { UserAvatar } from "@/shared/ui/UserAvatar"; function formatSearchUserName(user: UserSearchResult) { @@ -23,17 +21,6 @@ function formatSearchUserName(user: UserSearchResult) { ); } -function formatSearchUserSecondary(user: UserSearchResult) { - const displayName = user.displayName?.trim(); - const nip05Handle = user.nip05Handle?.trim(); - - if (displayName && nip05Handle) { - return nip05Handle; - } - - return formatPubkey(user.pubkey); -} - export function ChannelMemberInviteCard({ canAssignElevatedRoles, existingMembers, @@ -52,10 +39,7 @@ export function ChannelMemberInviteCard({ open: boolean; requestErrorMessage?: string | null; }) { - const [invitePubkeys, setInvitePubkeys] = React.useState(""); const [inviteQuery, setInviteQuery] = React.useState(""); - const [isDirectPubkeyEntryOpen, setIsDirectPubkeyEntryOpen] = - React.useState(false); const [selectedInvitees, setSelectedInvitees] = React.useState< UserSearchResult[] >([]); @@ -122,36 +106,17 @@ export function ChannelMemberInviteCard({ React.useEffect(() => { if (!open) { - setInvitePubkeys(""); setInviteQuery(""); - setIsDirectPubkeyEntryOpen(false); setSelectedInvitees([]); setSubmissionErrors([]); } }, [open]); - const parsedInvitePubkeys = React.useMemo( - () => - invitePubkeys - .split(/[\s,]+/) - .map((value) => value.trim()) - .filter((value) => value.length > 0), - [invitePubkeys], - ); - const inviteTargets = [ - ...new Set([ - ...selectedInvitees.map((invitee) => invitee.pubkey), - ...parsedInvitePubkeys, - ]), - ]; - const directEntryLabel = - parsedInvitePubkeys.length > 0 && !isDirectPubkeyEntryOpen - ? `Direct pubkey entry (${parsedInvitePubkeys.length} ready)` - : "Direct pubkey entry"; + const inviteTargets = selectedInvitees.map((invitee) => invitee.pubkey); return (
{ event.preventDefault(); void onSubmit({ @@ -166,13 +131,6 @@ export function ChannelMemberInviteCard({ (invitee) => !addedPubkeys.has(invitee.pubkey.toLowerCase()), ), ); - const remainingPubkeys = parsedInvitePubkeys - .filter((pubkey) => !addedPubkeys.has(pubkey.toLowerCase())) - .join("\n"); - setInvitePubkeys(remainingPubkeys); - if (remainingPubkeys.length > 0) { - setIsDirectPubkeyEntryOpen(true); - } setInviteQuery(""); setSubmissionErrors(result.errors); }); @@ -202,7 +160,7 @@ export function ChannelMemberInviteCard({ disabled={isPending} id="channel-management-search-users" onChange={(event) => setInviteQuery(event.target.value)} - placeholder="Search by name or NIP-05." + placeholder="Search people and agents" value={inviteQuery} /> @@ -265,14 +223,14 @@ export function ChannelMemberInviteCard({ displayName={formatSearchUserName(result)} size="xs" /> -
-

- {formatSearchUserName(result)} -

-

- {formatSearchUserSecondary(result)} -

-
+

+ {formatSearchUserName(result)} +

+ {result.isAgent ? ( + + agent + + ) : null} Add @@ -292,49 +250,6 @@ export function ChannelMemberInviteCard({

) : null} -
- - - {isDirectPubkeyEntryOpen ? ( -
- -

- For exact pubkeys when search is not the right fit. -

-