From 05942d7f68945b6adcd371589007ebf40281486c Mon Sep 17 00:00:00 2001 From: hzrd149 Date: Wed, 15 Jan 2025 11:21:22 -0600 Subject: [PATCH] allow bakery url to be changed without reload --- src/app.tsx | 70 +++++------------- src/components/bakery/connection-status.tsx | 5 +- .../dashboard/panel-item-string.tsx | 27 ++++--- src/components/layout/mobile/drawer-nav.tsx | 5 +- src/components/layout/presets/simple-view.tsx | 1 + src/components/router/require-bakery-auth.tsx | 3 +- src/components/router/require-bakery.tsx | 14 +++- src/hooks/use-reconnect-action.ts | 6 +- src/hooks/use-report.ts | 49 ++++++------ src/services/bakery.ts | 74 ++++++++++--------- src/services/reports.ts | 14 +++- src/services/web-push-notifications.ts | 6 +- src/views/bakery/settings/index.tsx | 48 ------------ .../{ => settings}/bakery/connect/auth.tsx | 20 ++--- .../{ => settings}/bakery/connect/index.tsx | 53 +++++-------- .../bakery/general-settings/index.tsx | 28 ++++--- src/views/settings/bakery/network/gossip.tsx | 6 +- .../bakery/network/hyper-outbound.tsx | 3 +- src/views/settings/bakery/network/hyper.tsx | 3 +- .../settings/bakery/network/i2p-outbound.tsx | 3 +- src/views/settings/bakery/network/i2p.tsx | 3 +- .../settings/bakery/network/tor-outbound.tsx | 3 +- src/views/settings/bakery/network/tor.tsx | 3 +- .../settings/bakery/notifications/index.tsx | 3 +- .../settings/bakery/notifications/ntfy.tsx | 4 +- .../settings/bakery/notifications/other.tsx | 4 +- .../bakery/notifications/web-push.tsx | 5 +- .../settings/bakery/service-logs/index.tsx | 4 +- .../{ => settings}/bakery/setup/index.tsx | 20 ++--- src/views/settings/index.tsx | 16 +++- 30 files changed, 238 insertions(+), 265 deletions(-) delete mode 100644 src/views/bakery/settings/index.tsx rename src/views/{ => settings}/bakery/connect/auth.tsx (87%) rename src/views/{ => settings}/bakery/connect/index.tsx (67%) rename src/views/{ => settings}/bakery/setup/index.tsx (80%) diff --git a/src/app.tsx b/src/app.tsx index cb406a5b8..c5f10a8a9 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -2,13 +2,15 @@ import { lazy, Suspense } from "react"; import { createBrowserRouter, Outlet, RouterProvider, ScrollRestoration } from "react-router-dom"; import { Spinner } from "@chakra-ui/react"; +import GlobalStyles from "./styles"; + import { ErrorBoundary } from "./components/error-boundary"; import AppLayout from "./components/layout"; import DrawerSubViewProvider from "./providers/drawer-sub-view-provider"; import useSetColorMode from "./hooks/use-set-color-mode"; import { RouteProviders } from "./providers/route"; import RequireCurrentAccount from "./components/router/require-current-account"; -import GlobalStyles from "./styles"; +import RequireBakery from "./components/router/require-bakery"; import HomeView from "./views/home/index"; const DiscoveryHomeView = lazy(() => import("./views/discovery/index")); @@ -99,7 +101,6 @@ import UserMediaPostsTab from "./views/user/media-posts"; import NewView from "./views/new"; import NewNoteView from "./views/new/note"; import NewMediaPostView from "./views/new/media"; -import ConnectionStatus from "./components/bakery/connection-status"; const TracksView = lazy(() => import("./views/tracks")); const UserTracksTab = lazy(() => import("./views/user/tracks")); const UserVideosTab = lazy(() => import("./views/user/videos")); @@ -146,11 +147,7 @@ const PodcastsHomeView = lazy(() => import("./views/podcasts")); const PodcastView = lazy(() => import("./views/podcasts/podcast")); const EpisodeView = lazy(() => import("./views/podcasts/podcast/episode")); -// bakery views -const ConnectView = lazy(() => import("./views/bakery/connect")); -const RequireBakery = lazy(() => import("./components/router/require-bakery")); -const BakerySetupView = lazy(() => import("./views/bakery/setup")); -const BakeryAuthView = lazy(() => import("./views/bakery/connect/auth")); +const BakerySetupView = lazy(() => import("./views/settings/bakery/setup")); const RequireBakeryAuth = lazy(() => import("./components/router/require-bakery-auth")); // setting views @@ -162,6 +159,8 @@ import PrivacySettings from "./views/settings/privacy"; import PostSettings from "./views/settings/post"; import AccountSettings from "./views/settings/accounts"; import MediaServersView from "./views/settings/media-servers"; +const BakeryConnectView = lazy(() => import("./views/settings/bakery/connect")); +const BakeryAuthView = lazy(() => import("./views/settings/bakery/connect/auth")); const NotificationSettingsView = lazy(() => import("./views/settings/bakery/notifications")); const BakeryGeneralSettingsView = lazy(() => import("./views/settings/bakery/general-settings")); const BakeryNetworkSettingsView = lazy(() => import("./views/settings/bakery/network")); @@ -298,10 +297,21 @@ const router = createBrowserRouter([ { path: "privacy", element: }, { path: "lightning", element: }, { path: "performance", element: }, + + { path: "bakery/connect", element: }, { path: "bakery", + element: ( + + + + ), children: [ { path: "", element: }, + { + path: "auth", + element: , + }, { path: "notifications", element: }, { path: "network", @@ -562,52 +572,6 @@ const router = createBrowserRouter([ }, ], }, - { - path: "/bakery", - children: [ - { - path: "connect", - children: [ - { path: "", element: }, - { - path: "auth", - element: ( - - - - - ), - }, - ], - }, - { - path: "setup", - element: , - }, - { - path: "", - element: ( - - - - - - - - ), - children: [ - { - path: "search", - element: , - }, - { - path: "", - element: , - }, - ], - }, - ], - }, ]); export const App = () => ( diff --git a/src/components/bakery/connection-status.tsx b/src/components/bakery/connection-status.tsx index 2dce131d9..dd3b7b139 100644 --- a/src/components/bakery/connection-status.tsx +++ b/src/components/bakery/connection-status.tsx @@ -1,10 +1,10 @@ import { Alert, AlertDescription, AlertTitle, Button, Flex, Text } from "@chakra-ui/react"; import { Link as RouterLink, useLocation } from "react-router-dom"; +import { useObservable } from "applesauce-react/hooks"; import WifiOff from "../icons/wifi-off"; -import bakery from "../../services/bakery"; import useReconnectAction from "../../hooks/use-reconnect-action"; -import { useObservable } from "applesauce-react/hooks"; +import { bakery$ } from "../../services/bakery"; function ReconnectPrompt() { const location = useLocation(); @@ -32,6 +32,7 @@ function ReconnectPrompt() { } export default function ConnectionStatus() { + const bakery = useObservable(bakery$); const connected = useObservable(bakery?.connectedSub); if (!bakery || connected) return null; diff --git a/src/components/dashboard/panel-item-string.tsx b/src/components/dashboard/panel-item-string.tsx index e94154b97..799b24566 100644 --- a/src/components/dashboard/panel-item-string.tsx +++ b/src/components/dashboard/panel-item-string.tsx @@ -1,9 +1,10 @@ import { PropsWithChildren, useState } from "react"; -import { Code, Flex, FormControl, FormLabel } from "@chakra-ui/react"; +import { ButtonGroup, Code, Flex, FormControl, FormLabel, IconButton } from "@chakra-ui/react"; import QrCodeSvg from "../qr-code/qr-code-svg"; import TextButton from "./text-button"; import { CopyIconButton } from "../copy-icon-button"; +import { QrCodeIcon } from "../icons"; export default function PanelItemString({ children, @@ -20,16 +21,20 @@ export default function PanelItemString({ return ( {label} - - {value} - - - - {qr && ( - setShowQR((v) => !v)}> - [qr] - - )} + + + {value} + + + + {qr && ( + setShowQR((v) => !v)} + icon={} + aria-label="show qrcode" + /> + )} + {showQR && } {children} diff --git a/src/components/layout/mobile/drawer-nav.tsx b/src/components/layout/mobile/drawer-nav.tsx index 5b9861d4a..91ce141d2 100644 --- a/src/components/layout/mobile/drawer-nav.tsx +++ b/src/components/layout/mobile/drawer-nav.tsx @@ -14,17 +14,18 @@ import { } from "@chakra-ui/react"; import { Link as RouterLink } from "react-router-dom"; import { IconButton } from "@chakra-ui/react"; +import { useObservable } from "applesauce-react/hooks"; import { UserAvatar } from "../../user/user-avatar"; import useCurrentAccount from "../../../hooks/use-current-account"; import UserName from "../../user/user-name"; import UserDnsIdentity from "../../user/user-dns-identity"; -// import ColorModeButton from '../../color-mode-button'; -import bakery from "../../../services/bakery"; import { DirectMessagesIcon, RelayIcon, SearchIcon, SettingsIcon } from "../../icons"; +import { bakery$ } from "../../../services/bakery"; export default function DrawerNav({ isOpen, onClose, ...props }: Omit) { const account = useCurrentAccount(); + const bakery = useObservable(bakery$); return ( diff --git a/src/components/layout/presets/simple-view.tsx b/src/components/layout/presets/simple-view.tsx index 640266954..102b4af11 100644 --- a/src/components/layout/presets/simple-view.tsx +++ b/src/components/layout/presets/simple-view.tsx @@ -20,6 +20,7 @@ export default function SimpleView({ @@ -36,12 +37,19 @@ function InitialConnectionOverlay() { export default function RequireBakery({ children }: PropsWithChildren & { requireConnection?: boolean }) { const location = useLocation(); + const bakery = useObservable(bakery$); const connected = useObservable(bakery?.connectedSub); const isFirstConnection = useObservable(bakery?.isFirstConnection); // if there is no node connection, setup a connection if (!bakery) - return ; + return ( + + ); if (bakery && isFirstConnection && connected === false) return ; diff --git a/src/hooks/use-reconnect-action.ts b/src/hooks/use-reconnect-action.ts index 7f2788733..278588cd9 100644 --- a/src/hooks/use-reconnect-action.ts +++ b/src/hooks/use-reconnect-action.ts @@ -1,10 +1,12 @@ import { useCallback, useEffect, useState } from "react"; -import bakery from "../services/bakery"; +import { useObservable } from "applesauce-react/hooks"; + +import { bakery$ } from "../services/bakery"; const steps = [2, 2, 3, 3, 5, 5, 10, 20, 30, 60]; -/** @deprecated */ export default function useReconnectAction() { + const bakery = useObservable(bakery$); const [tries, setTries] = useState(0); const [count, setCount] = useState(steps[0]); const [error, setError] = useState(); diff --git a/src/hooks/use-report.ts b/src/hooks/use-report.ts index 74eec7907..fd7cfde63 100644 --- a/src/hooks/use-report.ts +++ b/src/hooks/use-report.ts @@ -1,31 +1,34 @@ -import { useEffect, useMemo, useState } from 'react'; -import { ReportArguments } from '@satellite-earth/core/types'; -import { nanoid } from 'nanoid'; +import { useEffect, useMemo, useState } from "react"; +import { ReportArguments } from "@satellite-earth/core/types"; +import { nanoid } from "nanoid"; -import reportManagerService from '../services/reports'; +import { useObservable } from "applesauce-react/hooks"; +import reportManager$ from "../services/reports"; export default function useReport(type: T, id?: string, args?: ReportArguments[T]) { - const [hookId] = useState(() => nanoid()); - const argsKey = JSON.stringify(args); + const [hookId] = useState(() => nanoid()); + const argsKey = JSON.stringify(args); - const report = useMemo(() => { - if (id && args) return reportManagerService?.getOrCreateReport(type, id, args); - }, [type, id, argsKey]); + const reportManager = useObservable(reportManager$); - useEffect(() => { - if (args && report) { - // @ts-expect-error - report.setArgs(args); - report.fireThrottle(); - } - }, [argsKey, report]); + const report = useMemo(() => { + if (id && args) return reportManager?.getOrCreateReport(type, id, args); + }, [type, id, argsKey, reportManager]); - useEffect(() => { - if (report) { - reportManagerService?.addDependency(hookId, report); - return () => reportManagerService?.removeDependency(hookId, report); - } - }, [report]); + useEffect(() => { + if (args && report) { + // @ts-expect-error + report.setArgs(args); + report.fireThrottle(); + } + }, [argsKey, report]); - return report; + useEffect(() => { + if (report) { + reportManager?.addDependency(hookId, report); + return () => reportManager?.removeDependency(hookId, report); + } + }, [report, reportManager]); + + return report; } diff --git a/src/services/bakery.ts b/src/services/bakery.ts index 430268843..54781080a 100644 --- a/src/services/bakery.ts +++ b/src/services/bakery.ts @@ -1,3 +1,5 @@ +import { BehaviorSubject, filter, mergeMap } from "rxjs"; + import { logger } from "../helpers/debug"; import BakeryConnection from "../classes/bakery/bakery-connection"; import BakeryControlApi from "../classes/bakery/control-api"; @@ -10,61 +12,67 @@ const log = logger.extend("bakery"); export function setBakeryURL(url: string) { localSettings.bakeryURL.next(url); - location.reload(); } export function clearBakeryURL() { localSettings.bakeryURL.clear(); - location.reload(); } -let bakery: BakeryConnection | null = null; +export const bakery$ = new BehaviorSubject(null); + +localSettings.bakeryURL.subscribe((url) => { + if (!URL.canParse(url)) return bakery$.next(null); -if (localSettings.bakeryURL.value) { try { - log("Using URL from localStorage"); - bakery = new BakeryConnection(localSettings.bakeryURL.value); + const bakery = new BakeryConnection(localSettings.bakeryURL.value); + + // add the bakery to the relay pool and connect + relayPoolService.relays.set(bakery.url, bakery); + relayPoolService.requestConnect(bakery); + + bakery$.next(bakery); } catch (err) { log("Failed to create bakery connection, clearing storage"); localSettings.bakeryURL.clear(); } -} else { - log("Unable to find private node URL"); -} +}); -if (bakery) { - // add the bakery to the relay pool and connect - relayPoolService.relays.set(bakery.url, bakery); - relayPoolService.requestConnect(bakery); +// automatically authenticate with bakery +bakery$ + .pipe( + filter((r) => r !== null), + mergeMap((r) => r.onChallenge), + ) + .subscribe(async () => { + if (!bakery$.value) return; + + const account = accountService.current.value; + if (!account) return; - // automatically authenticate with bakery - bakery.onChallenge.subscribe(async () => { try { - const savedAuth = localStorage.getItem("personal-node-auth"); - if (savedAuth) { - if (savedAuth === "nostr") { - const account = accountService.current.value; - if (!account) return; - - await bakery.authenticate((draft) => signingService.requestSignature(draft, account)); - } else { - await bakery.authenticate(savedAuth); - } - } + await bakery$.value.authenticate((draft) => signingService.requestSignature(draft, account)); } catch (err) { console.log("Failed to authenticate with bakery", err); - localStorage.removeItem("personal-node-auth"); } }); -} -const controlApi = bakery ? new BakeryControlApi(bakery) : undefined; +export const controlApi$ = new BehaviorSubject(null); + +// create a control api for the bakery +bakery$.subscribe((relay) => { + if (!relay) return controlApi$.next(null); + else controlApi$.next(new BakeryControlApi(relay)); +}); if (import.meta.env.DEV) { // @ts-expect-error - window.bakery = bakery; + window.bakery = bakery$; // @ts-expect-error - window.controlApi = controlApi; + window.controlApi = controlApi$; } -export { controlApi }; -export default bakery; +export function getControlApi() { + return controlApi$.value; +} +export function getBakery() { + return bakery$.value; +} diff --git a/src/services/reports.ts b/src/services/reports.ts index 369f965ba..b1598b7a6 100644 --- a/src/services/reports.ts +++ b/src/services/reports.ts @@ -2,11 +2,12 @@ import { ReportArguments, ControlResponse } from "@satellite-earth/core/types"; import _throttle from "lodash.throttle"; import BakeryControlApi from "../classes/bakery/control-api"; -import { controlApi } from "./bakery"; +import { controlApi$ } from "./bakery"; import Report from "../classes/bakery/reports/report"; import SuperMap from "../classes/super-map"; import { logger } from "../helpers/debug"; import { ReportClasses, ReportTypes } from "../classes/bakery/reports"; +import { BehaviorSubject } from "rxjs"; class ReportManager { log = logger.extend("ReportManager"); @@ -86,11 +87,16 @@ class ReportManager { } } -const reportManagerService = controlApi ? new ReportManager(controlApi) : undefined; +const reportManager$ = new BehaviorSubject(null); + +controlApi$.subscribe((api) => { + if (api) reportManager$.next(new ReportManager(api)); + else reportManager$.next(null); +}); if (import.meta.env.DEV) { // @ts-expect-error - window.reportManagerService = reportManagerService; + window.reportManager$ = reportManager$; } -export default reportManagerService; +export default reportManager$; diff --git a/src/services/web-push-notifications.ts b/src/services/web-push-notifications.ts index ec235252f..5d75b552c 100644 --- a/src/services/web-push-notifications.ts +++ b/src/services/web-push-notifications.ts @@ -1,7 +1,7 @@ import { type WebPushChannel } from "@satellite-earth/core/types/control-api/notifications.js"; import { nanoid } from "nanoid"; -import { controlApi } from "./bakery"; +import { getControlApi } from "./bakery"; import { BehaviorSubject } from "rxjs"; import { serviceWorkerRegistration } from "./worker"; import localSettings from "./local-settings"; @@ -14,6 +14,8 @@ serviceWorkerRegistration.subscribe(async (registration) => { }); export async function enableNotifications() { + const controlApi = getControlApi(); + if (!controlApi) throw new Error("Missing control api"); const subscription = await serviceWorkerRegistration.value?.pushManager.subscribe({ userVisibleOnly: true, @@ -42,6 +44,8 @@ export async function enableNotifications() { } export async function disableNotifications() { + const controlApi = getControlApi(); + if (pushSubscription.value) { const key = pushSubscription.value.toJSON().keys?.p256dh; if (key) controlApi?.send(["CONTROL", "NOTIFICATIONS", "UNREGISTER", key]); diff --git a/src/views/bakery/settings/index.tsx b/src/views/bakery/settings/index.tsx deleted file mode 100644 index 62001a41e..000000000 --- a/src/views/bakery/settings/index.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { Divider, Flex, Text } from "@chakra-ui/react"; -import { Outlet, useMatch } from "react-router-dom"; - -import SimpleHeader from "../../../components/layout/presets/simple-header"; -import { useBreakpointValue } from "../../../providers/global/breakpoint-provider"; -import SimpleNavItem from "../../../components/layout/presets/simple-nav-item"; -import { ErrorBoundary } from "../../../components/error-boundary"; - -export default function SettingsView() { - const match = useMatch("/settings"); - const isMobile = useBreakpointValue({ base: true, lg: false }); - const showMenu = !isMobile || !!match; - - if (showMenu) { - return ( - - - - - Display - Notifications - - - - Node - - - - General - Network - Service Logs - - - {!isMobile && ( - - - - )} - - ); - } - - return ( - - - - ); -} diff --git a/src/views/bakery/connect/auth.tsx b/src/views/settings/bakery/connect/auth.tsx similarity index 87% rename from src/views/bakery/connect/auth.tsx rename to src/views/settings/bakery/connect/auth.tsx index 2938d7086..4f6d05381 100644 --- a/src/views/bakery/connect/auth.tsx +++ b/src/views/settings/bakery/connect/auth.tsx @@ -14,15 +14,15 @@ import { useDisclosure, useToast, } from "@chakra-ui/react"; - -import bakery, { setBakeryURL } from "../../../services/bakery"; -import Panel from "../../../components/dashboard/panel"; -import useCurrentAccount from "../../../hooks/use-current-account"; -import accountService from "../../../services/account"; -import { useSigningContext } from "../../../providers/global/signing-provider"; import { useObservable } from "applesauce-react/hooks"; -export function PersonalNodeAuthPage() { +import useCurrentAccount from "../../../../hooks/use-current-account"; +import { useSigningContext } from "../../../../providers/global/signing-provider"; +import { bakery$, setBakeryURL } from "../../../../services/bakery"; +import accountService from "../../../../services/account"; +import Panel from "../../../../components/dashboard/panel"; + +export function BakeryAuthPage() { const toast = useToast(); const navigate = useNavigate(); const account = useCurrentAccount(); @@ -30,6 +30,7 @@ export function PersonalNodeAuthPage() { const [search] = useSearchParams(); const remember = useDisclosure({ defaultIsOpen: true }); const location = useLocation(); + const bakery = useObservable(bakery$); const { register, handleSubmit, formState } = useForm({ defaultValues: { auth: search.get("auth") ?? "" }, @@ -87,7 +88,7 @@ export function PersonalNodeAuthPage() { Remember Me - @@ -125,11 +126,12 @@ export function PersonalNodeAuthPage() { export default function BakeryAuthView() { const location = useLocation(); + const bakery = useObservable(bakery$); const authenticated = useObservable(bakery?.authenticated); if (authenticated) { return ; } - return ; + return ; } diff --git a/src/views/bakery/connect/index.tsx b/src/views/settings/bakery/connect/index.tsx similarity index 67% rename from src/views/bakery/connect/index.tsx rename to src/views/settings/bakery/connect/index.tsx index 6d09509d7..29667334f 100644 --- a/src/views/bakery/connect/index.tsx +++ b/src/views/settings/bakery/connect/index.tsx @@ -1,30 +1,21 @@ import { Navigate, useLocation, useNavigate, useSearchParams } from "react-router-dom"; -import { - Box, - Button, - Code, - Flex, - FormControl, - FormHelperText, - FormLabel, - Heading, - Input, - Text, -} from "@chakra-ui/react"; +import { Box, Button, Code, Flex, FormControl, FormLabel, Heading, Input, Text } from "@chakra-ui/react"; import { useForm } from "react-hook-form"; -import { Link as RouterLink } from "react-router-dom"; import { useObservable } from "applesauce-react/hooks"; -import bakery, { setBakeryURL } from "../../../services/bakery"; -import QRCodeScannerButton from "../../../components/qr-code/qr-code-scanner-button"; -import TextButton from "../../../components/dashboard/text-button"; +import SimpleView from "../../../../components/layout/presets/simple-view"; +import { bakery$, setBakeryURL } from "../../../../services/bakery"; +import QRCodeScannerButton from "../../../../components/qr-code/qr-code-scanner-button"; +import TextButton from "../../../../components/dashboard/text-button"; function ConnectForm() { const [params] = useSearchParams(); + const bakery = useObservable(bakery$); const { register, handleSubmit, formState, setValue } = useForm({ defaultValues: { url: params.get("relay") ?? bakery?.url ?? "", }, + mode: "all", }); const handleScanData = (data: string) => { @@ -42,31 +33,24 @@ function ConnectForm() { return ( - Bakery Bakery URL - + - This is the URL to your bakery - - {params.has("config") && ( - - )} - - + + ); } function ConnectConfirmation() { const [params] = useSearchParams(); + const bakery = useObservable(bakery$); const relay = params.get("relay"); const navigate = useNavigate(); @@ -95,8 +79,9 @@ function ConnectConfirmation() { ); } -export default function ConnectView() { +export default function BakeryConnectView() { const location = useLocation(); + const bakery = useObservable(bakery$); const connected = useObservable(bakery?.connectedSub); const [params] = useSearchParams(); @@ -110,10 +95,8 @@ export default function ConnectView() { if (isRelayParamEqual) return ; return ( - - - {relayParam ? : } - - + + + ); } diff --git a/src/views/settings/bakery/general-settings/index.tsx b/src/views/settings/bakery/general-settings/index.tsx index b9d18c6d7..f68ad151b 100644 --- a/src/views/settings/bakery/general-settings/index.tsx +++ b/src/views/settings/bakery/general-settings/index.tsx @@ -3,10 +3,13 @@ import { useForm } from "react-hook-form"; import { Button, Flex, FormControl, FormHelperText, FormLabel, Heading, Input, Textarea } from "@chakra-ui/react"; import { useObservable } from "applesauce-react/hooks"; -import personalNode, { controlApi, clearBakeryURL } from "../../../../services/bakery"; +import { controlApi$, clearBakeryURL, bakery$ } from "../../../../services/bakery"; import SimpleView from "../../../../components/layout/presets/simple-view"; +import { Navigate } from "react-router-dom"; -function NodeGeneralSettingsPage() { +function BakeryGeneralSettingsPage() { + const bakery = useObservable(bakery$); + const controlApi = useObservable(controlApi$); const config = useObservable(controlApi?.config); const { register, handleSubmit, formState, reset } = useForm({ defaultValues: config || {}, @@ -30,22 +33,19 @@ function NodeGeneralSettingsPage() { }); const disconnect = () => { - if (confirm("Disconnect from personal node?")) { - clearBakeryURL(); - } + if (confirm("Disconnect from bakery?")) clearBakeryURL(); }; return ( Bakery URL - - - + + + - @@ -75,5 +75,9 @@ function NodeGeneralSettingsPage() { } export default function BakeryGeneralSettingsView() { - return <>{personalNode ? : Missing personal node connection}; + const bakery = useObservable(bakery$); + + if (!bakery) return ; + + return ; } diff --git a/src/views/settings/bakery/network/gossip.tsx b/src/views/settings/bakery/network/gossip.tsx index da75e9908..c8394b0e5 100644 --- a/src/views/settings/bakery/network/gossip.tsx +++ b/src/views/settings/bakery/network/gossip.tsx @@ -5,10 +5,11 @@ import { safeRelayUrl } from "applesauce-core/helpers"; import { useObservable } from "applesauce-react/hooks"; import useAsyncErrorHandler from "../../../../hooks/use-async-error-handler"; -import { controlApi } from "../../../../services/bakery"; +import { controlApi$ } from "../../../../services/bakery"; import { RelayFavicon } from "../../../../components/relay-favicon"; function BroadcastRelay({ relay }: { relay: string }) { + const controlApi = useObservable(controlApi$); const config = useObservable(controlApi?.config); const remove = useAsyncErrorHandler(async () => { if (!config) return; @@ -37,6 +38,7 @@ function BroadcastRelay({ relay }: { relay: string }) { } function AddRelayForm() { + const controlApi = useObservable(controlApi$); const config = useObservable(controlApi?.config); const { register, handleSubmit, reset } = useForm({ defaultValues: { url: "" } }); @@ -59,6 +61,7 @@ function AddRelayForm() { } function IntervalSelect() { + const controlApi = useObservable(controlApi$); const config = useObservable(controlApi?.config); return ( @@ -81,6 +84,7 @@ function IntervalSelect() { } export default function GossipSettings() { + const controlApi = useObservable(controlApi$); const config = useObservable(controlApi?.config); return ( diff --git a/src/views/settings/bakery/network/hyper-outbound.tsx b/src/views/settings/bakery/network/hyper-outbound.tsx index 745cd958a..f83c9bfaf 100644 --- a/src/views/settings/bakery/network/hyper-outbound.tsx +++ b/src/views/settings/bakery/network/hyper-outbound.tsx @@ -3,9 +3,10 @@ import { Alert, AlertIcon, FormControl, FormHelperText, Switch } from "@chakra-u import { useObservable } from "applesauce-react/hooks"; import useNetworkOverviewReport from "../../../../hooks/reports/use-network-status-report"; -import { controlApi } from "../../../../services/bakery"; +import { controlApi$ } from "../../../../services/bakery"; export default function HyperOutboundStatus() { + const controlApi = useObservable(controlApi$); const config = useObservable(controlApi?.config); const status = useNetworkOverviewReport(); diff --git a/src/views/settings/bakery/network/hyper.tsx b/src/views/settings/bakery/network/hyper.tsx index 54b2ff056..b332671ac 100644 --- a/src/views/settings/bakery/network/hyper.tsx +++ b/src/views/settings/bakery/network/hyper.tsx @@ -5,9 +5,10 @@ import { useObservable } from "applesauce-react/hooks"; import useNetworkOverviewReport from "../../../../hooks/reports/use-network-status-report"; import HyperInboundStatus from "./hyper-inbound"; import HyperOutboundStatus from "./hyper-outbound"; -import { controlApi } from "../../../../services/bakery"; +import { controlApi$ } from "../../../../services/bakery"; export default function HyperNetworkStatus() { + const controlApi = useObservable(controlApi$); const config = useObservable(controlApi?.config); const status = useNetworkOverviewReport(); diff --git a/src/views/settings/bakery/network/i2p-outbound.tsx b/src/views/settings/bakery/network/i2p-outbound.tsx index 46bc18f52..389c316e0 100644 --- a/src/views/settings/bakery/network/i2p-outbound.tsx +++ b/src/views/settings/bakery/network/i2p-outbound.tsx @@ -2,10 +2,11 @@ import { ReactNode } from "react"; import { Alert, AlertIcon, FormControl, FormHelperText, Switch } from "@chakra-ui/react"; import { useObservable } from "applesauce-react/hooks"; -import { controlApi } from "../../../../services/bakery"; +import { controlApi$ } from "../../../../services/bakery"; import useNetworkOverviewReport from "../../../../hooks/reports/use-network-status-report"; export default function I2POutboundStatus() { + const controlApi = useObservable(controlApi$); const config = useObservable(controlApi?.config); const status = useNetworkOverviewReport(); diff --git a/src/views/settings/bakery/network/i2p.tsx b/src/views/settings/bakery/network/i2p.tsx index 07f4fcdf9..fae03fda8 100644 --- a/src/views/settings/bakery/network/i2p.tsx +++ b/src/views/settings/bakery/network/i2p.tsx @@ -4,10 +4,11 @@ import { useObservable } from "applesauce-react/hooks"; import I2POutboundStatus from "./i2p-outbound"; import I2PInboundStatus from "./i2p-inbound"; -import { controlApi } from "../../../../services/bakery"; +import { controlApi$ } from "../../../../services/bakery"; import useNetworkOverviewReport from "../../../../hooks/reports/use-network-status-report"; export default function I2PNetworkStatus() { + const controlApi = useObservable(controlApi$); const config = useObservable(controlApi?.config); const status = useNetworkOverviewReport(); diff --git a/src/views/settings/bakery/network/tor-outbound.tsx b/src/views/settings/bakery/network/tor-outbound.tsx index 395e409b1..08909c558 100644 --- a/src/views/settings/bakery/network/tor-outbound.tsx +++ b/src/views/settings/bakery/network/tor-outbound.tsx @@ -2,10 +2,11 @@ import { ReactNode } from "react"; import { Alert, AlertIcon, FormControl, FormHelperText, Switch } from "@chakra-ui/react"; import { useObservable } from "applesauce-react/hooks"; -import { controlApi } from "../../../../services/bakery"; +import { controlApi$ } from "../../../../services/bakery"; import useNetworkOverviewReport from "../../../../hooks/reports/use-network-status-report"; export default function TorOutboundStatus() { + const controlApi = useObservable(controlApi$); const config = useObservable(controlApi?.config); const status = useNetworkOverviewReport(); diff --git a/src/views/settings/bakery/network/tor.tsx b/src/views/settings/bakery/network/tor.tsx index b6e4a279e..354d75eee 100644 --- a/src/views/settings/bakery/network/tor.tsx +++ b/src/views/settings/bakery/network/tor.tsx @@ -4,10 +4,11 @@ import { useObservable } from "applesauce-react/hooks"; import TorOutboundStatus from "./tor-outbound"; import TorInboundStatus from "./tor-inbound"; -import { controlApi } from "../../../../services/bakery"; +import { controlApi$ } from "../../../../services/bakery"; import useNetworkOverviewReport from "../../../../hooks/reports/use-network-status-report"; export default function TorNetworkStatus() { + const controlApi = useObservable(controlApi$); const config = useObservable(controlApi?.config); const status = useNetworkOverviewReport(); diff --git a/src/views/settings/bakery/notifications/index.tsx b/src/views/settings/bakery/notifications/index.tsx index d10cabcbb..5db6ce388 100644 --- a/src/views/settings/bakery/notifications/index.tsx +++ b/src/views/settings/bakery/notifications/index.tsx @@ -6,11 +6,12 @@ import { useObservable } from "applesauce-react/hooks"; import NtfyNotificationSettings from "./ntfy"; import OtherSubscriptions from "./other"; import WebPushNotificationSettings from "./web-push"; -import { controlApi } from "../../../../services/bakery"; +import { controlApi$ } from "../../../../services/bakery"; import SimpleView from "../../../../components/layout/presets/simple-view"; import { CAP_IS_NATIVE, CAP_IS_WEB } from "../../../../env"; function EmailForm() { + const controlApi = useObservable(controlApi$); const config = useObservable(controlApi?.config); const { register, handleSubmit, reset } = useForm({ defaultValues: { email: config?.notificationEmail ?? "" }, diff --git a/src/views/settings/bakery/notifications/ntfy.tsx b/src/views/settings/bakery/notifications/ntfy.tsx index 09b3aabbf..b1d0bf2fa 100644 --- a/src/views/settings/bakery/notifications/ntfy.tsx +++ b/src/views/settings/bakery/notifications/ntfy.tsx @@ -5,13 +5,15 @@ import { kinds, NostrEvent } from "nostr-tools"; import { useObservable } from "applesauce-react/hooks"; import useCurrentAccount from "../../../../hooks/use-current-account"; -import bakery, { controlApi } from "../../../../services/bakery"; +import { bakery$, controlApi$ } from "../../../../services/bakery"; import localSettings from "../../../../services/local-settings"; import useNotificationChannelsReport from "../../../../hooks/reports/use-notification-channels"; import { CopyIconButton } from "../../../../components/copy-icon-button"; import { ExternalLinkIcon } from "../../../../components/icons"; export default function NtfyNotificationSettings() { + const bakery = useObservable(bakery$); + const controlApi = useObservable(controlApi$); const account = useCurrentAccount(); const device = useObservable(localSettings.deviceId); diff --git a/src/views/settings/bakery/notifications/other.tsx b/src/views/settings/bakery/notifications/other.tsx index 6a50841ed..921561e0c 100644 --- a/src/views/settings/bakery/notifications/other.tsx +++ b/src/views/settings/bakery/notifications/other.tsx @@ -2,10 +2,12 @@ import { ReactNode } from "react"; import { Badge, Button, Flex, Heading, Text } from "@chakra-ui/react"; import { NotificationChannel } from "@satellite-earth/core/types/control-api/notifications.js"; -import { controlApi } from "../../../../services/bakery"; +import { controlApi$ } from "../../../../services/bakery"; import useNotificationChannelsReport from "../../../../hooks/reports/use-notification-channels"; +import { useObservable } from "applesauce-react/hooks"; function Channel({ channel }: { channel: NotificationChannel }) { + const controlApi = useObservable(controlApi$); let details: ReactNode = null; switch (channel.type) { diff --git a/src/views/settings/bakery/notifications/web-push.tsx b/src/views/settings/bakery/notifications/web-push.tsx index d8f5d5abe..63b046c74 100644 --- a/src/views/settings/bakery/notifications/web-push.tsx +++ b/src/views/settings/bakery/notifications/web-push.tsx @@ -8,7 +8,7 @@ import { enableNotifications, pushSubscription, } from "../../../../services/web-push-notifications"; -import { controlApi } from "../../../../services/bakery"; +import { controlApi$ } from "../../../../services/bakery"; function WebPushNotificationStatus() { const toast = useToast(); @@ -74,9 +74,10 @@ function WebPushNotificationStatus() { } export default function WebPushNotificationSettings() { + const controlApi = useObservable(controlApi$); useEffect(() => { controlApi?.send(["CONTROL", "NOTIFICATIONS", "GET-VAPID-KEY"]); - }, []); + }, [controlApi]); return ( <> diff --git a/src/views/settings/bakery/service-logs/index.tsx b/src/views/settings/bakery/service-logs/index.tsx index f0982531f..3dc06fc90 100644 --- a/src/views/settings/bakery/service-logs/index.tsx +++ b/src/views/settings/bakery/service-logs/index.tsx @@ -14,16 +14,18 @@ import { useDisclosure, } from "@chakra-ui/react"; import Convert from "ansi-to-html"; +import { useObservable } from "applesauce-react/hooks"; import useLogsReport from "../../../../hooks/reports/use-logs-report"; import Timestamp from "../../../../components/timestamp"; import SimpleView from "../../../../components/layout/presets/simple-view"; -import { controlApi } from "../../../../services/bakery"; +import { controlApi$ } from "../../../../services/bakery"; import ServicesTree from "./service-tree"; const convert = new Convert(); export default function BakeryServiceLogsView() { + const controlApi = useObservable(controlApi$); const [service, setService] = useState(undefined); const { report, logs } = useLogsReport(service); const raw = useDisclosure(); diff --git a/src/views/bakery/setup/index.tsx b/src/views/settings/bakery/setup/index.tsx similarity index 80% rename from src/views/bakery/setup/index.tsx rename to src/views/settings/bakery/setup/index.tsx index bfbfa5278..2640b19f7 100644 --- a/src/views/bakery/setup/index.tsx +++ b/src/views/settings/bakery/setup/index.tsx @@ -5,15 +5,17 @@ import { Navigate } from "react-router-dom"; import { isHexKey } from "applesauce-core/helpers"; import { useObservable } from "applesauce-react/hooks"; -import personalNode, { controlApi, setBakeryURL } from "../../../services/bakery"; -import useRouteSearchValue from "../../../hooks/use-route-search-value"; -import dnsIdentityService from "../../../services/dns-identity"; -import QRCodeScannerButton from "../../../components/qr-code/qr-code-scanner-button"; +import { bakery$, controlApi$, setBakeryURL } from "../../../../services/bakery"; +import useRouteSearchValue from "../../../../hooks/use-route-search-value"; +import dnsIdentityService from "../../../../services/dns-identity"; +import QRCodeScannerButton from "../../../../components/qr-code/qr-code-scanner-button"; export default function BakerySetupView() { const toast = useToast(); const relayParam = useRouteSearchValue("relay"); const authParam = useRouteSearchValue("auth"); + const bakery = useObservable(bakery$); + const controlApi = useObservable(controlApi$); const config = useObservable(controlApi?.config); const { register, setValue, formState, handleSubmit } = useForm({ @@ -23,7 +25,7 @@ export default function BakerySetupView() { const submit = handleSubmit(async (values) => { try { - if (!personalNode) throw new Error("Missing personal node connection"); + if (!bakery) throw new Error("Missing personal node connection"); if (!controlApi) throw new Error("Missing control api connection"); let pubkey: string = ""; @@ -63,7 +65,7 @@ export default function BakerySetupView() { if (!pubkey) throw new Error("Unable to find nostr public key"); if (!authParam.value) throw new Error("Missing auth code"); - await personalNode.authenticate(authParam.value); + await bakery.authenticate(authParam.value); controlApi.send(["CONTROL", "CONFIG", "SET", "owner", pubkey]); } catch (error) { @@ -76,8 +78,8 @@ export default function BakerySetupView() { } if (relayParam.value) { - if (personalNode) { - if (new URL(personalNode.url).toString() !== new URL(relayParam.value).toString()) { + if (bakery) { + if (new URL(bakery.url).toString() !== new URL(relayParam.value).toString()) { setBakeryURL(relayParam.value); } } else setBakeryURL(relayParam.value); @@ -95,7 +97,7 @@ export default function BakerySetupView() { Enter the NIP-05, npub, or hex pubkey of the owner of this node - diff --git a/src/views/settings/index.tsx b/src/views/settings/index.tsx index a3306478e..cdc7bac53 100644 --- a/src/views/settings/index.tsx +++ b/src/views/settings/index.tsx @@ -19,12 +19,13 @@ import Image01 from "../../components/icons/image-01"; import UserAvatar from "../../components/user/user-avatar"; import VersionButton from "../../components/version-button"; import SimpleHeader from "../../components/layout/presets/simple-header"; -import bakery from "../../services/bakery"; import SimpleNavItem from "../../components/layout/presets/simple-nav-item"; import Bell01 from "../../components/icons/bell-01"; import Share07 from "../../components/icons/share-07"; import Database01 from "../../components/icons/database-01"; import Mail02 from "../../components/icons/mail-02"; +import { useObservable } from "applesauce-react/hooks"; +import { bakery$ } from "../../services/bakery"; function DividerHeader({ title }: { title: string }) { return ( @@ -44,6 +45,8 @@ export default function SettingsView() { const isMobile = useBreakpointValue({ base: true, lg: false }); const showMenu = !isMobile || !!match; + const bakery = useObservable(bakery$) + if (showMenu) return ( @@ -87,10 +90,10 @@ export default function SettingsView() { Database Tools - {bakery && ( + {bakery ? ( <> - - Bakery + + General }> Notifications @@ -101,6 +104,11 @@ export default function SettingsView() { Service Logs + ) : ( + <> + + Connect + )}