0 ? "-ml-2" : ""}
data-testid="message-thread-summary-participant"
style={{
- zIndex: 10 - index,
- ...(index > 0 && {
- mask: "radial-gradient(circle 16px at -4px 50%, transparent 99%, #fff 100%)",
+ zIndex: index + 1,
+ ...(index < participantCount - 1 && {
+ mask: "radial-gradient(circle 16px at calc(100% + 4px) 50%, transparent 99%, #fff 100%)",
WebkitMask:
- "radial-gradient(circle 16px at -4px 50%, transparent 99%, #fff 100%)",
+ "radial-gradient(circle 16px at calc(100% + 4px) 50%, transparent 99%, #fff 100%)",
}),
}}
>
@@ -44,11 +46,13 @@ export function MessageThreadSummaryRow({
depth = 0,
message,
onOpenThread,
+ showDepthGuides = true,
summary,
}: {
depth?: number;
message: TimelineMessage;
onOpenThread: (message: TimelineMessage) => void;
+ showDepthGuides?: boolean;
summary: TimelineThreadSummary;
}) {
const visibleDepth = Math.min(Math.max(depth, 0), 6);
@@ -74,7 +78,7 @@ export function MessageThreadSummaryRow({
return (
- {depthGuideOffsets.length > 0 ? (
+ {showDepthGuides && depthGuideOffsets.length > 0 ? (
))}
diff --git a/desktop/src/features/messages/ui/MessageTimeline.tsx b/desktop/src/features/messages/ui/MessageTimeline.tsx
index 1fa347a20..b20073754 100644
--- a/desktop/src/features/messages/ui/MessageTimeline.tsx
+++ b/desktop/src/features/messages/ui/MessageTimeline.tsx
@@ -1,15 +1,16 @@
import * as React from "react";
import { ArrowDown, Hash } from "lucide-react";
+import { getDmParticipantPreview } from "@/features/channels/lib/dmParticipantDisplay";
import type { TimelineMessage } from "@/features/messages/types";
import type { UserProfileLookup } from "@/features/profile/lib/identity";
import type { ChannelType } from "@/shared/api/types";
-import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar";
import { cn } from "@/shared/lib/cn";
import { channelChrome } from "@/shared/layout/chromeLayout";
import { Button } from "@/shared/ui/button";
import { Spinner } from "@/shared/ui/spinner";
import { TooltipProvider } from "@/shared/ui/tooltip";
+import { UserAvatar } from "@/shared/ui/UserAvatar";
import { TimelineSkeleton } from "./TimelineSkeleton";
import { TimelineMessageList } from "./TimelineMessageList";
import { useLoadOlderOnScroll } from "./useLoadOlderOnScroll";
@@ -23,8 +24,8 @@ type MessageTimelineProps = {
channelType?: ChannelType | null;
messages: TimelineMessage[];
directMessageIntro?: {
- avatarUrl: string | null;
displayName: string;
+ participants: DirectMessageIntroParticipant[];
} | null;
isLoading?: boolean;
emptyTitle?: string;
@@ -88,6 +89,12 @@ type ChannelIntro = {
icon?: React.ReactNode;
};
+type DirectMessageIntroParticipant = {
+ avatarUrl: string | null;
+ displayName: string;
+ pubkey: string;
+};
+
export const MessageTimeline = React.memo(function MessageTimeline({
agentPubkeys,
channelId,
@@ -229,12 +236,8 @@ export const MessageTimeline = React.memo(function MessageTimeline({
className="mb-0.5 mt-auto flex w-full flex-col items-start px-3 py-2 text-left"
data-testid="message-dm-intro"
>
-
{directMessageIntro.displayName}
@@ -420,3 +423,55 @@ export const MessageTimeline = React.memo(function MessageTimeline({
);
});
+
+function DirectMessageIntroAvatarStack({
+ participants,
+}: {
+ participants: DirectMessageIntroParticipant[];
+}) {
+ const { hiddenCount, visibleParticipants } =
+ getDmParticipantPreview(participants);
+ const stackItemCount = visibleParticipants.length + (hiddenCount > 0 ? 1 : 0);
+
+ return (
+
+ {visibleParticipants.map((participant, index) => (
+
0 ? "-ml-5" : ""}
+ data-testid="message-dm-intro-avatar-stack-participant"
+ key={participant.pubkey}
+ style={{
+ zIndex: index + 1,
+ ...(index < stackItemCount - 1 && {
+ mask: "radial-gradient(circle 34px at calc(100% + 10px) 50%, transparent 99%, #fff 100%)",
+ WebkitMask:
+ "radial-gradient(circle 34px at calc(100% + 10px) 50%, transparent 99%, #fff 100%)",
+ }),
+ }}
+ >
+
+
+ ))}
+ {hiddenCount > 0 ? (
+
0 ? "-ml-5" : ""}
+ data-testid="message-dm-intro-avatar-stack-more"
+ style={{ zIndex: stackItemCount }}
+ >
+
+ +{hiddenCount}
+
+
+ ) : null}
+
+ );
+}
diff --git a/desktop/src/features/messages/ui/TimelineMessageList.tsx b/desktop/src/features/messages/ui/TimelineMessageList.tsx
index a649fa357..a298d7e33 100644
--- a/desktop/src/features/messages/ui/TimelineMessageList.tsx
+++ b/desktop/src/features/messages/ui/TimelineMessageList.tsx
@@ -265,12 +265,14 @@ export const TimelineMessageList = React.memo(function TimelineMessageList({
: undefined
}
profiles={profiles}
+ showDepthGuides={false}
videoReviewContext={videoReviewContextById.get(message.id)}
/>
{footer}
@@ -303,6 +305,7 @@ export const TimelineMessageList = React.memo(function TimelineMessageList({
onReply={onReply}
profiles={profiles}
searchQuery={isSearchMatch ? searchQuery : undefined}
+ showDepthGuides={false}
videoReviewContext={videoReviewContextById.get(message.id)}
/>
{footer}
diff --git a/desktop/src/features/sidebar/lib/channelLabels.ts b/desktop/src/features/sidebar/lib/channelLabels.ts
index 893e4f318..8408c34d8 100644
--- a/desktop/src/features/sidebar/lib/channelLabels.ts
+++ b/desktop/src/features/sidebar/lib/channelLabels.ts
@@ -2,6 +2,7 @@ import {
resolveUserLabel,
type UserProfileLookup,
} from "@/features/profile/lib/identity";
+import { formatDmParticipantDisplayName } from "@/features/channels/lib/dmParticipantDisplay";
import type { Channel } from "@/shared/api/types";
function isGenericDmChannelName(name: string) {
@@ -46,5 +47,9 @@ export function resolveChannelDisplayLabel(
);
const uniqueLabels = [...new Set(resolvedLabels)];
- return uniqueLabels.length > 0 ? uniqueLabels.join(", ") : channel.name;
+ return uniqueLabels.length > 0
+ ? formatDmParticipantDisplayName(
+ uniqueLabels.map((displayName) => ({ displayName })),
+ )
+ : channel.name;
}
diff --git a/desktop/src/features/sidebar/ui/AppSidebar.tsx b/desktop/src/features/sidebar/ui/AppSidebar.tsx
index 00e8af524..458d36836 100644
--- a/desktop/src/features/sidebar/ui/AppSidebar.tsx
+++ b/desktop/src/features/sidebar/ui/AppSidebar.tsx
@@ -6,7 +6,7 @@ import {
Bot,
FolderGit2,
Home,
- PenSquare,
+ MessageCirclePlus,
Zap,
} from "lucide-react";
import { useReconnectRelay } from "@/shared/api/useReconnectRelay";
@@ -43,7 +43,7 @@ import {
import { CreateChannelDialog } from "@/features/sidebar/ui/CreateChannelDialog";
import { NewDirectMessageDialog } from "@/features/sidebar/ui/NewDirectMessageDialog";
import { SidebarProfileCard } from "@/features/sidebar/ui/SidebarProfileCard";
-import { SECTION_ACTION_VISIBILITY_CLASS } from "@/features/sidebar/ui/sidebarSectionStyles";
+import { SECTION_ICON_BUTTON_CLASS } from "@/features/sidebar/ui/sidebarSectionStyles";
import type {
Channel,
ChannelVisibility,
@@ -51,13 +51,11 @@ import type {
Profile,
UserStatus,
} from "@/shared/api/types";
-import { cn } from "@/shared/lib/cn";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
- SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
@@ -492,10 +490,10 @@ export function AppSidebar({
{shouldShowAgentCount ? (
- {totalAgentCount}
+ {totalAgentCount}
) : null}
@@ -688,21 +686,21 @@ export function AppSidebar({
{
- setIsNewDmOpen(true);
- }}
- type="button"
- >
-
-
+
+
+
}
dmParticipantsByChannelId={dmParticipantsByChannelId}
isCollapsed={collapsedGroups.directMessages}
diff --git a/desktop/src/features/sidebar/ui/NewDirectMessageDialog.tsx b/desktop/src/features/sidebar/ui/NewDirectMessageDialog.tsx
index 70b34d6a3..2a90dc51d 100644
--- a/desktop/src/features/sidebar/ui/NewDirectMessageDialog.tsx
+++ b/desktop/src/features/sidebar/ui/NewDirectMessageDialog.tsx
@@ -1,20 +1,38 @@
-import { Search, X } from "lucide-react";
+import { Bot, Search, X } from "lucide-react";
import * as React from "react";
+import {
+ useManagedAgentsQuery,
+ useRelayAgentsQuery,
+} from "@/features/agents/hooks";
import { useIsArchivedPredicate } from "@/features/identity-archive/hooks";
import { useUserSearchQuery } from "@/features/profile/hooks";
import { truncatePubkey } from "@/features/profile/lib/identity";
import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar";
import type { UserSearchResult } from "@/shared/api/types";
+import { normalizePubkey } from "@/shared/lib/pubkey";
import { Button } from "@/shared/ui/button";
import {
Dialog,
+ DialogClose,
DialogContent,
- DialogDescription,
DialogHeader,
DialogTitle,
} from "@/shared/ui/dialog";
-import { Input } from "@/shared/ui/input";
+
+const NEW_DM_SEARCH_SHELL_CLASS =
+ "group/search mt-4 flex cursor-text items-center gap-3 rounded-xl border border-input bg-background px-3 py-2.5 transition-colors duration-150 ease-out hover:border-muted-foreground/40 hover:bg-muted/70 focus-within:border-muted-foreground/50 focus-within:bg-muted/70";
+const NEW_DM_SEARCH_INPUT_CLASS =
+ "block h-6 min-w-0 flex-1 border-0 bg-transparent p-0 text-sm leading-5 text-muted-foreground/55 shadow-none caret-foreground outline-none transition-colors duration-150 ease-out placeholder:text-muted-foreground/55 group-hover/search:text-muted-foreground group-hover/search:placeholder:text-muted-foreground group-focus-within/search:text-foreground group-focus-within/search:placeholder:text-foreground";
+const DIRECT_MESSAGE_RECIPIENT_LIMIT = 50;
+const BUTTON_LABEL_MORPH_DURATION_MS = 220;
+const BUTTON_LABEL_MORPH_EASE = "cubic-bezier(0.23, 1, 0.32, 1)";
+const BUTTON_LABEL_FADE_MS = Math.min(
+ BUTTON_LABEL_MORPH_DURATION_MS * 0.5,
+ 150,
+);
+const BUTTON_LABEL_EXIT_ATTR = "data-button-label-exiting";
+const BUTTON_LABEL_CURRENT_ATTR = "data-button-label-current";
function formatUserName(user: UserSearchResult) {
return (
@@ -24,15 +42,213 @@ function formatUserName(user: UserSearchResult) {
);
}
-function formatUserSecondary(user: UserSearchResult) {
- const displayName = user.displayName?.trim();
- const nip05Handle = user.nip05Handle?.trim();
+function scoreRecipientMatch(user: UserSearchResult, query: string) {
+ if (query.length === 0) {
+ return 0;
+ }
+
+ const labels = [
+ formatUserName(user),
+ user.nip05Handle?.trim() ?? "",
+ user.isAgent ? "agent" : "",
+ ];
- if (displayName && nip05Handle) {
- return nip05Handle;
+ for (const label of labels) {
+ const lower = label.toLowerCase();
+ if (lower.startsWith(query)) return 0;
+ if (lower.split(/[\s\-_]+/).some((word) => word.startsWith(query))) {
+ return 1;
+ }
+ if (lower.includes(query)) return 2;
}
- return truncatePubkey(user.pubkey);
+ const pubkey = normalizePubkey(user.pubkey);
+ if (pubkey.startsWith(query)) return 3;
+ if (pubkey.includes(query)) return 4;
+
+ return null;
+}
+
+function prefersReducedMotion() {
+ return (
+ typeof window !== "undefined" &&
+ window.matchMedia("(prefers-reduced-motion: reduce)").matches
+ );
+}
+
+function createButtonLabelSpan(text: string) {
+ const span = document.createElement("span");
+ span.setAttribute(BUTTON_LABEL_CURRENT_ATTR, "");
+ span.textContent = text;
+ span.style.display = "inline-block";
+ span.style.willChange = "opacity, transform";
+ return span;
+}
+
+function clearButtonLabelRoot(root: HTMLElement, text: string) {
+ root.replaceChildren(createButtonLabelSpan(text));
+ root.style.width = "auto";
+ root.style.height = "auto";
+}
+
+function MorphingButtonLabel({ text }: { text: string }) {
+ const rootRef = React.useRef(null);
+ const currentTextRef = React.useRef("");
+ const cleanupSizeTransitionRef = React.useRef<(() => void) | null>(null);
+
+ React.useLayoutEffect(() => {
+ const root = rootRef.current;
+ if (!root || text === currentTextRef.current) {
+ return;
+ }
+ const morphRoot = root;
+
+ cleanupSizeTransitionRef.current?.();
+ cleanupSizeTransitionRef.current = null;
+
+ if (!currentTextRef.current || prefersReducedMotion()) {
+ clearButtonLabelRoot(root, text);
+ currentTextRef.current = text;
+ return;
+ }
+
+ root.querySelectorAll(`[${BUTTON_LABEL_EXIT_ATTR}]`).forEach((element) => {
+ element.remove();
+ });
+
+ const oldRect = root.getBoundingClientRect();
+ const oldWidth = oldRect.width;
+ const oldHeight = oldRect.height;
+ const rootRect = root.getBoundingClientRect();
+ const currentChild = root.querySelector(
+ `[${BUTTON_LABEL_CURRENT_ATTR}]`,
+ );
+
+ if (!currentChild || oldWidth === 0 || oldHeight === 0) {
+ clearButtonLabelRoot(root, text);
+ currentTextRef.current = text;
+ return;
+ }
+
+ const currentChildRect = currentChild.getBoundingClientRect();
+ const currentChildOpacity =
+ Number(getComputedStyle(currentChild).opacity) || 1;
+ currentChild.getAnimations().forEach((animation) => {
+ animation.cancel();
+ });
+ currentChild.removeAttribute(BUTTON_LABEL_CURRENT_ATTR);
+ currentChild.setAttribute(BUTTON_LABEL_EXIT_ATTR, "");
+ currentChild.style.position = "absolute";
+ currentChild.style.pointerEvents = "none";
+ currentChild.style.left = `${currentChildRect.left - rootRect.left}px`;
+ currentChild.style.top = `${currentChildRect.top - rootRect.top}px`;
+ currentChild.style.width = `${currentChildRect.width}px`;
+ currentChild.style.height = `${currentChildRect.height}px`;
+ currentChild.style.opacity = String(currentChildOpacity);
+
+ const nextChild = createButtonLabelSpan(text);
+ root.appendChild(nextChild);
+
+ root.style.width = "auto";
+ root.style.height = "auto";
+ void root.offsetWidth;
+
+ const nextRect = root.getBoundingClientRect();
+
+ root.style.width = `${oldWidth}px`;
+ root.style.height = `${oldHeight}px`;
+ void root.offsetWidth;
+
+ root.style.width = `${nextRect.width}px`;
+ root.style.height = `${nextRect.height}px`;
+
+ function cleanupSizeTransition() {
+ morphRoot.removeEventListener("transitionend", handleTransitionEnd);
+ window.clearTimeout(fallbackTimer);
+ cleanupSizeTransitionRef.current = null;
+ if (currentTextRef.current === text) {
+ morphRoot.style.width = "auto";
+ morphRoot.style.height = "auto";
+ }
+ }
+
+ function handleTransitionEnd(event: TransitionEvent) {
+ if (event.target !== morphRoot) {
+ return;
+ }
+ if (event.propertyName !== "width" && event.propertyName !== "height") {
+ return;
+ }
+ cleanupSizeTransition();
+ }
+
+ root.addEventListener("transitionend", handleTransitionEnd);
+ const fallbackTimer = window.setTimeout(
+ cleanupSizeTransition,
+ BUTTON_LABEL_MORPH_DURATION_MS + 50,
+ );
+ cleanupSizeTransitionRef.current = () => {
+ root.removeEventListener("transitionend", handleTransitionEnd);
+ window.clearTimeout(fallbackTimer);
+ };
+
+ currentChild.animate(
+ [{ transform: "none" }, { transform: "scale(0.95)" }],
+ {
+ duration: BUTTON_LABEL_MORPH_DURATION_MS,
+ easing: BUTTON_LABEL_MORPH_EASE,
+ fill: "both",
+ },
+ );
+ const exitFade = currentChild.animate(
+ [{ opacity: currentChildOpacity }, { opacity: 0 }],
+ {
+ duration: Math.min(BUTTON_LABEL_MORPH_DURATION_MS * 0.25, 150),
+ easing: "linear",
+ fill: "both",
+ },
+ );
+ exitFade.onfinish = () => currentChild.remove();
+
+ nextChild.animate([{ transform: "scale(0.95)" }, { transform: "none" }], {
+ duration: BUTTON_LABEL_MORPH_DURATION_MS,
+ easing: BUTTON_LABEL_MORPH_EASE,
+ fill: "both",
+ });
+ nextChild.animate([{ opacity: 0 }, { opacity: 1 }], {
+ delay: Math.min(BUTTON_LABEL_MORPH_DURATION_MS * 0.25, 150),
+ duration: BUTTON_LABEL_FADE_MS,
+ easing: "linear",
+ fill: "both",
+ });
+
+ currentTextRef.current = text;
+ }, [text]);
+
+ React.useEffect(() => {
+ return () => {
+ cleanupSizeTransitionRef.current?.();
+ rootRef.current?.getAnimations({ subtree: true }).forEach((animation) => {
+ animation.cancel();
+ });
+ };
+ }, []);
+
+ return (
+ <>
+
+ {text}
+ >
+ );
}
export function NewDirectMessageDialog({
@@ -56,49 +272,181 @@ export function NewDirectMessageDialog({
string | null
>(null);
const searchInputRef = React.useRef(null);
+ const selectedRecipientsRef = React.useRef(null);
+ const [selectedRecipientsHeight, setSelectedRecipientsHeight] =
+ React.useState(0);
const deferredSearchQuery = React.useDeferredValue(searchQuery.trim());
+ const normalizedDeferredSearchQuery = deferredSearchQuery.toLowerCase();
const hasReachedRecipientLimit = selectedUsers.length >= 8;
const selectedPubkeys = React.useMemo(
- () => new Set(selectedUsers.map((user) => user.pubkey.toLowerCase())),
+ () => new Set(selectedUsers.map((user) => normalizePubkey(user.pubkey))),
[selectedUsers],
);
+ const managedAgentsQuery = useManagedAgentsQuery({ enabled: open });
+ const relayAgentsQuery = useRelayAgentsQuery({ enabled: open });
const userSearchQuery = useUserSearchQuery(deferredSearchQuery, {
- enabled:
- open && deferredSearchQuery.length > 0 && !hasReachedRecipientLimit,
- limit: 8,
+ allowEmpty: true,
+ enabled: open && !hasReachedRecipientLimit,
+ limit: DIRECT_MESSAGE_RECIPIENT_LIMIT,
});
const isArchivedDiscovery = useIsArchivedPredicate();
- const searchResults = React.useMemo(
- () =>
- (userSearchQuery.data ?? []).filter((user) => {
- const normalizedPubkey = user.pubkey.toLowerCase();
- return (
- normalizedPubkey !== currentPubkey?.toLowerCase() &&
- !selectedPubkeys.has(normalizedPubkey) &&
- !isArchivedDiscovery(user.pubkey)
- );
- }),
- [currentPubkey, isArchivedDiscovery, selectedPubkeys, userSearchQuery.data],
- );
+ const searchResults = React.useMemo(() => {
+ const candidatesByPubkey = new Map();
+ const currentPubkeyNormalized = currentPubkey
+ ? normalizePubkey(currentPubkey)
+ : null;
+ const eligibleAgentPubkeys = new Set([
+ ...(managedAgentsQuery.data ?? []).map((agent) =>
+ normalizePubkey(agent.pubkey),
+ ),
+ ...(relayAgentsQuery.data ?? [])
+ .filter((agent) => agent.respondTo === "anyone")
+ .map((agent) => normalizePubkey(agent.pubkey)),
+ ]);
+
+ const addCandidate = (candidate: UserSearchResult) => {
+ const pubkey = normalizePubkey(candidate.pubkey);
+
+ if (
+ pubkey === currentPubkeyNormalized ||
+ selectedPubkeys.has(pubkey) ||
+ isArchivedDiscovery(pubkey) ||
+ (candidate.isAgent && !eligibleAgentPubkeys.has(pubkey))
+ ) {
+ return;
+ }
+
+ const current = candidatesByPubkey.get(pubkey);
+ if (!current) {
+ candidatesByPubkey.set(pubkey, { ...candidate, pubkey });
+ return;
+ }
+
+ const candidateName = candidate.displayName?.trim() || null;
+ const currentName = current.displayName?.trim() || null;
+
+ candidatesByPubkey.set(pubkey, {
+ pubkey,
+ avatarUrl: current.avatarUrl ?? candidate.avatarUrl ?? null,
+ displayName:
+ candidate.isAgent && candidateName
+ ? candidateName
+ : current.isAgent
+ ? currentName
+ : (currentName ?? candidateName),
+ nip05Handle: current.nip05Handle ?? candidate.nip05Handle ?? null,
+ isAgent: current.isAgent || candidate.isAgent,
+ });
+ };
+
+ for (const user of userSearchQuery.data ?? []) {
+ addCandidate(user);
+ }
+
+ for (const agent of relayAgentsQuery.data ?? []) {
+ if (agent.respondTo !== "anyone") {
+ continue;
+ }
+
+ addCandidate({
+ pubkey: agent.pubkey,
+ displayName: agent.name,
+ avatarUrl: null,
+ nip05Handle: null,
+ isAgent: true,
+ });
+ }
+
+ for (const agent of managedAgentsQuery.data ?? []) {
+ addCandidate({
+ pubkey: agent.pubkey,
+ displayName: agent.name,
+ avatarUrl: null,
+ nip05Handle: null,
+ isAgent: true,
+ });
+ }
+
+ return [...candidatesByPubkey.values()]
+ .map((candidate, order) => ({
+ candidate,
+ order,
+ score: scoreRecipientMatch(candidate, normalizedDeferredSearchQuery),
+ }))
+ .filter(
+ (item): item is typeof item & { score: number } => item.score !== null,
+ )
+ .sort(
+ (left, right) =>
+ left.score - right.score ||
+ formatUserName(left.candidate).localeCompare(
+ formatUserName(right.candidate),
+ ) ||
+ left.order - right.order,
+ )
+ .slice(0, DIRECT_MESSAGE_RECIPIENT_LIMIT)
+ .map(({ candidate }) => candidate);
+ }, [
+ currentPubkey,
+ isArchivedDiscovery,
+ managedAgentsQuery.data,
+ normalizedDeferredSearchQuery,
+ relayAgentsQuery.data,
+ selectedPubkeys,
+ userSearchQuery.data,
+ ]);
+ const isDirectoryLoading =
+ userSearchQuery.isLoading ||
+ managedAgentsQuery.isLoading ||
+ relayAgentsQuery.isLoading;
React.useEffect(() => {
if (!open) {
setSearchQuery("");
setSelectedUsers([]);
setSubmitErrorMessage(null);
+ setSelectedRecipientsHeight(0);
return;
}
searchInputRef.current?.focus();
}, [open]);
+ React.useEffect(() => {
+ const node = selectedRecipientsRef.current;
+ if (!node) {
+ setSelectedRecipientsHeight(0);
+ return;
+ }
+
+ const updateHeight = () => {
+ setSelectedRecipientsHeight(
+ selectedUsers.length > 0 ? node.scrollHeight : 0,
+ );
+ };
+
+ const animationFrame = window.requestAnimationFrame(updateHeight);
+ const resizeObserver = new ResizeObserver(updateHeight);
+ resizeObserver.observe(node);
+
+ return () => {
+ window.cancelAnimationFrame(animationFrame);
+ resizeObserver.disconnect();
+ };
+ }, [selectedUsers.length]);
+
function handleSelectUser(user: UserSearchResult) {
if (hasReachedRecipientLimit) {
return;
}
setSelectedUsers((current) => {
- if (current.some((candidate) => candidate.pubkey === user.pubkey)) {
+ const pubkey = normalizePubkey(user.pubkey);
+ if (
+ current.some(
+ (candidate) => normalizePubkey(candidate.pubkey) === pubkey,
+ )
+ ) {
return current;
}
@@ -110,17 +458,56 @@ export function NewDirectMessageDialog({
return (