import { getCurrentWindow } from "@tauri-apps/api/window"; import * as React from "react"; import { Settings2 } from "lucide-react"; import { ChatHeader } from "@/features/chat/ui/ChatHeader"; import { useCreateChannelMutation, useChannelsQuery, useSelectedChannel, } from "@/features/channels/hooks"; import { useUnreadChannels } from "@/features/channels/useUnreadChannels"; import { ChannelManagementSheet } from "@/features/channels/ui/ChannelManagementSheet"; import { useHomeFeedQuery } from "@/features/home/hooks"; import { HomeView } from "@/features/home/ui/HomeView"; import { useChannelMessagesQuery, mergeMessages, useSendMessageMutation, useChannelSubscription, } from "@/features/messages/hooks"; import { collectMessageAuthorPubkeys, formatTimelineMessages, } from "@/features/messages/lib/formatTimelineMessages"; import { useProfileQuery, useUsersBatchQuery } from "@/features/profile/hooks"; import { MessageComposer } from "@/features/messages/ui/MessageComposer"; import { MessageTimeline } from "@/features/messages/ui/MessageTimeline"; import { SearchDialog } from "@/features/search/ui/SearchDialog"; import { SettingsView } from "@/features/settings/ui/SettingsView"; import { AppSidebar } from "@/features/sidebar/ui/AppSidebar"; import { getEventById } from "@/shared/api/tauri"; import { useIdentityQuery } from "@/shared/api/hooks"; import type { RelayEvent, SearchHit } from "@/shared/api/types"; import { Button } from "@/shared/ui/button"; import { SidebarInset, SidebarProvider, SidebarTrigger, } from "@/shared/ui/sidebar"; type AppView = "home" | "channel" | "settings"; function createSearchAnchorEvent(hit: SearchHit): RelayEvent { return { id: hit.eventId, pubkey: hit.pubkey, created_at: hit.createdAt, kind: hit.kind, tags: [["h", hit.channelId]], content: hit.content, sig: "", }; } export function AppShell() { const [selectedView, setSelectedView] = React.useState("home"); const [isChannelManagementOpen, setIsChannelManagementOpen] = React.useState(false); const [isSearchOpen, setIsSearchOpen] = React.useState(false); const [searchAnchor, setSearchAnchor] = React.useState( null, ); const [searchAnchorChannelId, setSearchAnchorChannelId] = React.useState< string | null >(null); const [searchAnchorEvent, setSearchAnchorEvent] = React.useState(null); const identityQuery = useIdentityQuery(); const profileQuery = useProfileQuery(); const homeFeedQuery = useHomeFeedQuery(); const channelsQuery = useChannelsQuery(); const channels = channelsQuery.data ?? []; const { selectedChannel, setSelectedChannelId } = useSelectedChannel( channels, null, ); const createChannelMutation = useCreateChannelMutation(); const activeChannel = selectedView === "channel" ? selectedChannel : null; const { unreadChannelIds } = useUnreadChannels(channels, activeChannel); const messagesQuery = useChannelMessagesQuery(activeChannel); useChannelSubscription(activeChannel); const sendMessageMutation = useSendMessageMutation( activeChannel, identityQuery.data, ); const homeUrgentCount = (homeFeedQuery.data?.feed.mentions.length ?? 0) + (homeFeedQuery.data?.feed.needsAction.length ?? 0); const availableChannelIds = React.useMemo( () => new Set(channels.map((channel) => channel.id)), [channels], ); const resolvedMessages = React.useMemo(() => { const currentMessages = messagesQuery.data ?? []; if ( !activeChannel || !searchAnchorEvent || searchAnchorChannelId !== activeChannel.id ) { return currentMessages; } return mergeMessages(currentMessages, searchAnchorEvent); }, [ activeChannel, messagesQuery.data, searchAnchorChannelId, searchAnchorEvent, ]); const messageAuthorPubkeys = React.useMemo( () => collectMessageAuthorPubkeys(resolvedMessages), [resolvedMessages], ); const messageProfilesQuery = useUsersBatchQuery(messageAuthorPubkeys, { enabled: resolvedMessages.length > 0, }); const timelineMessages = React.useMemo( () => formatTimelineMessages( resolvedMessages, activeChannel, identityQuery.data?.pubkey, profileQuery.data?.avatarUrl ?? null, messageProfilesQuery.data?.profiles, ), [ activeChannel, identityQuery.data?.pubkey, profileQuery.data?.avatarUrl, messageProfilesQuery.data?.profiles, resolvedMessages, ], ); const channelDescription = activeChannel ? [ activeChannel.archivedAt ? "Archived." : null, activeChannel.topic, activeChannel.description, activeChannel.purpose, activeChannel.channelType === "forum" ? "Forum channels are listed, but this first pass only wires message streams and DMs." : null, ] .filter((value) => value && value.trim().length > 0) .join(" ") || "Channel details and activity." : "Connect to the relay to browse channels and read messages."; const contentPaneKey = selectedView === "home" ? "home" : selectedView === "settings" ? "settings" : `channel:${activeChannel?.id ?? "none"}`; const isTimelineLoading = messagesQuery.isLoading && resolvedMessages.length === 0; const handleOpenChannel = React.useCallback( (channelId: string) => { React.startTransition(() => { setSelectedChannelId(channelId); setSelectedView("channel"); }); }, [setSelectedChannelId], ); const handleOpenSettings = React.useCallback(() => { setIsSearchOpen(false); setIsChannelManagementOpen(false); React.startTransition(() => { setSelectedView("settings"); }); }, []); const handleOpenSearchResult = React.useCallback( (hit: SearchHit) => { setSearchAnchor(hit); setSearchAnchorChannelId(hit.channelId); setSearchAnchorEvent(createSearchAnchorEvent(hit)); handleOpenChannel(hit.channelId); void getEventById(hit.eventId) .then((event) => { setSearchAnchorEvent((current) => { if (current?.id !== hit.eventId) { return current; } return event; }); }) .catch((error) => { console.error( "Failed to load search result event", hit.eventId, error, ); }); }, [handleOpenChannel], ); React.useEffect(() => { function handleKeyDown(event: KeyboardEvent) { const isSettingsShortcut = (event.key === "," || event.code === "Comma") && (event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey; if (!isSettingsShortcut) { return; } event.preventDefault(); handleOpenSettings(); } window.addEventListener("keydown", handleKeyDown); return () => { window.removeEventListener("keydown", handleKeyDown); }; }, [handleOpenSettings]); return ( { const createdChannel = await createChannelMutation.mutateAsync({ name, description, channelType: "stream", visibility: "open", }); React.startTransition(() => { setSelectedChannelId(createdChannel.id); setSelectedView("channel"); }); }} onOpenSearch={() => { setIsSearchOpen(true); }} onSelectHome={() => { React.startTransition(() => { setSelectedView("home"); }); void homeFeedQuery.refetch(); }} onSelectChannel={handleOpenChannel} onSelectSettings={handleOpenSettings} selectedChannelId={selectedChannel?.id ?? null} selectedView={selectedView} unreadChannelIds={unreadChannelIds} /> {/* Drag strip covering the traffic-light inset area */}
{ if (e.button !== 0) return; const target = e.target as HTMLElement; if (target.closest('button, a, input, [role="button"]')) return; e.preventDefault(); getCurrentWindow().startDragging(); }} /> {selectedView === "home" ? ( ) : selectedView === "settings" ? ( ) : ( { setIsChannelManagementOpen(true); }} size="sm" type="button" variant="outline" > Manage ) : null } channelType={activeChannel?.channelType} description={channelDescription} title={activeChannel?.name ?? "Channels"} /> )}
{selectedView === "home" ? ( { void homeFeedQuery.refetch(); }} /> ) : selectedView === "settings" ? ( ) : ( <> { setSearchAnchor((current) => current?.eventId === messageId ? null : current, ); }} targetMessageId={ activeChannel && searchAnchor?.channelId === activeChannel.id ? searchAnchor.eventId : null } /> { await sendMessageMutation.mutateAsync(content); }} placeholder={ activeChannel?.archivedAt ? "Archived channels are read-only." : activeChannel?.channelType === "forum" ? "Forum posting is not wired in this pass." : activeChannel ? `Message #${activeChannel.name}` : "Select a channel" } /> )}
{ React.startTransition(() => { setIsChannelManagementOpen(false); setSelectedView("home"); }); }} onOpenChange={setIsChannelManagementOpen} open={isChannelManagementOpen && activeChannel !== null} /> ); }