2026-03-09 14:49:50 -07:00
|
|
|
import * as React from "react";
|
2026-03-12 12:53:59 -07:00
|
|
|
import { useQueryClient } from "@tanstack/react-query";
|
2026-04-07 12:20:42 -07:00
|
|
|
import { Outlet, useLocation } from "@tanstack/react-router";
|
2026-03-09 14:49:50 -07:00
|
|
|
|
2026-06-24 22:28:35 -04:00
|
|
|
import { deriveShellRoute } from "@/app/AppShell.helpers";
|
2026-04-07 12:20:42 -07:00
|
|
|
import { AppShellProvider } from "@/app/AppShellContext";
|
2026-04-06 09:19:47 -07:00
|
|
|
import {
|
|
|
|
|
AppShellOverlays,
|
|
|
|
|
type BrowseDialogType,
|
|
|
|
|
} from "@/app/AppShellOverlays";
|
2026-05-29 11:26:41 -04:00
|
|
|
import { AppTopChrome } from "@/app/AppTopChrome";
|
2026-04-07 12:20:42 -07:00
|
|
|
import { useAppNavigation } from "@/app/navigation/useAppNavigation";
|
|
|
|
|
import { useBackForwardControls } from "@/app/navigation/useBackForwardControls";
|
2026-06-22 19:48:32 +01:00
|
|
|
import { useLiveHomeFeedActions } from "@/app/useLiveHomeFeedActions";
|
2026-05-26 20:38:29 -04:00
|
|
|
import { useMarkAsReadShortcuts } from "@/app/useMarkAsReadShortcuts";
|
2026-06-04 11:48:57 -04:00
|
|
|
import { useSettingsShortcuts } from "@/app/useSettingsShortcuts";
|
2026-06-24 14:51:05 -07:00
|
|
|
import { useAppShellDesktopNotifications } from "@/app/useAppShellDesktopNotifications";
|
2026-06-22 19:48:32 +01:00
|
|
|
import { useThreadActivityFeedItems } from "@/app/useThreadActivityFeedItems";
|
2026-06-24 22:28:35 -04:00
|
|
|
import { useTauriWindowDrag } from "@/app/useTauriWindowDrag";
|
2026-04-06 09:19:47 -07:00
|
|
|
import { useWebviewZoomShortcuts } from "@/app/useWebviewZoomShortcuts";
|
2026-03-09 14:49:50 -07:00
|
|
|
import {
|
2026-03-12 12:53:59 -07:00
|
|
|
channelsQueryKey,
|
2026-04-06 09:19:47 -07:00
|
|
|
useChannelsQuery,
|
2026-03-09 17:33:13 -07:00
|
|
|
useCreateChannelMutation,
|
2026-03-22 09:00:45 -07:00
|
|
|
useHideDmMutation,
|
2026-03-18 16:01:58 -07:00
|
|
|
useOpenDmMutation,
|
2026-03-09 14:49:50 -07:00
|
|
|
} from "@/features/channels/hooks";
|
2026-03-11 11:11:06 -07:00
|
|
|
import { useUnreadChannels } from "@/features/channels/useUnreadChannels";
|
2026-06-22 22:25:10 -04:00
|
|
|
import { msgContextKey } from "@/features/channels/readState/readStateFormat";
|
2026-06-11 07:27:23 -07:00
|
|
|
import { useMembershipNotifications } from "@/features/channels/useMembershipNotifications";
|
2026-06-22 19:48:32 +01:00
|
|
|
import { useFeedItemState } from "@/features/home/useFeedItemState";
|
2026-05-28 18:43:41 -04:00
|
|
|
import { useThreadFollows } from "@/features/messages/lib/useThreadFollows";
|
2026-05-20 17:13:59 -04:00
|
|
|
import {
|
|
|
|
|
useHomeFeedNotifications,
|
|
|
|
|
useHomeFeedNotificationState,
|
|
|
|
|
} from "@/features/notifications/hooks";
|
2026-06-24 14:51:05 -07:00
|
|
|
import { setDesktopAppBadge } from "@/features/notifications/lib/desktop";
|
2026-05-05 14:14:00 -06:00
|
|
|
import { PreventSleepProvider } from "@/features/agents/usePreventSleep";
|
2026-06-23 22:31:06 +01:00
|
|
|
import { requestOpenCreateAgent } from "@/features/agents/openCreateAgentEvent";
|
2026-06-25 14:10:06 -04:00
|
|
|
import { useAgentsDataRefresh } from "@/features/agents/lib/useAgentsDataRefresh";
|
2026-06-24 18:28:25 -04:00
|
|
|
import { usePersonaSync } from "@/features/agents/lib/usePersonaSync";
|
2026-04-30 13:02:08 -04:00
|
|
|
import {
|
|
|
|
|
usePresenceSession,
|
|
|
|
|
usePresenceSubscription,
|
|
|
|
|
} from "@/features/presence/hooks";
|
2026-05-01 18:02:59 -06:00
|
|
|
import {
|
|
|
|
|
useSetUserStatusMutation,
|
|
|
|
|
useUserStatusQuery,
|
|
|
|
|
useUserStatusSubscription,
|
|
|
|
|
} from "@/features/user-status/hooks";
|
2026-06-02 14:19:07 -07:00
|
|
|
import { useWorkspaceEmojiLiveUpdates } from "@/features/custom-emoji/hooks";
|
2026-04-06 09:19:47 -07:00
|
|
|
import { useProfileQuery } from "@/features/profile/hooks";
|
2026-04-22 16:45:56 -06:00
|
|
|
import {
|
|
|
|
|
DEFAULT_SETTINGS_SECTION,
|
|
|
|
|
type SettingsSection,
|
2026-06-16 23:37:09 -07:00
|
|
|
isSettingsSection,
|
2026-04-22 16:45:56 -06:00
|
|
|
} from "@/features/settings/ui/SettingsPanels";
|
2026-04-14 13:11:09 -04:00
|
|
|
import { HuddleBar, HuddleProvider } from "@/features/huddle";
|
2026-06-23 10:46:18 -04:00
|
|
|
import { useDueReminderBadgeCount } from "@/features/reminders/hooks";
|
2026-06-17 11:03:45 -04:00
|
|
|
import { RemindMeLaterProvider } from "@/features/reminders/ui/RemindMeLaterProvider";
|
2026-06-17 17:47:41 -04:00
|
|
|
import { useReminderNotifications } from "@/features/reminders/useReminderNotifications";
|
2026-03-09 13:02:11 -07:00
|
|
|
import { AppSidebar } from "@/features/sidebar/ui/AppSidebar";
|
2026-06-04 15:53:53 -04:00
|
|
|
import { useChannelMutes } from "@/features/sidebar/lib/useChannelMutes";
|
2026-06-04 15:32:28 -07:00
|
|
|
import { useChannelStars } from "@/features/sidebar/lib/useChannelStars";
|
2026-04-27 22:47:11 -06:00
|
|
|
import { useWorkspaces } from "@/features/workspaces/useWorkspaces";
|
2026-05-11 12:31:25 -07:00
|
|
|
import { useApplyTemplate } from "@/features/channel-templates/useApplyTemplate";
|
2026-03-13 16:26:07 -07:00
|
|
|
import { relayClient } from "@/shared/api/relayClient";
|
2026-03-09 14:49:50 -07:00
|
|
|
import { useIdentityQuery } from "@/shared/api/hooks";
|
2026-06-12 14:00:41 -04:00
|
|
|
import { useRelayAutoHeal } from "@/shared/api/useRelayAutoHeal";
|
2026-04-18 11:06:55 -06:00
|
|
|
import { useDeferredStartup } from "@/shared/hooks/useDeferredStartup";
|
2026-04-07 12:20:42 -07:00
|
|
|
import { joinChannel } from "@/shared/api/tauri";
|
2026-06-24 14:51:05 -07:00
|
|
|
import type { SearchHit } from "@/shared/api/types";
|
2026-03-20 15:19:13 -07:00
|
|
|
import { ChannelNavigationProvider } from "@/shared/context/ChannelNavigationContext";
|
2026-06-11 07:27:23 -07:00
|
|
|
import { MainInsetProvider } from "@/shared/layout/MainInsetContext";
|
|
|
|
|
import { chromeCssVarDefaults } from "@/shared/layout/chromeLayout";
|
2026-06-15 21:58:12 +01:00
|
|
|
import { cn } from "@/shared/lib/cn";
|
2026-05-09 19:34:05 -07:00
|
|
|
import { hasPrimaryShortcutModifier } from "@/shared/lib/platform";
|
2026-05-27 12:27:34 -07:00
|
|
|
import { useMessageDeepLinks } from "@/shared/useMessageDeepLinks";
|
2026-06-12 14:00:41 -04:00
|
|
|
import { ConnectionBanner } from "@/shared/ui/ConnectionBanner";
|
2026-05-29 11:26:41 -04:00
|
|
|
import { SidebarInset, SidebarProvider } from "@/shared/ui/sidebar";
|
2026-03-09 13:02:11 -07:00
|
|
|
|
2026-04-08 11:13:14 -07:00
|
|
|
const LazySettingsScreen = React.lazy(async () => {
|
|
|
|
|
const module = await import("@/features/settings/ui/SettingsScreen");
|
|
|
|
|
return { default: module.SettingsScreen };
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-09 13:02:11 -07:00
|
|
|
export function AppShell() {
|
2026-03-25 10:28:23 -07:00
|
|
|
useWebviewZoomShortcuts();
|
2026-06-24 22:28:35 -04:00
|
|
|
useTauriWindowDrag();
|
|
|
|
|
|
2026-04-27 22:47:11 -06:00
|
|
|
const workspacesHook = useWorkspaces();
|
|
|
|
|
const [isAddWorkspaceOpen, setIsAddWorkspaceOpen] = React.useState(false);
|
2026-03-10 17:11:49 -07:00
|
|
|
const [isChannelManagementOpen, setIsChannelManagementOpen] =
|
|
|
|
|
React.useState(false);
|
2026-06-24 22:28:35 -04:00
|
|
|
const [managedChannelId, setManagedChannelId] = React.useState<string | null>(
|
|
|
|
|
null,
|
|
|
|
|
);
|
2026-05-29 11:26:41 -04:00
|
|
|
const [searchFocusRequest, setSearchFocusRequest] = React.useState(0);
|
2026-04-06 09:19:47 -07:00
|
|
|
const [browseDialogType, setBrowseDialogType] =
|
|
|
|
|
React.useState<BrowseDialogType>(null);
|
2026-04-22 16:45:56 -06:00
|
|
|
const [isNewDmOpen, setIsNewDmOpen] = React.useState(false);
|
2026-06-01 09:12:45 -07:00
|
|
|
const [isCreateChannelOpen, setIsCreateChannelOpen] = React.useState(false);
|
2026-06-15 21:58:12 +01:00
|
|
|
const [isHuddleDrawerOpen, setIsHuddleDrawerOpen] = React.useState(false);
|
2026-06-11 07:27:23 -07:00
|
|
|
const mainInsetRef = React.useRef<HTMLElement>(null);
|
2026-04-07 12:20:42 -07:00
|
|
|
const location = useLocation();
|
2026-03-12 12:53:59 -07:00
|
|
|
const queryClient = useQueryClient();
|
2026-05-12 10:51:09 -07:00
|
|
|
const {
|
|
|
|
|
goAgents,
|
|
|
|
|
goChannel,
|
|
|
|
|
goHome,
|
|
|
|
|
goProjects,
|
|
|
|
|
goPulse,
|
2026-06-16 23:37:09 -07:00
|
|
|
goSettings,
|
2026-05-12 10:51:09 -07:00
|
|
|
goWorkflows,
|
2026-06-16 23:37:09 -07:00
|
|
|
closeSettings,
|
2026-05-12 10:51:09 -07:00
|
|
|
openSearchHit,
|
|
|
|
|
} = useAppNavigation();
|
2026-04-07 12:20:42 -07:00
|
|
|
const { canGoBack, canGoForward, goBack, goForward } =
|
|
|
|
|
useBackForwardControls();
|
|
|
|
|
const { selectedChannelId, selectedView } = React.useMemo(
|
|
|
|
|
() => deriveShellRoute(location.pathname),
|
|
|
|
|
[location.pathname],
|
|
|
|
|
);
|
2026-06-24 22:28:35 -04:00
|
|
|
// Settings lives in history so back returns to the previous app entry.
|
2026-06-16 23:37:09 -07:00
|
|
|
const settingsOpen = location.pathname === "/settings";
|
|
|
|
|
const locationSearchSection = (location.search as { section?: unknown })
|
|
|
|
|
.section;
|
|
|
|
|
const settingsSection: SettingsSection = isSettingsSection(
|
|
|
|
|
locationSearchSection,
|
|
|
|
|
)
|
|
|
|
|
? locationSearchSection
|
|
|
|
|
: DEFAULT_SETTINGS_SECTION;
|
2026-04-18 11:06:55 -06:00
|
|
|
const startupReady = useDeferredStartup();
|
|
|
|
|
|
2026-03-09 14:49:50 -07:00
|
|
|
const identityQuery = useIdentityQuery();
|
2026-06-04 15:53:53 -04:00
|
|
|
const { mutedChannelIds, muteChannel, unmuteChannel } = useChannelMutes(
|
|
|
|
|
identityQuery.data?.pubkey,
|
|
|
|
|
);
|
2026-06-04 15:32:28 -07:00
|
|
|
const { starredChannelIds, starChannel, unstarChannel } = useChannelStars(
|
|
|
|
|
identityQuery.data?.pubkey,
|
|
|
|
|
);
|
2026-06-24 18:28:25 -04:00
|
|
|
usePersonaSync(identityQuery.data?.pubkey);
|
2026-06-25 14:10:06 -04:00
|
|
|
useAgentsDataRefresh();
|
2026-03-11 12:09:09 -07:00
|
|
|
const profileQuery = useProfileQuery();
|
2026-04-18 11:06:55 -06:00
|
|
|
const deferredPubkey = startupReady ? identityQuery.data?.pubkey : undefined;
|
2026-06-12 14:00:41 -04:00
|
|
|
useRelayAutoHeal();
|
2026-04-30 13:02:08 -04:00
|
|
|
usePresenceSubscription();
|
2026-05-01 18:02:59 -06:00
|
|
|
useUserStatusSubscription();
|
2026-06-02 14:19:07 -07:00
|
|
|
useWorkspaceEmojiLiveUpdates();
|
2026-06-10 17:40:53 +01:00
|
|
|
useMembershipNotifications(identityQuery.data?.pubkey);
|
2026-04-18 11:06:55 -06:00
|
|
|
const presenceSession = usePresenceSession(deferredPubkey);
|
2026-05-01 18:02:59 -06:00
|
|
|
const selfStatusQuery = useUserStatusQuery(
|
|
|
|
|
deferredPubkey ? [deferredPubkey] : [],
|
|
|
|
|
);
|
|
|
|
|
const setUserStatusMutation = useSetUserStatusMutation(deferredPubkey);
|
2026-05-20 17:13:59 -04:00
|
|
|
const { feedProfilesQuery, homeFeedQuery, notificationSettings } =
|
|
|
|
|
useHomeFeedNotifications(identityQuery.data?.pubkey);
|
2026-06-22 19:48:32 +01:00
|
|
|
const feedItemState = useFeedItemState(identityQuery.data?.pubkey);
|
2026-06-25 13:07:04 -04:00
|
|
|
const channelsQuery = useChannelsQuery();
|
|
|
|
|
const channels = channelsQuery.data ?? [];
|
2026-06-17 17:47:41 -04:00
|
|
|
useReminderNotifications(
|
|
|
|
|
identityQuery.data?.pubkey,
|
|
|
|
|
notificationSettings.settings,
|
2026-06-25 13:07:04 -04:00
|
|
|
channels,
|
2026-06-17 17:47:41 -04:00
|
|
|
);
|
2026-06-22 19:48:32 +01:00
|
|
|
const refetchHomeFeedFromLiveSignal = React.useEffectEvent(() => {
|
2026-04-03 18:49:43 -07:00
|
|
|
void homeFeedQuery.refetch();
|
|
|
|
|
});
|
2026-06-22 19:48:32 +01:00
|
|
|
useLiveHomeFeedActions(
|
|
|
|
|
identityQuery.data?.pubkey,
|
|
|
|
|
refetchHomeFeedFromLiveSignal,
|
|
|
|
|
);
|
2026-03-12 12:53:59 -07:00
|
|
|
const { refetch: refetchChannels } = channelsQuery;
|
2026-06-17 18:48:35 +01:00
|
|
|
const channelsErrorMessage =
|
|
|
|
|
channelsQuery.error instanceof Error
|
|
|
|
|
? channelsQuery.error.message
|
|
|
|
|
: undefined;
|
2026-03-12 12:53:59 -07:00
|
|
|
const memberChannels = React.useMemo(
|
|
|
|
|
() => channels.filter((channel) => channel.isMember),
|
|
|
|
|
[channels],
|
|
|
|
|
);
|
2026-04-07 14:41:22 -07:00
|
|
|
const sidebarChannels = React.useMemo(
|
|
|
|
|
() => memberChannels.filter((channel) => channel.archivedAt === null),
|
|
|
|
|
[memberChannels],
|
|
|
|
|
);
|
2026-04-07 12:20:42 -07:00
|
|
|
const activeChannel = React.useMemo(
|
|
|
|
|
() =>
|
|
|
|
|
selectedChannelId
|
|
|
|
|
? (channels.find((channel) => channel.id === selectedChannelId) ?? null)
|
|
|
|
|
: null,
|
|
|
|
|
[channels, selectedChannelId],
|
2026-04-06 09:19:47 -07:00
|
|
|
);
|
2026-06-24 22:28:35 -04:00
|
|
|
const managedChannel = React.useMemo(() => {
|
|
|
|
|
const targetChannelId = managedChannelId ?? selectedChannelId;
|
|
|
|
|
return targetChannelId
|
|
|
|
|
? (channels.find((channel) => channel.id === targetChannelId) ?? null)
|
|
|
|
|
: null;
|
|
|
|
|
}, [channels, managedChannelId, selectedChannelId]);
|
2026-04-06 09:19:47 -07:00
|
|
|
|
2026-06-24 14:51:05 -07:00
|
|
|
const {
|
|
|
|
|
handleChannelNotification,
|
|
|
|
|
handleDmNotification,
|
|
|
|
|
handleThreadReplyDesktopNotification,
|
|
|
|
|
} = useAppShellDesktopNotifications({
|
|
|
|
|
channels,
|
|
|
|
|
goChannel,
|
|
|
|
|
goHome,
|
|
|
|
|
notificationSettings: notificationSettings.settings,
|
|
|
|
|
openSearchHit,
|
|
|
|
|
pubkey: identityQuery.data?.pubkey,
|
|
|
|
|
});
|
2026-06-11 07:31:19 -07:00
|
|
|
|
2026-05-28 18:43:41 -04:00
|
|
|
const {
|
|
|
|
|
followedRootIds,
|
|
|
|
|
isFollowing: isFollowingThread,
|
|
|
|
|
followThread,
|
|
|
|
|
unfollowThread,
|
|
|
|
|
} = useThreadFollows(identityQuery.data?.pubkey);
|
|
|
|
|
|
2026-05-20 17:13:59 -04:00
|
|
|
const {
|
2026-05-26 20:38:29 -04:00
|
|
|
markAllChannelsRead,
|
2026-05-20 17:13:59 -04:00
|
|
|
markChannelRead,
|
|
|
|
|
markChannelUnread,
|
|
|
|
|
unreadChannelIds,
|
2026-06-18 16:26:22 -04:00
|
|
|
unreadChannelCounts,
|
2026-06-03 21:16:49 -04:00
|
|
|
highPriorityUnreadChannelIds,
|
2026-06-22 19:48:32 +01:00
|
|
|
unreadChannelNotificationCount,
|
2026-05-20 17:13:59 -04:00
|
|
|
getEffectiveTimestamp: getChannelReadAt,
|
2026-06-22 19:48:32 +01:00
|
|
|
getOwnTimestamp: getOwnReadAt,
|
2026-05-20 17:13:59 -04:00
|
|
|
readStateVersion,
|
2026-06-15 22:45:57 -04:00
|
|
|
setContextParentResolver,
|
2026-05-28 18:43:41 -04:00
|
|
|
participatedRootIds,
|
|
|
|
|
authoredRootIds,
|
2026-06-17 16:12:18 -04:00
|
|
|
mentionedRootIds,
|
2026-05-28 18:43:41 -04:00
|
|
|
threadActivityItems,
|
|
|
|
|
mutedRootIds,
|
|
|
|
|
muteThread,
|
|
|
|
|
unmuteThread,
|
2026-06-15 22:45:57 -04:00
|
|
|
} = useUnreadChannels(sidebarChannels, activeChannel, {
|
|
|
|
|
pubkey: identityQuery.data?.pubkey,
|
|
|
|
|
relayClient,
|
|
|
|
|
currentPubkey: identityQuery.data?.pubkey,
|
|
|
|
|
mutedChannelIds,
|
|
|
|
|
notifyForActiveChannel: notificationSettings.settings.notifyWhileViewing,
|
|
|
|
|
onChannelMessage: handleChannelNotification,
|
|
|
|
|
onDmMessage: handleDmNotification,
|
2026-06-22 19:48:32 +01:00
|
|
|
onLiveMention: refetchHomeFeedFromLiveSignal,
|
2026-06-15 22:45:57 -04:00
|
|
|
onThreadReplyDesktopNotification: handleThreadReplyDesktopNotification,
|
|
|
|
|
followedRootIds,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const getThreadReadAt = React.useCallback(
|
2026-06-22 19:48:32 +01:00
|
|
|
(rootId: string, channelId?: string | null) => {
|
|
|
|
|
const threadReadAt = getOwnReadAt(`thread:${rootId}`);
|
|
|
|
|
if (!channelId) {
|
|
|
|
|
return threadReadAt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const channelReadAt = getChannelReadAt(channelId);
|
|
|
|
|
if (threadReadAt === null) {
|
|
|
|
|
return channelReadAt;
|
|
|
|
|
}
|
|
|
|
|
if (channelReadAt === null) {
|
|
|
|
|
return threadReadAt;
|
|
|
|
|
}
|
|
|
|
|
return Math.max(threadReadAt, channelReadAt);
|
|
|
|
|
},
|
|
|
|
|
[getChannelReadAt, getOwnReadAt],
|
2026-06-15 22:45:57 -04:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const markThreadRead = React.useCallback(
|
|
|
|
|
(rootId: string, timestamp: number) => {
|
|
|
|
|
markChannelRead(
|
|
|
|
|
`thread:${rootId}`,
|
|
|
|
|
new Date(timestamp * 1_000).toISOString(),
|
|
|
|
|
);
|
2026-05-20 17:13:59 -04:00
|
|
|
},
|
2026-06-15 22:45:57 -04:00
|
|
|
[markChannelRead],
|
2026-05-20 17:13:59 -04:00
|
|
|
);
|
2026-06-22 22:25:10 -04:00
|
|
|
|
|
|
|
|
// Per-message read frontier (LP4 v3): effective(msg:<id>) folds through the
|
|
|
|
|
// channel, so a channel-read clears messages older than the top-level frontier.
|
|
|
|
|
const getMessageReadAt = React.useCallback(
|
|
|
|
|
(messageId: string) => getChannelReadAt(msgContextKey(messageId)),
|
|
|
|
|
[getChannelReadAt],
|
|
|
|
|
);
|
|
|
|
|
const markMessageRead = React.useCallback(
|
|
|
|
|
(messageId: string, timestamp: number) =>
|
|
|
|
|
markChannelRead(
|
|
|
|
|
msgContextKey(messageId),
|
|
|
|
|
new Date(timestamp * 1_000).toISOString(),
|
|
|
|
|
),
|
|
|
|
|
[markChannelRead],
|
|
|
|
|
);
|
2026-06-22 19:48:32 +01:00
|
|
|
const threadActivityFeedItems = useThreadActivityFeedItems(
|
|
|
|
|
threadActivityItems,
|
|
|
|
|
mutedRootIds,
|
|
|
|
|
channels,
|
|
|
|
|
);
|
2026-05-20 17:13:59 -04:00
|
|
|
|
2026-06-24 22:28:35 -04:00
|
|
|
// Badge count consumes the shared NIP-RS read-state from useUnreadChannels.
|
2026-06-03 21:16:49 -04:00
|
|
|
const { homeBadgeCount, homeBadgeCountExcludingHighPriority } =
|
|
|
|
|
useHomeFeedNotificationState(
|
|
|
|
|
homeFeedQuery.data,
|
|
|
|
|
identityQuery.data?.pubkey,
|
|
|
|
|
notificationSettings.settings,
|
|
|
|
|
notificationSettings.setDesktopEnabled,
|
2026-06-16 23:37:09 -07:00
|
|
|
selectedView === "home" && !settingsOpen,
|
2026-06-03 21:16:49 -04:00
|
|
|
getChannelReadAt,
|
|
|
|
|
readStateVersion,
|
|
|
|
|
highPriorityUnreadChannelIds,
|
|
|
|
|
feedProfilesQuery.data?.profiles,
|
2026-06-04 15:53:53 -04:00
|
|
|
mutedChannelIds,
|
2026-06-22 19:48:32 +01:00
|
|
|
feedItemState.unreadSet,
|
|
|
|
|
threadActivityFeedItems,
|
|
|
|
|
getThreadReadAt,
|
2026-06-24 09:39:16 -07:00
|
|
|
getMessageReadAt,
|
2026-06-03 21:16:49 -04:00
|
|
|
);
|
2026-03-10 13:54:25 -07:00
|
|
|
|
2026-06-23 10:46:18 -04:00
|
|
|
const dueReminderBadge = useDueReminderBadgeCount(
|
|
|
|
|
identityQuery.data?.pubkey,
|
|
|
|
|
notificationSettings.settings.homeBadgeEnabled,
|
|
|
|
|
);
|
2026-05-28 18:43:41 -04:00
|
|
|
const isNotifiedForThread = React.useCallback(
|
|
|
|
|
(rootId: string) =>
|
|
|
|
|
!mutedRootIds.has(rootId) &&
|
|
|
|
|
(followedRootIds.has(rootId) ||
|
|
|
|
|
participatedRootIds.has(rootId) ||
|
2026-06-17 16:12:18 -04:00
|
|
|
authoredRootIds.has(rootId) ||
|
|
|
|
|
mentionedRootIds.has(rootId)),
|
|
|
|
|
[
|
|
|
|
|
followedRootIds,
|
|
|
|
|
mutedRootIds,
|
|
|
|
|
participatedRootIds,
|
|
|
|
|
authoredRootIds,
|
|
|
|
|
mentionedRootIds,
|
|
|
|
|
],
|
2026-05-28 18:43:41 -04:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handleFollowThread = React.useCallback(
|
|
|
|
|
(rootId: string) => {
|
|
|
|
|
followThread(rootId);
|
|
|
|
|
unmuteThread(rootId);
|
|
|
|
|
},
|
|
|
|
|
[followThread, unmuteThread],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handleUnfollowThread = React.useCallback(
|
|
|
|
|
(rootId: string) => {
|
|
|
|
|
unfollowThread(rootId);
|
|
|
|
|
muteThread(rootId);
|
|
|
|
|
},
|
|
|
|
|
[unfollowThread, muteThread],
|
|
|
|
|
);
|
|
|
|
|
|
2026-04-06 09:19:47 -07:00
|
|
|
const createChannelMutation = useCreateChannelMutation();
|
|
|
|
|
const createForumMutation = useCreateChannelMutation();
|
2026-05-11 12:31:25 -07:00
|
|
|
const { applyCanvas, applyAgents } = useApplyTemplate();
|
|
|
|
|
|
2026-04-06 09:19:47 -07:00
|
|
|
const openDmMutation = useOpenDmMutation();
|
|
|
|
|
const hideDmMutation = useHideDmMutation();
|
|
|
|
|
const handleOpenBrowseChannels = React.useCallback(() => {
|
|
|
|
|
setBrowseDialogType("stream");
|
|
|
|
|
void refetchChannels();
|
|
|
|
|
}, [refetchChannels]);
|
|
|
|
|
const handleOpenSearch = React.useCallback(() => {
|
2026-05-29 11:26:41 -04:00
|
|
|
setSearchFocusRequest((request) => request + 1);
|
2026-04-06 09:19:47 -07:00
|
|
|
void refetchChannels();
|
|
|
|
|
}, [refetchChannels]);
|
|
|
|
|
|
|
|
|
|
const handleBrowseDialogOpenChange = React.useCallback((open: boolean) => {
|
|
|
|
|
if (!open) {
|
|
|
|
|
setBrowseDialogType(null);
|
2026-03-10 13:54:25 -07:00
|
|
|
}
|
2026-03-20 16:32:16 -07:00
|
|
|
}, []);
|
|
|
|
|
|
2026-03-12 12:53:59 -07:00
|
|
|
const handleBrowseChannelJoin = React.useCallback(
|
|
|
|
|
async (channelId: string) => {
|
|
|
|
|
await joinChannel(channelId);
|
|
|
|
|
await queryClient.invalidateQueries({ queryKey: channelsQueryKey });
|
|
|
|
|
},
|
|
|
|
|
[queryClient],
|
2026-03-10 13:54:25 -07:00
|
|
|
);
|
|
|
|
|
|
2026-03-22 09:00:45 -07:00
|
|
|
const handleHideDm = React.useCallback(
|
|
|
|
|
async (channelId: string) => {
|
|
|
|
|
try {
|
|
|
|
|
await hideDmMutation.mutateAsync(channelId);
|
|
|
|
|
} catch {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-04-06 09:19:47 -07:00
|
|
|
|
2026-04-07 12:20:42 -07:00
|
|
|
if (selectedChannelId === channelId) {
|
|
|
|
|
void goHome();
|
2026-03-22 09:00:45 -07:00
|
|
|
}
|
|
|
|
|
},
|
2026-04-07 12:20:42 -07:00
|
|
|
[goHome, hideDmMutation, selectedChannelId],
|
2026-03-22 09:00:45 -07:00
|
|
|
);
|
|
|
|
|
|
2026-03-16 10:17:16 -07:00
|
|
|
const handleOpenSettings = React.useCallback(
|
2026-06-08 15:23:11 +01:00
|
|
|
(section: SettingsSection = DEFAULT_SETTINGS_SECTION) => {
|
2026-03-16 10:17:16 -07:00
|
|
|
setIsChannelManagementOpen(false);
|
2026-06-16 23:37:09 -07:00
|
|
|
void goSettings(section);
|
2026-03-16 10:17:16 -07:00
|
|
|
},
|
2026-06-16 23:37:09 -07:00
|
|
|
[goSettings],
|
2026-03-16 10:17:16 -07:00
|
|
|
);
|
|
|
|
|
|
2026-06-22 22:25:10 -04:00
|
|
|
const handleCloseSettings = React.useCallback(
|
|
|
|
|
() => closeSettings(),
|
|
|
|
|
[closeSettings],
|
|
|
|
|
);
|
2026-06-16 23:37:09 -07:00
|
|
|
|
|
|
|
|
// Section switches rewrite the settings entry rather than stacking one
|
|
|
|
|
// history entry per section, so back always exits settings in one step.
|
|
|
|
|
const handleSettingsSectionChange = React.useCallback(
|
|
|
|
|
(section: SettingsSection) => {
|
|
|
|
|
void goSettings(section, { replace: true });
|
|
|
|
|
},
|
|
|
|
|
[goSettings],
|
|
|
|
|
);
|
2026-03-11 10:34:41 -07:00
|
|
|
|
2026-04-07 10:26:13 -07:00
|
|
|
const handleOpenSearchResult = React.useCallback(
|
|
|
|
|
(hit: SearchHit) => {
|
2026-04-07 12:20:42 -07:00
|
|
|
void openSearchHit(hit);
|
2026-04-07 10:26:13 -07:00
|
|
|
},
|
2026-04-07 12:20:42 -07:00
|
|
|
[openSearchHit],
|
2026-04-07 10:26:13 -07:00
|
|
|
);
|
|
|
|
|
|
2026-05-11 16:26:12 -07:00
|
|
|
// Prevent webview file:/// navigation on file drop outside the composer.
|
|
|
|
|
// Scoped to file drags only (text drag-and-drop into inputs still works).
|
|
|
|
|
// Composer's onDrop fires first (React synthetic before window bubble).
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
function preventNavigation(e: DragEvent) {
|
|
|
|
|
if (e.dataTransfer?.types.includes("Files")) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
window.addEventListener("dragover", preventNavigation);
|
|
|
|
|
window.addEventListener("drop", preventNavigation);
|
|
|
|
|
return () => {
|
|
|
|
|
window.removeEventListener("dragover", preventNavigation);
|
|
|
|
|
window.removeEventListener("drop", preventNavigation);
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-03-13 16:26:07 -07:00
|
|
|
React.useEffect(() => {
|
|
|
|
|
let isCancelled = false;
|
|
|
|
|
|
2026-04-08 11:13:14 -07:00
|
|
|
const startPreconnect = () => {
|
|
|
|
|
if (isCancelled) {
|
|
|
|
|
return;
|
2026-03-13 16:26:07 -07:00
|
|
|
}
|
|
|
|
|
|
2026-04-08 11:13:14 -07:00
|
|
|
void relayClient.preconnect().catch((error) => {
|
|
|
|
|
if (!isCancelled) {
|
|
|
|
|
console.error("Failed to preconnect to relay", error);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if ("requestIdleCallback" in window) {
|
|
|
|
|
const idleId = window.requestIdleCallback(startPreconnect, {
|
|
|
|
|
timeout: 1_500,
|
|
|
|
|
});
|
|
|
|
|
return () => {
|
|
|
|
|
isCancelled = true;
|
|
|
|
|
window.cancelIdleCallback(idleId);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const timeoutId = globalThis.setTimeout(startPreconnect, 250);
|
2026-03-13 16:26:07 -07:00
|
|
|
return () => {
|
|
|
|
|
isCancelled = true;
|
2026-04-08 11:13:14 -07:00
|
|
|
globalThis.clearTimeout(timeoutId);
|
2026-03-13 16:26:07 -07:00
|
|
|
};
|
|
|
|
|
}, []);
|
2026-04-06 09:19:47 -07:00
|
|
|
|
2026-04-07 10:26:13 -07:00
|
|
|
React.useEffect(() => {
|
2026-06-23 13:59:21 -07:00
|
|
|
const count =
|
2026-06-22 19:48:32 +01:00
|
|
|
unreadChannelNotificationCount + homeBadgeCountExcludingHighPriority;
|
2026-06-23 13:59:21 -07:00
|
|
|
void setDesktopAppBadge(
|
|
|
|
|
count
|
|
|
|
|
? { kind: "count", count }
|
|
|
|
|
: { kind: unreadChannelIds.size ? "dot" : "none" },
|
|
|
|
|
);
|
|
|
|
|
}, [
|
|
|
|
|
homeBadgeCountExcludingHighPriority,
|
|
|
|
|
unreadChannelIds,
|
|
|
|
|
unreadChannelNotificationCount,
|
|
|
|
|
]);
|
2026-04-07 10:26:13 -07:00
|
|
|
|
2026-06-10 14:07:02 -07:00
|
|
|
// Dispatch `buzz://message` deep links into the router.
|
2026-05-27 12:27:34 -07:00
|
|
|
useMessageDeepLinks();
|
|
|
|
|
|
2026-06-22 22:25:10 -04:00
|
|
|
const handleOpenNewDm = React.useCallback(() => setIsNewDmOpen(true), []);
|
|
|
|
|
const handleOpenCreateChannel = React.useCallback(
|
|
|
|
|
() => setIsCreateChannelOpen(true),
|
|
|
|
|
[],
|
|
|
|
|
);
|
2026-04-06 09:19:47 -07:00
|
|
|
React.useLayoutEffect(() => {
|
|
|
|
|
if (settingsOpen) {
|
2026-03-13 08:38:42 -07:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-06 09:19:47 -07:00
|
|
|
function handleKeyDown(event: KeyboardEvent) {
|
2026-05-09 19:34:05 -07:00
|
|
|
if (!hasPrimaryShortcutModifier(event) || event.altKey) {
|
2026-04-06 09:19:47 -07:00
|
|
|
return;
|
|
|
|
|
}
|
2026-03-13 08:38:42 -07:00
|
|
|
|
2026-04-06 09:19:47 -07:00
|
|
|
const key = event.key.toLowerCase();
|
|
|
|
|
if (key === "k" && !event.shiftKey) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
handleOpenSearch();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-03-13 08:38:42 -07:00
|
|
|
|
2026-04-22 16:45:56 -06:00
|
|
|
if (key === "k" && event.shiftKey) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
handleOpenNewDm();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 09:12:45 -07:00
|
|
|
if (key === "n" && event.shiftKey) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
handleOpenCreateChannel();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-06 09:19:47 -07:00
|
|
|
if (key === "o" && event.shiftKey) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
handleOpenBrowseChannels();
|
2026-04-22 16:45:56 -06:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (key === "a" && event.shiftKey) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
void goHome();
|
|
|
|
|
return;
|
2026-03-13 08:38:42 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-06 09:19:47 -07:00
|
|
|
window.addEventListener("keydown", handleKeyDown);
|
2026-03-13 08:38:42 -07:00
|
|
|
return () => {
|
2026-04-06 09:19:47 -07:00
|
|
|
window.removeEventListener("keydown", handleKeyDown);
|
2026-03-13 08:38:42 -07:00
|
|
|
};
|
2026-04-22 16:45:56 -06:00
|
|
|
}, [
|
|
|
|
|
handleOpenBrowseChannels,
|
|
|
|
|
handleOpenNewDm,
|
2026-06-01 09:12:45 -07:00
|
|
|
handleOpenCreateChannel,
|
2026-04-22 16:45:56 -06:00
|
|
|
handleOpenSearch,
|
|
|
|
|
goHome,
|
|
|
|
|
settingsOpen,
|
|
|
|
|
]);
|
2026-06-04 11:48:57 -04:00
|
|
|
useSettingsShortcuts({
|
|
|
|
|
onClose: handleCloseSettings,
|
|
|
|
|
onOpenSettings: handleOpenSettings,
|
|
|
|
|
open: settingsOpen,
|
|
|
|
|
});
|
2026-05-26 20:38:29 -04:00
|
|
|
useMarkAsReadShortcuts({
|
|
|
|
|
activeChannelId: activeChannel?.id ?? null,
|
|
|
|
|
activeChannelLastMessageAt: activeChannel?.lastMessageAt,
|
|
|
|
|
markAllChannelsRead,
|
|
|
|
|
markChannelRead,
|
|
|
|
|
selectedView,
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-09 13:02:11 -07:00
|
|
|
return (
|
2026-05-05 14:14:00 -06:00
|
|
|
<PreventSleepProvider>
|
|
|
|
|
<ChannelNavigationProvider channels={channels}>
|
|
|
|
|
<AppShellProvider
|
|
|
|
|
value={{
|
2026-05-26 20:38:29 -04:00
|
|
|
markAllChannelsRead,
|
2026-05-05 14:14:00 -06:00
|
|
|
markChannelRead,
|
2026-05-14 15:31:55 -07:00
|
|
|
markChannelUnread,
|
2026-06-11 22:56:13 +01:00
|
|
|
openCreateChannel: handleOpenCreateChannel,
|
2026-06-24 22:28:35 -04:00
|
|
|
openChannelManagement: (channelId?: string) => {
|
|
|
|
|
setManagedChannelId(
|
|
|
|
|
typeof channelId === "string" ? channelId : null,
|
|
|
|
|
);
|
|
|
|
|
setIsChannelManagementOpen(true);
|
|
|
|
|
},
|
2026-05-20 17:13:59 -04:00
|
|
|
getChannelReadAt,
|
2026-06-15 22:45:57 -04:00
|
|
|
getThreadReadAt,
|
|
|
|
|
markThreadRead,
|
2026-06-22 22:25:10 -04:00
|
|
|
getMessageReadAt,
|
|
|
|
|
markMessageRead,
|
2026-05-20 17:13:59 -04:00
|
|
|
readStateVersion,
|
2026-06-15 22:45:57 -04:00
|
|
|
setContextParentResolver,
|
2026-05-28 18:43:41 -04:00
|
|
|
followThread: handleFollowThread,
|
|
|
|
|
unfollowThread: handleUnfollowThread,
|
|
|
|
|
isFollowingThread,
|
|
|
|
|
isNotifiedForThread,
|
2026-06-22 19:48:32 +01:00
|
|
|
isThreadMuted: (rootId) => mutedRootIds.has(rootId),
|
2026-05-28 18:43:41 -04:00
|
|
|
threadActivityItems,
|
2026-06-22 19:48:32 +01:00
|
|
|
threadActivityFeedItems,
|
|
|
|
|
feedItemState,
|
2026-05-05 14:14:00 -06:00
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<HuddleProvider>
|
2026-06-17 17:47:41 -04:00
|
|
|
<RemindMeLaterProvider pubkey={identityQuery.data?.pubkey}>
|
2026-06-15 21:58:12 +01:00
|
|
|
<div
|
2026-06-17 11:03:45 -04:00
|
|
|
className="buzz-huddle-shell relative h-dvh overflow-hidden overscroll-none"
|
|
|
|
|
data-huddle-open={isHuddleDrawerOpen}
|
2026-06-15 21:58:12 +01:00
|
|
|
>
|
2026-06-17 11:03:45 -04:00
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
"buzz-huddle-app-surface z-10 flex min-h-0 flex-col overflow-hidden bg-background",
|
|
|
|
|
isHuddleDrawerOpen && "buzz-huddle-app-surface-open",
|
|
|
|
|
)}
|
|
|
|
|
>
|
2026-06-24 20:54:05 +01:00
|
|
|
<SidebarProvider className="min-h-0 flex-1 flex-col overflow-hidden">
|
2026-06-17 11:03:45 -04:00
|
|
|
{!settingsOpen ? (
|
|
|
|
|
<AppTopChrome
|
|
|
|
|
canGoBack={canGoBack}
|
|
|
|
|
canGoForward={canGoForward}
|
|
|
|
|
onGoBack={goBack}
|
|
|
|
|
onGoForward={goForward}
|
|
|
|
|
/>
|
|
|
|
|
) : null}
|
|
|
|
|
{settingsOpen ? (
|
2026-06-24 20:54:05 +01:00
|
|
|
<div className="flex min-h-0 flex-1 overflow-hidden">
|
|
|
|
|
<React.Suspense fallback={null}>
|
|
|
|
|
<LazySettingsScreen
|
|
|
|
|
currentPubkey={identityQuery.data?.pubkey}
|
|
|
|
|
fallbackDisplayName={
|
|
|
|
|
identityQuery.data?.displayName
|
|
|
|
|
}
|
|
|
|
|
isUpdatingDesktopNotifications={
|
|
|
|
|
notificationSettings.isUpdatingDesktopEnabled
|
|
|
|
|
}
|
|
|
|
|
notificationErrorMessage={
|
|
|
|
|
notificationSettings.errorMessage
|
|
|
|
|
}
|
|
|
|
|
notificationPermission={
|
|
|
|
|
notificationSettings.permission
|
|
|
|
|
}
|
|
|
|
|
notificationSettings={notificationSettings.settings}
|
|
|
|
|
onClose={handleCloseSettings}
|
|
|
|
|
onSectionChange={handleSettingsSectionChange}
|
|
|
|
|
onSetDesktopNotificationsEnabled={
|
|
|
|
|
notificationSettings.setDesktopEnabled
|
|
|
|
|
}
|
|
|
|
|
onSetHomeBadgeEnabled={
|
|
|
|
|
notificationSettings.setHomeBadgeEnabled
|
|
|
|
|
}
|
|
|
|
|
onSetSlotAlertsEnabled={
|
|
|
|
|
notificationSettings.setSlotAlertsEnabled
|
|
|
|
|
}
|
|
|
|
|
onSetNotifyWhileViewing={
|
|
|
|
|
notificationSettings.setNotifyWhileViewing
|
|
|
|
|
}
|
|
|
|
|
onSetAllSlotAlertsEnabled={
|
|
|
|
|
notificationSettings.setAllSlotAlertsEnabled
|
|
|
|
|
}
|
|
|
|
|
onSetSoundForSlot={
|
|
|
|
|
notificationSettings.setSoundForSlot
|
|
|
|
|
}
|
|
|
|
|
section={settingsSection}
|
|
|
|
|
/>
|
|
|
|
|
</React.Suspense>
|
|
|
|
|
</div>
|
2026-06-17 11:03:45 -04:00
|
|
|
) : (
|
2026-06-24 20:54:05 +01:00
|
|
|
<div className="flex min-h-0 flex-1 overflow-hidden">
|
2026-06-17 11:03:45 -04:00
|
|
|
<AppSidebar
|
|
|
|
|
activeWorkspace={workspacesHook.activeWorkspace}
|
|
|
|
|
channels={sidebarChannels}
|
|
|
|
|
currentPubkey={identityQuery.data?.pubkey}
|
2026-06-17 18:48:35 +01:00
|
|
|
errorMessage={channelsErrorMessage}
|
2026-06-17 11:03:45 -04:00
|
|
|
fallbackDisplayName={identityQuery.data?.displayName}
|
2026-06-23 10:46:18 -04:00
|
|
|
homeBadgeCount={homeBadgeCount + dueReminderBadge}
|
2026-06-17 11:03:45 -04:00
|
|
|
isAddWorkspaceOpen={isAddWorkspaceOpen}
|
|
|
|
|
isCreatingChannel={createChannelMutation.isPending}
|
|
|
|
|
isCreatingForum={createForumMutation.isPending}
|
|
|
|
|
isLoading={channelsQuery.isLoading}
|
|
|
|
|
isOpeningDm={openDmMutation.isPending}
|
|
|
|
|
isNewDmOpen={isNewDmOpen}
|
|
|
|
|
isCreateChannelOpen={isCreateChannelOpen}
|
|
|
|
|
isPresencePending={presenceSession.isPending}
|
|
|
|
|
onAddWorkspace={(workspace) => {
|
|
|
|
|
const id = workspacesHook.addWorkspace(workspace);
|
|
|
|
|
workspacesHook.switchWorkspace(id);
|
|
|
|
|
}}
|
|
|
|
|
onAddWorkspaceOpenChange={setIsAddWorkspaceOpen}
|
|
|
|
|
onNewDmOpenChange={setIsNewDmOpen}
|
|
|
|
|
onCreateChannelOpenChange={setIsCreateChannelOpen}
|
|
|
|
|
onOpenAddWorkspace={() => setIsAddWorkspaceOpen(true)}
|
|
|
|
|
onUpdateWorkspace={workspacesHook.updateWorkspace}
|
|
|
|
|
onRemoveWorkspace={workspacesHook.removeWorkspace}
|
|
|
|
|
onSwitchWorkspace={workspacesHook.switchWorkspace}
|
2026-06-23 22:31:06 +01:00
|
|
|
onCreateAgent={() =>
|
|
|
|
|
void goAgents().then(requestOpenCreateAgent)
|
|
|
|
|
}
|
2026-06-17 11:03:45 -04:00
|
|
|
selfPresenceStatus={presenceSession.currentStatus}
|
|
|
|
|
workspaces={workspacesHook.workspaces}
|
|
|
|
|
onCreateChannel={async ({
|
|
|
|
|
description,
|
|
|
|
|
name,
|
|
|
|
|
visibility,
|
|
|
|
|
ttlSeconds,
|
|
|
|
|
templateId,
|
|
|
|
|
}) => {
|
|
|
|
|
const createdChannel =
|
|
|
|
|
await createChannelMutation.mutateAsync({
|
|
|
|
|
name,
|
|
|
|
|
description,
|
|
|
|
|
channelType: "stream",
|
|
|
|
|
visibility,
|
|
|
|
|
ttlSeconds,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await applyCanvas(
|
|
|
|
|
templateId,
|
|
|
|
|
createdChannel.id,
|
|
|
|
|
name,
|
|
|
|
|
);
|
|
|
|
|
await goChannel(createdChannel.id);
|
|
|
|
|
void applyAgents(templateId, createdChannel.id);
|
|
|
|
|
}}
|
|
|
|
|
onCreateForum={async ({
|
|
|
|
|
description,
|
|
|
|
|
name,
|
|
|
|
|
visibility,
|
|
|
|
|
ttlSeconds,
|
|
|
|
|
templateId,
|
|
|
|
|
}) => {
|
|
|
|
|
const createdForum =
|
|
|
|
|
await createForumMutation.mutateAsync({
|
|
|
|
|
name,
|
|
|
|
|
description,
|
|
|
|
|
channelType: "forum",
|
|
|
|
|
visibility,
|
|
|
|
|
ttlSeconds,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await applyCanvas(
|
|
|
|
|
templateId,
|
|
|
|
|
createdForum.id,
|
|
|
|
|
name,
|
|
|
|
|
);
|
|
|
|
|
await goChannel(createdForum.id);
|
|
|
|
|
void applyAgents(templateId, createdForum.id);
|
|
|
|
|
}}
|
|
|
|
|
onHideDm={handleHideDm}
|
|
|
|
|
onMarkAllChannelsRead={markAllChannelsRead}
|
|
|
|
|
onMarkChannelRead={markChannelRead}
|
|
|
|
|
onMarkChannelUnread={markChannelUnread}
|
2026-06-19 12:41:23 -04:00
|
|
|
onBrowseChannels={handleOpenBrowseChannels}
|
2026-06-17 11:03:45 -04:00
|
|
|
onOpenDm={async ({ pubkeys }) => {
|
|
|
|
|
const directMessage =
|
|
|
|
|
await openDmMutation.mutateAsync({
|
|
|
|
|
pubkeys,
|
|
|
|
|
});
|
|
|
|
|
await goChannel(directMessage.id);
|
|
|
|
|
}}
|
|
|
|
|
onSelectAgents={() => void goAgents()}
|
|
|
|
|
onSelectChannel={(channelId) =>
|
|
|
|
|
void goChannel(channelId)
|
|
|
|
|
}
|
2026-06-19 17:08:41 -04:00
|
|
|
onOpenSearchResult={handleOpenSearchResult}
|
|
|
|
|
searchChannels={channels}
|
|
|
|
|
searchFocusRequest={searchFocusRequest}
|
2026-06-17 11:03:45 -04:00
|
|
|
onSelectHome={() => void goHome()}
|
|
|
|
|
onSelectProjects={() => void goProjects()}
|
|
|
|
|
onSelectPulse={() => void goPulse()}
|
|
|
|
|
onSelectSettings={handleOpenSettings}
|
|
|
|
|
onSelectWorkflows={() => void goWorkflows()}
|
|
|
|
|
onSetPresenceStatus={(status) =>
|
|
|
|
|
presenceSession.setStatus(status)
|
|
|
|
|
}
|
|
|
|
|
onSetUserStatus={(text, emoji) =>
|
|
|
|
|
setUserStatusMutation.mutate({ text, emoji })
|
|
|
|
|
}
|
|
|
|
|
onClearUserStatus={() =>
|
|
|
|
|
setUserStatusMutation.mutate({
|
|
|
|
|
text: "",
|
|
|
|
|
emoji: "",
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
profile={profileQuery.data}
|
|
|
|
|
selfUserStatus={
|
|
|
|
|
deferredPubkey
|
|
|
|
|
? (selfStatusQuery.data?.[
|
|
|
|
|
deferredPubkey.toLowerCase()
|
|
|
|
|
] ?? undefined)
|
|
|
|
|
: undefined
|
|
|
|
|
}
|
|
|
|
|
selectedChannelId={selectedChannelId}
|
|
|
|
|
selectedView={selectedView}
|
|
|
|
|
unreadChannelIds={unreadChannelIds}
|
2026-06-18 16:26:22 -04:00
|
|
|
unreadChannelCounts={unreadChannelCounts}
|
2026-06-17 11:03:45 -04:00
|
|
|
mutedChannelIds={mutedChannelIds}
|
|
|
|
|
onMuteChannel={muteChannel}
|
|
|
|
|
onUnmuteChannel={unmuteChannel}
|
|
|
|
|
starredChannelIds={starredChannelIds}
|
|
|
|
|
onStarChannel={starChannel}
|
|
|
|
|
onUnstarChannel={unstarChannel}
|
|
|
|
|
/>
|
|
|
|
|
<MainInsetProvider mainInsetRef={mainInsetRef}>
|
|
|
|
|
<SidebarInset
|
|
|
|
|
ref={mainInsetRef}
|
2026-06-24 20:54:05 +01:00
|
|
|
className="isolate min-h-0 min-w-0 overflow-hidden bg-sidebar"
|
2026-06-17 11:03:45 -04:00
|
|
|
style={chromeCssVarDefaults}
|
|
|
|
|
>
|
2026-06-26 02:16:56 +01:00
|
|
|
<div className="relative z-10 mb-2 ml-px mr-2 mt-px flex min-h-0 flex-1 flex-col overflow-hidden rounded-xl bg-background shadow-[-1px_-1px_0_0_hsl(var(--sidebar-border)/0.45)]">
|
2026-06-24 20:54:05 +01:00
|
|
|
<ConnectionBanner
|
|
|
|
|
errorMessage={channelsErrorMessage}
|
|
|
|
|
/>
|
|
|
|
|
<Outlet />
|
|
|
|
|
</div>
|
2026-06-17 11:03:45 -04:00
|
|
|
</SidebarInset>
|
|
|
|
|
</MainInsetProvider>
|
2026-06-24 20:54:05 +01:00
|
|
|
</div>
|
2026-06-17 11:03:45 -04:00
|
|
|
)}
|
|
|
|
|
<AppShellOverlays
|
2026-06-24 22:28:35 -04:00
|
|
|
activeChannel={managedChannel}
|
2026-06-17 11:03:45 -04:00
|
|
|
browseDialogType={browseDialogType}
|
2026-06-15 21:58:12 +01:00
|
|
|
channels={channels}
|
2026-06-08 15:23:11 +01:00
|
|
|
currentPubkey={identityQuery.data?.pubkey}
|
2026-06-17 11:03:45 -04:00
|
|
|
isChannelManagementOpen={isChannelManagementOpen}
|
|
|
|
|
onBrowseChannelJoin={handleBrowseChannelJoin}
|
|
|
|
|
onBrowseDialogOpenChange={handleBrowseDialogOpenChange}
|
2026-06-24 22:28:35 -04:00
|
|
|
onChannelManagementOpenChange={(open) => {
|
|
|
|
|
setIsChannelManagementOpen(open);
|
|
|
|
|
if (!open) {
|
|
|
|
|
setManagedChannelId(null);
|
|
|
|
|
}
|
|
|
|
|
}}
|
2026-06-17 11:03:45 -04:00
|
|
|
onDeleteActiveChannel={() => {
|
|
|
|
|
setIsChannelManagementOpen(false);
|
2026-06-24 22:28:35 -04:00
|
|
|
setManagedChannelId(null);
|
2026-06-17 11:03:45 -04:00
|
|
|
void goHome({ replace: true });
|
|
|
|
|
}}
|
|
|
|
|
onSelectChannel={(channelId) => {
|
2026-06-08 15:23:11 +01:00
|
|
|
void goChannel(channelId);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
2026-06-17 11:03:45 -04:00
|
|
|
</SidebarProvider>
|
|
|
|
|
</div>
|
2026-04-07 12:20:42 -07:00
|
|
|
|
2026-06-17 11:03:45 -04:00
|
|
|
<div className="absolute inset-x-0 bottom-0 z-0 h-(--buzz-huddle-drawer-height)">
|
|
|
|
|
<HuddleBar
|
|
|
|
|
className="h-full"
|
|
|
|
|
onVisibilityChange={setIsHuddleDrawerOpen}
|
2026-06-15 21:58:12 +01:00
|
|
|
/>
|
2026-06-17 11:03:45 -04:00
|
|
|
</div>
|
2026-06-15 21:58:12 +01:00
|
|
|
</div>
|
2026-06-17 11:03:45 -04:00
|
|
|
</RemindMeLaterProvider>
|
2026-05-05 14:14:00 -06:00
|
|
|
</HuddleProvider>
|
|
|
|
|
</AppShellProvider>
|
|
|
|
|
</ChannelNavigationProvider>
|
|
|
|
|
</PreventSleepProvider>
|
2026-03-09 13:02:11 -07:00
|
|
|
);
|
|
|
|
|
}
|