diff --git a/desktop/scripts/check-file-sizes.mjs b/desktop/scripts/check-file-sizes.mjs index 149b1e08c..ff880dc06 100644 --- a/desktop/scripts/check-file-sizes.mjs +++ b/desktop/scripts/check-file-sizes.mjs @@ -153,7 +153,10 @@ const overrides = new Map([ // importIdentity, persistCurrentIdentity) moved to tauriIdentity.ts; // limit ratcheted down 1380 → 1360 to bank the headroom (absorbs main-side // growth landed between the split and the rebase). - ["src/shared/api/tauri.ts", 1360], + // mention-alias fix: profile wrappers (RawProfile/RawUserProfileSummary types, + // getProfile/updateProfile/getUserProfile/getUsersBatch/searchUsers) moved to + // tauriProfiles.ts; limit ratcheted down 1360 → 1241 to bank the headroom. + ["src/shared/api/tauri.ts", 1241], // readiness-gate: PersonaDialog.tsx threads computeLocalModeGate + // requiredCredentialEnvKeys + RequiredFieldLabel so the "New agent" dialog // shows required markers and credential amber rows (parity with diff --git a/desktop/src-tauri/src/models.rs b/desktop/src-tauri/src/models.rs index 881cd11a3..8f002fdb1 100644 --- a/desktop/src-tauri/src/models.rs +++ b/desktop/src-tauri/src/models.rs @@ -37,6 +37,11 @@ pub struct ProfileInfo { #[derive(Serialize, Deserialize)] pub struct UserProfileSummaryInfo { pub display_name: Option, + /// Kind-0 `name` field, carried separately from `display_name` so clients + /// can match @mention text against either alias (agents and the CLI + /// resolve mentions server-side against `display_name` *or* `name`). + #[serde(default)] + pub name: Option, pub avatar_url: Option, pub nip05_handle: Option, pub owner_pubkey: Option, diff --git a/desktop/src-tauri/src/nostr_convert.rs b/desktop/src-tauri/src/nostr_convert.rs index c1db2a1b5..ec4970e0c 100644 --- a/desktop/src-tauri/src/nostr_convert.rs +++ b/desktop/src-tauri/src/nostr_convert.rs @@ -339,6 +339,7 @@ pub fn users_batch_from_events( .and_then(Value::as_str) .or_else(|| v.get("name").and_then(Value::as_str)) .map(str::to_string), + name: v.get("name").and_then(Value::as_str).map(str::to_string), avatar_url: v.get("picture").and_then(Value::as_str).map(str::to_string), nip05_handle: v.get("nip05").and_then(Value::as_str).map(str::to_string), is_agent: owner_pubkey.is_some(), diff --git a/desktop/src/features/channels/ui/ChannelScreen.tsx b/desktop/src/features/channels/ui/ChannelScreen.tsx index ac3eded07..5d1b0e7d6 100644 --- a/desktop/src/features/channels/ui/ChannelScreen.tsx +++ b/desktop/src/features/channels/ui/ChannelScreen.tsx @@ -14,10 +14,8 @@ import { } from "@/features/channels/readState/readStateFormat"; import { ChannelScreenEmptyState } from "@/features/channels/ui/ChannelScreenEmptyState"; import { ChannelScreenHeader } from "@/features/channels/ui/ChannelScreenHeader"; -import { - ChannelPane, - ForumView, -} from "@/features/channels/ui/ChannelScreenLazyViews"; +import { ChannelPane } from "@/features/channels/ui/ChannelScreenLazyViews"; +import { ForumChannelContent } from "@/features/channels/ui/ForumChannelContent"; import { MembersSidebar } from "@/features/channels/ui/MembersSidebar"; import { useManagedAgentsQuery, @@ -839,19 +837,27 @@ export function ChannelScreen({ > {activeChannel ? ( activeChannel.channelType === "forum" ? ( - <> - {channelHeader} - }> - - - + ) : ( } diff --git a/desktop/src/features/channels/ui/ChannelScreenLazyViews.ts b/desktop/src/features/channels/ui/ChannelScreenLazyViews.ts index 4e4fc0f2a..86619a3c5 100644 --- a/desktop/src/features/channels/ui/ChannelScreenLazyViews.ts +++ b/desktop/src/features/channels/ui/ChannelScreenLazyViews.ts @@ -9,3 +9,8 @@ export const ForumView = React.lazy(async () => { const module = await import("@/features/forum/ui/ForumView"); return { default: module.ForumView }; }); + +export const UserProfilePanel = React.lazy(async () => { + const module = await import("@/features/profile/ui/UserProfilePanel"); + return { default: module.UserProfilePanel }; +}); diff --git a/desktop/src/features/channels/ui/ForumChannelContent.tsx b/desktop/src/features/channels/ui/ForumChannelContent.tsx new file mode 100644 index 000000000..428413c59 --- /dev/null +++ b/desktop/src/features/channels/ui/ForumChannelContent.tsx @@ -0,0 +1,121 @@ +import * as React from "react"; + +import { + ForumView, + UserProfilePanel, +} from "@/features/channels/ui/ChannelScreenLazyViews"; +import { RightAuxiliaryPane } from "@/features/channels/ui/RightAuxiliaryPane"; +import type { + ProfilePanelTab, + ProfilePanelView, +} from "@/features/profile/ui/UserProfilePanelUtils"; +import type { Channel } from "@/shared/api/types"; +import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback"; + +type ForumChannelContentProps = { + canResetPanelWidth: boolean; + channel: Channel; + currentPubkey?: string; + header: React.ReactNode; + onClosePost: () => void; + onCloseProfilePanel: () => void; + onOpenDm?: (pubkeys: string[]) => Promise | void; + onOpenProfilePanel: (pubkey: string) => void; + onPanelResizeStart: (event: React.PointerEvent) => void; + onProfilePanelTabChange: ( + tab: ProfilePanelTab, + options?: { replace?: boolean }, + ) => void; + onProfilePanelViewChange: ( + view: ProfilePanelView, + options?: { replace?: boolean }, + ) => void; + onResetPanelWidth: () => void; + onSelectPost: (postId: string) => void; + panelWidthPx: number; + profilePanelPubkey?: string | null; + profilePanelTab: ProfilePanelTab; + profilePanelView: ProfilePanelView; + selectedPostId: string | null; + targetReplyId: string | null; +}; + +/** + * Forum-channel body for ChannelScreen: the post list/thread plus the + * user-profile auxiliary pane. Forums replace ChannelPane (which hosts the + * profile panel for message channels), so without this host, opening a + * profile from a mention chip, avatar, or the members sidebar would set + * state that never renders. + */ +export function ForumChannelContent({ + canResetPanelWidth, + channel, + currentPubkey, + header, + onClosePost, + onCloseProfilePanel, + onOpenDm, + onOpenProfilePanel, + onPanelResizeStart, + onProfilePanelTabChange, + onProfilePanelViewChange, + onResetPanelWidth, + onSelectPost, + panelWidthPx, + profilePanelPubkey, + profilePanelTab, + profilePanelView, + selectedPostId, + targetReplyId, +}: ForumChannelContentProps) { + return ( + <> + {header} +
+
+ }> + + +
+ {profilePanelPubkey ? ( + + + + + + ) : null} +
+ + ); +} diff --git a/desktop/src/features/channels/ui/MembersSidebar.tsx b/desktop/src/features/channels/ui/MembersSidebar.tsx index 314d28f41..48a04b591 100644 --- a/desktop/src/features/channels/ui/MembersSidebar.tsx +++ b/desktop/src/features/channels/ui/MembersSidebar.tsx @@ -511,18 +511,15 @@ export function MembersSidebar({ useFeedbackToasts(actionNoticeMessage, actionErrorMessage); const { openProfilePanel } = useProfilePanel(); - // UserProfilePanel only renders inside ChannelPane, which forums replace - // with ForumView — opening there would close the sheet and show nothing. - const isForumChannel = channel?.channelType === "forum"; const handleOpenProfile = React.useMemo( () => - openProfilePanel && !isForumChannel + openProfilePanel ? (pubkey: string) => { onOpenChange(false); openProfilePanel(pubkey); } : undefined, - [isForumChannel, onOpenChange, openProfilePanel], + [onOpenChange, openProfilePanel], ); const [editRespondToAgent, setEditRespondToAgent] = diff --git a/desktop/src/features/forum/ui/ForumPostCard.tsx b/desktop/src/features/forum/ui/ForumPostCard.tsx index 4bd5f0f08..311df0f97 100644 --- a/desktop/src/features/forum/ui/ForumPostCard.tsx +++ b/desktop/src/features/forum/ui/ForumPostCard.tsx @@ -10,7 +10,7 @@ import { UserAvatar } from "@/shared/ui/UserAvatar"; import type { ForumPost } from "@/shared/api/types"; import { cn } from "@/shared/lib/cn"; import { parseImetaTags } from "@/features/messages/lib/parseImeta"; -import { resolveMentionNames } from "@/shared/lib/resolveMentionNames"; +import { resolveMentionProps } from "@/shared/lib/resolveMentionNames"; import { Markdown } from "@/shared/ui/markdown"; import { formatRelativeTime } from "../lib/time"; @@ -44,7 +44,10 @@ export function ForumPostCard({ preferResolvedSelfLabel: true, }); const avatarUrl = profiles?.[post.pubkey.toLowerCase()]?.avatarUrl ?? null; - const mentionNames = resolveMentionNames(post.tags, profiles); + const { mentionNames, mentionPubkeysByName } = resolveMentionProps( + post.tags, + profiles, + ); // Memoize the imeta map: `parseImetaTags` builds a fresh object each render, // and the `Markdown` memo compares `imetaByUrl` by reference. Without this, // the post's Markdown (and the FileCard