allow bakery url to be changed without reload

This commit is contained in:
hzrd149 2025-01-15 11:21:22 -06:00
parent dabf5c5061
commit 05942d7f68
30 changed files with 238 additions and 265 deletions

View File

@ -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: <PrivacySettings /> },
{ path: "lightning", element: <LightningSettings /> },
{ path: "performance", element: <PerformanceSettings /> },
{ path: "bakery/connect", element: <BakeryConnectView /> },
{
path: "bakery",
element: (
<RequireBakery>
<Outlet />
</RequireBakery>
),
children: [
{ path: "", element: <BakeryGeneralSettingsView /> },
{
path: "auth",
element: <BakeryAuthView />,
},
{ path: "notifications", element: <NotificationSettingsView /> },
{
path: "network",
@ -562,52 +572,6 @@ const router = createBrowserRouter([
},
],
},
{
path: "/bakery",
children: [
{
path: "connect",
children: [
{ path: "", element: <ConnectView /> },
{
path: "auth",
element: (
<RequireBakery>
<ConnectionStatus />
<BakeryAuthView />
</RequireBakery>
),
},
],
},
{
path: "setup",
element: <BakerySetupView />,
},
{
path: "",
element: (
<RequireBakery>
<RequireCurrentAccount>
<RequireBakeryAuth>
<AppLayout />
</RequireBakeryAuth>
</RequireCurrentAccount>
</RequireBakery>
),
children: [
{
path: "search",
element: <SearchView />,
},
{
path: "",
element: <HomeView />,
},
],
},
],
},
]);
export const App = () => (

View File

@ -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;

View File

@ -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 (
<FormControl>
<FormLabel>{label}</FormLabel>
<Code bg="none" userSelect="all" fontFamily="monospace" mr="auto" maxW="full">
{value}
</Code>
<Flex gap="2" w="full" alignItems="center" wrap="wrap">
<CopyIconButton variant="link" value={value} fontFamily="monospace" aria-label="Copy value" />
{qr && (
<TextButton variant="link" onClick={() => setShowQR((v) => !v)}>
[qr]
</TextButton>
)}
<Flex gap="2">
<Code bg="none" userSelect="all" fontFamily="monospace" maxW="full" whiteSpace="pre" overflow="auto" p="1">
{value}
</Code>
<ButtonGroup size="sm" variant="ghost">
<CopyIconButton value={value} fontFamily="monospace" aria-label="Copy value" />
{qr && (
<IconButton
onClick={() => setShowQR((v) => !v)}
icon={<QrCodeIcon boxSize={5} />}
aria-label="show qrcode"
/>
)}
</ButtonGroup>
</Flex>
{showQR && <QrCodeSvg content={value} style={{ maxWidth: "3in", marginTop: "1em" }} />}
{children}

View File

@ -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<ModalProps, "children">) {
const account = useCurrentAccount();
const bakery = useObservable(bakery$);
return (
<Drawer placement="left" onClose={onClose} isOpen={isOpen} {...props}>

View File

@ -20,6 +20,7 @@ export default function SimpleView({
<Flex
direction="column"
overflowY="auto"
overflowX="hidden"
px={flush ? 0 : "4"}
pt={flush ? 0 : "4"}
pb={flush ? 0 : "max(1rem, var(--safe-bottom))"}

View File

@ -3,11 +3,12 @@ import { Button, Flex, Heading, Spinner } from "@chakra-ui/react";
import { To, useLocation, Link as RouterLink, useNavigate } from "react-router-dom";
import { useObservable } from "applesauce-react/hooks";
import bakery from "../../services/bakery";
import { useSigningContext } from "../../providers/global/signing-provider";
import { bakery$ } from "../../services/bakery";
export default function RequireBakeryAuth({ children }: PropsWithChildren) {
const location = useLocation();
const bakery = useObservable(bakery$);
const isFirstAuthentication = useObservable(bakery?.isFirstAuthentication);
const connected = useObservable(bakery?.connectedSub);
const authenticated = useObservable(bakery?.authenticated);

View File

@ -4,11 +4,12 @@ import { Navigate, To, useLocation } from "react-router-dom";
import { Link as RouterLink } from "react-router-dom";
import { useObservable } from "applesauce-react/hooks";
import bakery from "../../services/bakery";
import useReconnectAction from "../../hooks/use-reconnect-action";
import { bakery$ } from "../../services/bakery";
function InitialConnectionOverlay() {
const location = useLocation();
const bakery = useObservable(bakery$);
const { error } = useReconnectAction();
@ -24,7 +25,7 @@ function InitialConnectionOverlay() {
variant="link"
mt="4"
as={RouterLink}
to="/bakery/connect"
to="/settings/bakery/connect"
replace
state={{ back: (location.state?.back ?? location) satisfies To }}
>
@ -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 <Navigate to="/bakery/connect" replace state={{ back: (location.state?.back ?? location) satisfies To }} />;
return (
<Navigate
to="/settings/bakery/connect"
replace
state={{ back: (location.state?.back ?? location) satisfies To }}
/>
);
if (bakery && isFirstConnection && connected === false) return <InitialConnectionOverlay />;

View File

@ -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<Error>();

View File

@ -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<T extends keyof ReportArguments>(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;
}

View File

@ -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<BakeryConnection | null>(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<BakeryControlApi | null>(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;
}

View File

@ -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<ReportManager | null>(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$;

View File

@ -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]);

View File

@ -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 (
<Flex overflow="hidden" flex={1} direction={{ base: "column", lg: "row" }}>
<Flex overflowY="auto" overflowX="hidden" h="full" minW="xs" direction="column">
<SimpleHeader title="Settings" />
<Flex direction="column" p="2" gap="2">
<SimpleNavItem to="/settings/display">Display</SimpleNavItem>
<SimpleNavItem to="/settings/notifications">Notifications</SimpleNavItem>
<Flex alignItems="center" gap="2">
<Divider />
<Text fontWeight="bold" fontSize="md">
Node
</Text>
<Divider />
</Flex>
<SimpleNavItem to="/settings/general">General</SimpleNavItem>
<SimpleNavItem to="/settings/networking">Network</SimpleNavItem>
<SimpleNavItem to="/settings/logs">Service Logs</SimpleNavItem>
</Flex>
</Flex>
{!isMobile && (
<ErrorBoundary>
<Outlet />
</ErrorBoundary>
)}
</Flex>
);
}
return (
<ErrorBoundary>
<Outlet />
</ErrorBoundary>
);
}

View File

@ -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() {
<Checkbox isChecked={remember.isOpen} onChange={remember.onToggle}>
Remember Me
</Checkbox>
<Button type="submit" size="sm" colorScheme="brand">
<Button type="submit" size="sm" colorScheme="primary">
Login
</Button>
</Flex>
@ -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 <Navigate to={location.state?.back ?? "/"} replace />;
}
return <PersonalNodeAuthPage />;
return <BakeryAuthPage />;
}

View File

@ -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 (
<Flex as="form" onSubmit={submit} gap="2" direction="column">
<Heading size="lg">Bakery</Heading>
<FormControl>
<FormLabel>Bakery URL</FormLabel>
<Flex gap="2">
<Input type="text" {...register("url", { required: true })} isRequired placeholder="ws://127.0.0.1:2012" />
<Input type="text" {...register("url", { required: true })} isRequired placeholder="ws://localhost:2012" />
<QRCodeScannerButton onData={handleScanData} />
</Flex>
<FormHelperText>This is the URL to your bakery</FormHelperText>
</FormControl>
<Flex>
{params.has("config") && (
<Button as={RouterLink} to="/" p="2" variant="link">
Back
</Button>
)}
<Button isLoading={formState.isSubmitting} type="submit" ml="auto" colorScheme="brand">
Connect
</Button>
</Flex>
<Button isLoading={formState.isSubmitting} type="submit" ml="auto" colorScheme="primary">
Connect
</Button>
</Flex>
);
}
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 <Navigate replace to="/" />;
return (
<Flex w="full" h="full" alignItems="center" justifyContent="center">
<Flex direction="column" gap="2" w="full" maxW="sm" m="4">
{relayParam ? <ConnectConfirmation /> : <ConnectForm />}
</Flex>
</Flex>
<SimpleView title="Connect bakery" maxW="4xl">
<ConnectForm />
</SimpleView>
);
}

View File

@ -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 (
<SimpleView title="Node Settings">
<FormControl>
<FormLabel>Bakery URL</FormLabel>
<Flex gap="2">
<Input readOnly value={personalNode!.url} maxW="xs" />
<Button isDisabled>Change</Button>
<Flex maxW="lg" gap="2">
<Input readOnly value={bakery!.url} />
<Button colorScheme="red" onClick={disconnect} variant="ghost" flexShrink={0}>
disconnect
</Button>
</Flex>
<Button variant="link" colorScheme="red" mt="2" onClick={disconnect}>
disconnect
</Button>
</FormControl>
<Flex as="form" onSubmit={submit} direction="column" maxW="lg" gap="4">
@ -75,5 +75,9 @@ function NodeGeneralSettingsPage() {
}
export default function BakeryGeneralSettingsView() {
return <>{personalNode ? <NodeGeneralSettingsPage /> : <Heading>Missing personal node connection</Heading>}</>;
const bakery = useObservable(bakery$);
if (!bakery) return <Navigate to="/settings/bakery/connect" />;
return <BakeryGeneralSettingsPage />;
}

View File

@ -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 (

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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 ?? "" },

View File

@ -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);

View File

@ -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) {

View File

@ -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 (
<>

View File

@ -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<string | undefined>(undefined);
const { report, logs } = useLogsReport(service);
const raw = useDisclosure();

View File

@ -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() {
</Flex>
<FormHelperText>Enter the NIP-05, npub, or hex pubkey of the owner of this node</FormHelperText>
</FormControl>
<Button type="submit" colorScheme="brand" isLoading={formState.isSubmitting} ml="auto">
<Button type="submit" colorScheme="primary" isLoading={formState.isSubmitting} ml="auto">
Setup
</Button>
</Flex>

View File

@ -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 (
<Flex overflow="hidden" flex={1} direction={{ base: "column", lg: "row" }}>
@ -87,10 +90,10 @@ export default function SettingsView() {
Database Tools
</SimpleNavItem>
{bakery && (
{bakery ? (
<>
<DividerHeader title="Relay" />
<SimpleNavItem to="/settings/bakery">Bakery</SimpleNavItem>
<DividerHeader title="bakery" />
<SimpleNavItem to="/settings/bakery">General</SimpleNavItem>
<SimpleNavItem to="/settings/bakery/notifications" leftIcon={<Bell01 boxSize={5} />}>
Notifications
</SimpleNavItem>
@ -101,6 +104,11 @@ export default function SettingsView() {
Service Logs
</SimpleNavItem>
</>
) : (
<>
<DividerHeader title="bakery" />
<SimpleNavItem to="/settings/bakery/connect">Connect</SimpleNavItem>
</>
)}
<Flex alignItems="center">