show relay auth requests

add default relay auth option
This commit is contained in:
hzrd149 2024-05-30 11:06:47 -05:00
parent 0b887ac495
commit b4c4c7a9f2
24 changed files with 415 additions and 164 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Show relay authentication requests

View File

@ -68,7 +68,7 @@
"nanoid": "^5.0.4",
"ngeohash": "^0.6.3",
"nostr-idb": "^2.1.4",
"nostr-tools": "^2.5.2",
"nostr-tools": "2.5.2",
"nostr-wasm": "^0.1.0",
"prettier": "^3.2.5",
"react": "^18.2.0",

View File

@ -54,6 +54,13 @@ export default class ChunkedRequest {
async loadNextChunk() {
if (this.loading) return;
// check if its possible to subscribe to this relay
if (!relayPoolService.canSubscribe(this.relay)) {
this.log("Cant subscribe to relay, aborting");
return;
}
this.loading = true;
if (!this.relay.connected) {
@ -93,6 +100,9 @@ export default class ChunkedRequest {
this.process.active = false;
res(gotEvents);
},
onclose: (reason) => {
relayPoolService.handleRelayNotice(this.relay, reason);
},
});
});
}

View File

@ -76,7 +76,6 @@ export default class MultiSubscription {
this.relays.add(relay);
}
this.process.relays = new Set(this.relays);
this.updateSubscriptions();
}

View File

@ -44,6 +44,9 @@ export default class PersistentSubscription {
// this.subscription.close();
// }
// check if its possible to subscribe to this relay
if (!relayPoolService.canSubscribe(this.relay)) return;
this.closed = false;
this.eosed = false;
this.process.active = true;
@ -59,7 +62,9 @@ export default class PersistentSubscription {
onclose: (reason) => {
if (!this.closed) {
// unexpected close, reconnect?
console.log("Unexpected closed", this.relay, reason);
// console.log("Unexpected closed", this.relay, reason);
relayPoolService.handleRelayNotice(this.relay, reason);
this.closed = true;
this.process.active = false;

View File

@ -8,6 +8,8 @@ import Subject, { PersistentSubject } from "./subject";
import verifyEventMethod from "../services/verify-event";
import SuperMap from "./super-map";
import processManager from "../services/process-manager";
import signingService from "../services/signing";
import accountService from "../services/account";
export type Notice = {
message: string;
@ -15,6 +17,8 @@ export type Notice = {
relay: AbstractRelay;
};
export type RelayAuthMode = "always" | "ask" | "never";
export default class RelayPool {
relays = new Map<string, AbstractRelay>();
onRelayCreated = new Subject<AbstractRelay>();
@ -25,9 +29,12 @@ export default class RelayPool {
connectionErrors = new SuperMap<AbstractRelay, Error[]>(() => []);
connecting = new SuperMap<AbstractRelay, PersistentSubject<boolean>>(() => new PersistentSubject(false));
challenges = new SuperMap<AbstractRelay, Subject<string>>(() => new Subject<string>());
authForPublish = new SuperMap<AbstractRelay, Subject<boolean>>(() => new Subject());
authForSubscribe = new SuperMap<AbstractRelay, Subject<boolean>>(() => new Subject());
authenticated = new SuperMap<AbstractRelay, Subject<boolean>>(() => new Subject());
log = logger.extend("RelayPool");
getRelay(relayOrUrl: string | URL | AbstractRelay) {
@ -62,7 +69,10 @@ export default class RelayPool {
const key = url.toString();
if (!this.relays.has(key)) {
const r = new AbstractRelay(key, { verifyEvent: verifyEventMethod });
r._onauth = (challenge) => this.onRelayChallenge.next([r, challenge]);
r._onauth = (challenge) => {
this.onRelayChallenge.next([r, challenge]);
this.challenges.get(r).next(challenge);
};
r.onnotice = (notice) => this.handleRelayNotice(r, notice);
this.relays.set(key, r);
@ -112,12 +122,94 @@ export default class RelayPool {
}
}
getRelayAuthStorageKey(relayOrUrl: string | URL | AbstractRelay) {
let relay = this.getRelay(relayOrUrl);
return `${relay!.url}-auth-mode`;
}
getRelayAuthMode(relayOrUrl: string | URL | AbstractRelay): RelayAuthMode | undefined {
let relay = this.getRelay(relayOrUrl);
if (!relay) return;
const defaultMode = (localStorage.getItem(`default-auth-mode`) as RelayAuthMode) ?? undefined;
const mode = (localStorage.getItem(this.getRelayAuthStorageKey(relay)) as RelayAuthMode) ?? undefined;
return mode || defaultMode;
}
setRelayAuthMode(relayOrUrl: string | URL | AbstractRelay, mode: RelayAuthMode) {
let relay = this.getRelay(relayOrUrl);
if (!relay) return;
localStorage.setItem(this.getRelayAuthStorageKey(relay), mode);
}
pendingAuth = new Map<AbstractRelay, Promise<string | undefined>>();
async authenticate(
relayOrUrl: string | URL | AbstractRelay,
sign: Parameters<AbstractRelay["auth"]>[0],
quite = true,
) {
let relay = this.getRelay(relayOrUrl);
if (!relay) return;
const pending = this.pendingAuth.get(relay);
if (pending) return pending;
if (this.getRelayAuthMode(relay) === "never") throw new Error("Auth disabled for relay");
if (!relay.connected) throw new Error("Not connected");
const promise = new Promise<string | undefined>(async (res) => {
if (!relay) return;
try {
const message = await relay.auth(sign);
this.authenticated.get(relay).next(true);
res(message);
} catch (e) {
e = e || new Error("Unknown error");
if (e instanceof Error) {
this.log(`Failed to authenticate to ${relay.url}`, e.message);
}
this.authenticated.get(relay).next(false);
if (!quite) throw e;
}
this.pendingAuth.delete(relay);
});
this.pendingAuth.set(relay, promise);
return await promise;
}
canSubscribe(relayOrUrl: string | URL | AbstractRelay) {
let relay = this.getRelay(relayOrUrl);
if (!relay) return false;
return this.authForSubscribe.get(relay).value !== false;
}
handleRelayNotice(relay: AbstractRelay, message: string) {
const subject = this.notices.get(relay);
subject.next([...subject.value, { message, date: dayjs().unix(), relay }]);
const authForSubscribe = this.authForSubscribe.get(relay);
if (!authForSubscribe.value) authForSubscribe.next(true);
if (message.includes("auth-required")) {
const authForSubscribe = this.authForSubscribe.get(relay);
if (!authForSubscribe.value) authForSubscribe.next(true);
const account = accountService.current.value;
if (account) {
const authMode = this.getRelayAuthMode(relay);
if (authMode === "always") {
this.authenticate(relay, (draft) => {
return signingService.requestSignature(draft, account);
}).then(() => {
this.log(`Automatically authenticated to ${relay.url}`);
});
}
}
}
}
disconnectFromUnused() {

View File

@ -133,7 +133,7 @@ export function EmbeddedImage({ src, event, imageProps, ...props }: EmbeddedImag
);
}
export const GalleryImage = forwardRef<HTMLImageElement|null, EmbeddedImageProps>(
export const GalleryImage = forwardRef<HTMLImageElement | null, EmbeddedImageProps>(
({ src, event, imageProps, ...props }, ref) => {
const thumbnail = useImageThumbnail(src);

View File

@ -12,7 +12,7 @@ import useSubject from "../../hooks/use-subject";
import { offlineMode } from "../../services/offline-mode";
import WifiOff from "../icons/wifi-off";
import { useTaskManagerContext } from "../../views/task-manager/provider";
import TaskManagerButton from "./task-manager-button";
import TaskManagerButtons from "./task-manager-buttons";
const hideScrollbar = css`
-ms-overflow-style: none;
@ -89,7 +89,7 @@ export default function DesktopSideNav(props: Omit<FlexProps, "children">) {
</Button>
)}
</Flex>
<TaskManagerButton mt="auto" flexShrink={0} py="2" />
<TaskManagerButtons mt="auto" flexShrink={0} />
</Flex>
);
}

View File

@ -15,7 +15,7 @@ import { Link as RouterLink } from "react-router-dom";
import AccountSwitcher from "./account-switcher";
import useCurrentAccount from "../../hooks/use-current-account";
import NavItems from "./nav-items";
import TaskManagerButton from "./task-manager-button";
import TaskManagerButtons from "./task-manager-buttons";
export default function MobileSideDrawer({ ...props }: Omit<DrawerProps, "children">) {
const account = useCurrentAccount();
@ -40,7 +40,7 @@ export default function MobileSideDrawer({ ...props }: Omit<DrawerProps, "childr
Sign in
</Button>
)}
<TaskManagerButton mt="auto" flexShrink={0} py="2" />
<TaskManagerButtons mt="auto" flexShrink={0} />
</DrawerBody>
</DrawerContent>
</Drawer>

View File

@ -1,23 +0,0 @@
import { useContext } from "react";
import { Button, ButtonProps } from "@chakra-ui/react";
import { PublishContext } from "../../providers/global/publish-provider";
import { useTaskManagerContext } from "../../views/task-manager/provider";
import PublishActionStatusTag from "../../views/task-manager/publish-log/action-status-tag";
export default function TaskManagerButton({ ...props }: Omit<ButtonProps, "children">) {
const { log } = useContext(PublishContext);
const { openTaskManager } = useTaskManagerContext();
return (
<Button
variant="link"
justifyContent="space-between"
onClick={() => openTaskManager(log.length === 0 ? "/relays" : "/publish-log")}
{...props}
>
Task Manager
{log.length > 0 && <PublishActionStatusTag action={log[log.length - 1]} />}
</Button>
);
}

View File

@ -0,0 +1,46 @@
import { useContext } from "react";
import { Button, Flex, FlexProps, IconButton } from "@chakra-ui/react";
import { PublishContext } from "../../providers/global/publish-provider";
import { useTaskManagerContext } from "../../views/task-manager/provider";
import PublishActionStatusTag from "../../views/task-manager/publish-log/action-status-tag";
import PasscodeLock from "../icons/passcode-lock";
import relayPoolService from "../../services/relay-pool";
export default function TaskManagerButtons({ ...props }: Omit<FlexProps, "children">) {
const { log } = useContext(PublishContext);
const { openTaskManager } = useTaskManagerContext();
const pendingAuth = Array.from(relayPoolService.challenges.entries()).filter(
([r, c]) => r.connected && !!c.value && !relayPoolService.authenticated.get(r).value,
);
return (
<Flex gap="2" {...props}>
<Button
justifyContent="space-between"
onClick={() => openTaskManager(log.length === 0 ? "/relays" : "/publish-log")}
py="2"
variant="link"
w="full"
>
Task Manager
{log.length > 0 && <PublishActionStatusTag action={log[log.length - 1]} />}
</Button>
{pendingAuth.length > 0 && (
<Button
leftIcon={<PasscodeLock boxSize={5} />}
aria-label="Pending Auth"
title="Pending Auth"
ml="auto"
size="sm"
variant="ghost"
color="red"
onClick={() => openTaskManager({ pathname: "/relays", search: "?tab=auth" })}
>
{pendingAuth.length}
</Button>
)}
</Flex>
);
}

View File

@ -1,43 +1,83 @@
import { useCallback, useEffect, useState } from "react";
import { useCallback, useState } from "react";
import {
Button,
ButtonProps,
IconButton,
IconButtonProps,
useForceUpdate,
useInterval,
useToast,
} from "@chakra-ui/react";
import { AbstractRelay } from "nostr-tools";
import { Button, useToast } from "@chakra-ui/react";
import relayPoolService from "../../services/relay-pool";
import { useSigningContext } from "../../providers/global/signing-provider";
import PasscodeLock from "../icons/passcode-lock";
import useSubject from "../../hooks/use-subject";
import CheckCircleBroken from "../icons/check-circle-broken";
export default function RelayAuthButton({ relay }: { relay: string | URL | AbstractRelay }) {
export function useRelayChallenge(relay: AbstractRelay) {
return useSubject(relayPoolService.challenges.get(relay));
}
export function useRelayAuthMethod(relay: AbstractRelay) {
const toast = useToast();
const { requestSignature } = useSigningContext();
const r = relayPoolService.getRelay(relay);
if (!r) return null;
const challenge = useRelayChallenge(relay);
// @ts-expect-error
const [challenge, setChallenge] = useState(r.challenge ?? "");
useEffect(() => {
const sub = relayPoolService.onRelayChallenge.subscribe(([relay, challenge]) => {
if (r === relay) setChallenge(challenge);
});
return () => sub.unsubscribe();
}, [r]);
const authenticated = useSubject(relayPoolService.authenticated.get(relay));
const [loading, setLoading] = useState(false);
const auth = useCallback(async () => {
setLoading(true);
try {
const message = await r.auth(requestSignature);
const message = await relayPoolService.authenticate(relay, requestSignature, false);
toast({ description: message || "Success", status: "success" });
} catch (error) {
if (error instanceof Error) toast({ status: "error", description: error.message });
}
setLoading(false);
}, [r, requestSignature]);
}, [relay, requestSignature]);
if (challenge)
return { loading, auth, challenge, authenticated };
}
export function IconRelayAuthButton({
relay,
...props
}: { relay: string | URL | AbstractRelay } & Omit<IconButtonProps, "icon" | "aria-label" | "title">) {
const r = relayPoolService.getRelay(relay);
if (!r) return null;
const update = useForceUpdate();
useInterval(update, 500);
const { challenge, auth, loading, authenticated } = useRelayAuthMethod(r);
if (authenticated) {
return (
<Button onClick={auth} isLoading={loading}>
Authenticate
</Button>
<IconButton
icon={<CheckCircleBroken boxSize={6} />}
aria-label="Authenticated"
title="Authenticated"
colorScheme="green"
{...props}
/>
);
}
if (r.connected && challenge) {
return (
<IconButton
icon={<PasscodeLock boxSize={6} />}
onClick={auth}
isLoading={loading}
aria-label="Authenticate with relay"
title="Authenticate"
{...props}
/>
);
}
return null;
}

View File

@ -0,0 +1,36 @@
import { ChangeEventHandler } from "react";
import { Switch, useForceUpdate, useInterval, useToast } from "@chakra-ui/react";
import { AbstractRelay } from "nostr-tools";
import relayPoolService from "../../services/relay-pool";
import useSubject from "../../hooks/use-subject";
export default function RelayConnectSwitch({ relay }: { relay: string | URL | AbstractRelay }) {
const toast = useToast();
const r = relayPoolService.getRelay(relay);
if (!r) return null;
const update = useForceUpdate();
useInterval(update, 500);
const connecting = useSubject(relayPoolService.connecting.get(r));
const onChange: ChangeEventHandler<HTMLInputElement> = async (e) => {
try {
if (e.target.checked && !r.connected) await relayPoolService.requestConnect(r);
else if (r.connected) r.close();
} catch (error) {
if (error instanceof Error) toast({ status: "error", description: error.message });
}
};
return (
<Switch
isDisabled={connecting}
isChecked={r.connected || connecting}
onChange={onChange}
colorScheme={r.connected ? "green" : "red"}
/>
);
}

View File

@ -1,9 +1,9 @@
import { Badge, useForceUpdate } from "@chakra-ui/react";
import { useInterval } from "react-use";
import relayPoolService from "../services/relay-pool";
import relayPoolService from "../../services/relay-pool";
import { AbstractRelay } from "nostr-tools";
import useSubject from "../hooks/use-subject";
import useSubject from "../../hooks/use-subject";
const getStatusText = (relay: AbstractRelay, connecting = false) => {
if (connecting) return "Connecting...";

View File

@ -6,7 +6,7 @@ import { ChevronLeftIcon } from "../icons";
export default function BackButton({ ...props }: Omit<IconButtonProps, "onClick" | "children" | "aria-label">) {
const navigate = useNavigate();
return (
<IconButton icon={<ChevronLeftIcon />} aria-label="Back" {...props} onClick={() => navigate(-1)}>
<IconButton icon={<ChevronLeftIcon boxSize={6} />} aria-label="Back" {...props} onClick={() => navigate(-1)}>
Back
</IconButton>
);
@ -14,5 +14,7 @@ export default function BackButton({ ...props }: Omit<IconButtonProps, "onClick"
export function BackIconButton({ ...props }: Omit<IconButtonProps, "onClick" | "children" | "aria-label">) {
const navigate = useNavigate();
return <IconButton icon={<ChevronLeftIcon />} aria-label="Back" {...props} onClick={() => navigate(-1)} />;
return (
<IconButton icon={<ChevronLeftIcon boxSize={6} />} aria-label="Back" {...props} onClick={() => navigate(-1)} />
);
}

View File

@ -15,7 +15,7 @@ export function getPageTopic(page: NostrEvent) {
export function getPageSummary(page: NostrEvent, fallback = true) {
const summary = page.tags.find((t) => t[0] === "summary")?.[1];
return summary || (fallback ? page.content.split("\n")[0] : '');
return summary || (fallback ? page.content.split("\n")[0] : "");
}
export function getPageForks(page: NostrEvent) {

View File

@ -97,27 +97,6 @@ export function splitQueryByPubkeys(query: NostrQuery, relayPubkeyMap: Record<st
return filtersByRelay;
}
// NOTE: this is a hack because nostr-tools does not expose the "challenge" field on relays
export function getChallenge(relay: AbstractRelay): string {
// @ts-expect-error
return relay.challenge;
}
export function relayRequest(relay: SimpleRelay, filters: Filter[], opts: SubscriptionOptions = {}) {
return new Promise<NostrEvent[]>((res) => {
const events: NostrEvent[] = [];
const sub = relay.subscribe(filters, {
...opts,
onevent: (e) => events.push(e),
oneose: () => {
sub.close();
res(events);
},
onclose: () => res(events),
});
});
}
// copied from nostr-tools, SimplePool#subscribeMany
export function subscribeMany(relays: string[], filters: Filter[], params: SubscribeManyParams): SubCloser {
const _knownIds = new Set<string>();

View File

@ -1,20 +1,20 @@
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { createMemoryRouter } from 'react-router-dom';
import { useCallback, useEffect, useMemo, useRef } from "react";
import { createMemoryRouter } from "react-router-dom";
type Router = ReturnType<typeof createMemoryRouter>;
export default function useRouterMarker(router: Router) {
const index = useRef<number | null>(null);
const set = useCallback((v = 0) => (index.current = v), []);
const reset = useCallback(() => (index.current = null), []);
const index = useRef<number | null>(null);
const set = useCallback((v = 0) => (index.current = v), []);
const reset = useCallback(() => (index.current = null), []);
useEffect(() => {
return router.subscribe((event) => {
if (index.current === null) return;
if (event.historyAction === 'PUSH') index.current++;
else if (event.historyAction === 'POP') index.current--;
});
}, [router]);
useEffect(() => {
return router.subscribe((event) => {
if (index.current === null) return;
if (event.historyAction === "PUSH") index.current++;
else if (event.historyAction === "POP") index.current--;
});
}, [router]);
return useMemo(() => ({ index, set, reset }), [index, set, reset]);
return useMemo(() => ({ index, set, reset }), [index, set, reset]);
}

View File

@ -1 +0,0 @@

View File

@ -129,11 +129,11 @@ function NostrRelayTray() {
</Button>
)}
</CardHeader>
<CardBody p="4" pt="0">
<CardBody p="4" pt="0">
<Text mb="2">A cool little app that runs a local relay in your systems tray</Text>
<Text>Maximum capacity: Unlimited</Text>
<Text>Performance: As fast as your computer</Text>
</CardBody>
</CardBody>
</Card>
);
}
@ -171,11 +171,11 @@ function CitrineRelay() {
</Button>
)}
</CardHeader>
<CardBody p="4" pt="0">
<CardBody p="4" pt="0">
<Text mb="2">A cool little app that runs a local relay in your phone</Text>
<Text>Maximum capacity: Unlimited</Text>
<Text>Performance: As fast as your phone</Text>
</CardBody>
</CardBody>
</Card>
);
}
@ -302,13 +302,7 @@ export default function CacheRelayView() {
</Text>
<InternalRelay />
{WasmRelay.SUPPORTED && <WasmWorkerRelay />}
{
navigator.userAgent.includes("Android") ? (
<CitrineRelay />
) : (
<NostrRelayTray />
)
}
{navigator.userAgent.includes("Android") ? <CitrineRelay /> : <NostrRelayTray />}
{window.satellite && <SatelliteRelay />}
{window.CACHE_RELAY_ENABLED && <HostedRelay />}
<Button w="full" variant="link" p="4" onClick={showAdvanced.onToggle}>

View File

@ -1,3 +1,4 @@
import { useLocalStorage } from "react-use";
import {
Flex,
FormControl,
@ -13,12 +14,14 @@ import {
FormErrorMessage,
Code,
Switch,
Select,
} from "@chakra-ui/react";
import { useFormContext } from "react-hook-form";
import { safeUrl } from "../../helpers/parse";
import { AppSettings } from "../../services/settings/migrations";
import { createRequestProxyUrl } from "../../helpers/request";
import { SpyIcon } from "../../components/icons";
import { RelayAuthMode } from "../../classes/relay-pool";
async function validateInvidiousUrl(url?: string) {
if (!url) return true;
@ -45,6 +48,10 @@ async function validateRequestProxy(url?: string) {
export default function PrivacySettings() {
const { register, formState } = useFormContext<AppSettings>();
const [defaultAuthMode, setDefaultAuthMode] = useLocalStorage<RelayAuthMode>("default-relay-auth-mode", "ask", {
raw: true,
});
return (
<AccordionItem>
<h2>
@ -58,6 +65,23 @@ export default function PrivacySettings() {
</h2>
<AccordionPanel>
<Flex direction="column" gap="4">
<FormControl>
<FormLabel>Default authorization behavior</FormLabel>
<Select
size="sm"
w="xs"
rounded="md"
flexShrink={0}
value={defaultAuthMode || "ask"}
onChange={(e) => setDefaultAuthMode(e.target.value as RelayAuthMode)}
>
<option value="always">Always authenticate</option>
<option value="ask">Ask every time</option>
<option value="never">Never authenticate</option>
</Select>
<FormHelperText>How should the app handle relays requesting identification</FormHelperText>
</FormControl>
<FormControl isInvalid={!!formState.errors.twitterRedirect}>
<FormLabel>Nitter instance</FormLabel>
<Input

View File

@ -1,8 +1,11 @@
import {
Badge,
Box,
Flex,
Heading,
Link,
LinkBox,
Select,
SimpleGrid,
Spacer,
Tab,
TabIndicator,
@ -16,32 +19,82 @@ import {
} from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { AbstractRelay } from "nostr-tools";
import { useLocalStorage } from "react-use";
import relayPoolService from "../../../services/relay-pool";
import { RelayFavicon } from "../../../components/relay-favicon";
import { RelayStatus } from "../../../components/relay-status";
import HoverLinkOverlay from "../../../components/hover-link-overlay";
import { localRelay } from "../../../services/local-relay";
import useSubjects from "../../../hooks/use-subjects";
import { IconRelayAuthButton, useRelayAuthMethod } from "../../../components/relays/relay-auth-button";
import RelayConnectSwitch from "../../../components/relays/relay-connect-switch";
import useRouteSearchValue from "../../../hooks/use-route-search-value";
import processManager from "../../../services/process-manager";
import { RelayAuthMode } from "../../../classes/relay-pool";
import Timestamp from "../../../components/timestamp";
function RelayRow({ relay }: { relay: AbstractRelay }) {
function RelayCard({ relay }: { relay: AbstractRelay }) {
return (
<LinkBox display="flex" gap="2" p="2" alignItems="center">
<Flex gap="2" p="2" alignItems="center" borderWidth={1} rounded="md">
<RelayFavicon relay={relay.url} size="sm" mr="2" />
<HoverLinkOverlay as={RouterLink} to={`/r/${encodeURIComponent(relay.url)}`} isTruncated fontWeight="bold">
<Link as={RouterLink} to={`/r/${encodeURIComponent(relay.url)}`} isTruncated fontWeight="bold" py="1" pr="10">
{relay.url}
</HoverLinkOverlay>
</Link>
<Spacer />
<RelayStatus relay={relay} />
</LinkBox>
<IconRelayAuthButton relay={relay} size="sm" variant="ghost" />
<RelayConnectSwitch relay={relay} />
</Flex>
);
}
function RelayAuthCard({ relay }: { relay: AbstractRelay }) {
const { authenticated } = useRelayAuthMethod(relay);
const processes = processManager.getRootProcessesForRelay(relay);
const [authMode, setAuthMode] = useLocalStorage<RelayAuthMode>(
relayPoolService.getRelayAuthStorageKey(relay),
"ask",
{ raw: true },
);
return (
<Flex gap="2" p="2" alignItems="center" borderWidth={1} rounded="md">
<RelayFavicon relay={relay.url} size="sm" mr="2" />
<Box isTruncated>
<Link as={RouterLink} to={`/r/${encodeURIComponent(relay.url)}`} fontWeight="bold">
{relay.url}
</Link>
<br />
{authenticated ? <Badge colorScheme="green">Authenticated</Badge> : <Text>{processes.size} Processes</Text>}
</Box>
<Spacer />
<Select
size="sm"
w="auto"
rounded="md"
flexShrink={0}
value={authMode || "ask"}
onChange={(e) => setAuthMode(e.target.value as RelayAuthMode)}
>
<option value="always">Always</option>
<option value="ask">Ask</option>
<option value="never">Never</option>
</Select>
<IconRelayAuthButton relay={relay} variant="ghost" flexShrink={0} />
</Flex>
);
}
const TABS = ["relays", "auth", "notices"];
export default function TaskManagerRelays() {
const update = useForceUpdate();
useInterval(update, 2000);
const { value: tab, setValue: setTab } = useRouteSearchValue("tab", TABS[0]);
const tabIndex = TABS.indexOf(tab);
const relays = Array.from(relayPoolService.relays.values())
.filter((r) => r !== localRelay)
.sort((a, b) => +b.connected - +a.connected || a.url.localeCompare(b.url));
@ -50,34 +103,40 @@ export default function TaskManagerRelays() {
.flat()
.sort((a, b) => b.date - a.date);
const challenges = Array.from(relayPoolService.challenges.entries()).filter(([r, c]) => r.connected && !!c.value);
return (
<Tabs position="relative" variant="unstyled">
<Tabs position="relative" variant="unstyled" index={tabIndex} onChange={(i) => setTab(TABS[i])} isLazy>
<TabList>
<Tab>Relays ({relays.length})</Tab>
<Tab>Authentication ({challenges.length})</Tab>
<Tab>Notices ({notices.length})</Tab>
</TabList>
<TabIndicator mt="-1.5px" height="2px" bg="primary.500" borderRadius="1px" />
<TabPanels>
<TabPanel p="0">
<Flex direction="column">
{localRelay instanceof AbstractRelay && <RelayRow relay={localRelay} />}
<SimpleGrid spacing="2" columns={{ base: 1, md: 2 }} p="2">
{localRelay instanceof AbstractRelay && <RelayCard relay={localRelay} />}
{relays.map((relay) => (
<RelayRow key={relay.url} relay={relay} />
<RelayCard key={relay.url} relay={relay} />
))}
</Flex>
</SimpleGrid>
</TabPanel>
<TabPanel p="0">
<SimpleGrid spacing="2" columns={{ base: 1, md: 2 }} p="2">
{challenges.map(([relay, challenge]) => (
<RelayAuthCard key={relay.url} relay={relay} />
))}
</SimpleGrid>
</TabPanel>
<TabPanel p="0">
{notices.map((notice) => (
<LinkBox key={notice.date + notice.message} px="2" py="1">
<HoverLinkOverlay
as={RouterLink}
to={`/r/${encodeURIComponent(notice.relay.url)}`}
fontFamily="monospace"
fontWeight="bold"
>
<LinkBox key={notice.date + notice.message} px="2" py="1" fontFamily="monospace">
<HoverLinkOverlay as={RouterLink} to={`/r/${encodeURIComponent(notice.relay.url)}`} fontWeight="bold">
{notice.relay.url}
</HoverLinkOverlay>
<Timestamp timestamp={notice.date} ml={2} />
<Text fontFamily="monospace">{notice.message}</Text>
</LinkBox>
))}

View File

@ -1,7 +1,5 @@
import { useCallback, useMemo } from "react";
import { useMemo } from "react";
import {
Button,
ButtonGroup,
Flex,
Heading,
Spacer,
@ -14,7 +12,6 @@ import {
Text,
useForceUpdate,
useInterval,
useToast,
} from "@chakra-ui/react";
import { useParams } from "react-router-dom";
@ -25,12 +22,12 @@ import useSubject from "../../../hooks/use-subject";
import ProcessBranch from "../processes/process/process-tree";
import processManager from "../../../services/process-manager";
import RelayAuthButton from "../../../components/relays/relay-auth-button";
import { RelayStatus } from "../../../components/relay-status";
import { IconRelayAuthButton } from "../../../components/relays/relay-auth-button";
import { RelayStatus } from "../../../components/relays/relay-status";
import Timestamp from "../../../components/timestamp";
import RelayConnectSwitch from "../../../components/relays/relay-connect-switch";
export default function InspectRelayView() {
const toast = useToast();
const { url } = useParams();
if (!url) throw new Error("Missing url param");
@ -38,15 +35,6 @@ export default function InspectRelayView() {
useInterval(update, 500);
const relay = useMemo(() => relayPoolService.requestRelay(url, false), [url]);
const connecting = useSubject(relayPoolService.connecting.get(relay));
const connect = useCallback(async () => {
try {
await relayPoolService.requestConnect(relay, false);
} catch (error) {
if (error instanceof Error) toast({ status: "error", description: error.message });
}
}, [toast]);
const rootProcesses = processManager.getRootProcessesForRelay(relay);
const notices = useSubject(relayPoolService.notices.get(relay));
@ -58,12 +46,8 @@ export default function InspectRelayView() {
<Heading size="md">{url}</Heading>
<RelayStatus relay={relay} />
<Spacer />
<ButtonGroup size="sm">
<RelayAuthButton relay={relay} />
<Button variant="outline" colorScheme={connecting ? "orange" : "green"} onClick={connect}>
{connecting ? "Connecting..." : relay.connected ? "Connected" : "Connect"}
</Button>
</ButtonGroup>
<IconRelayAuthButton relay={relay} size="sm" variant="ghost" />
<RelayConnectSwitch relay={relay} />
</Flex>
<Tabs position="relative" variant="unstyled">
@ -84,9 +68,9 @@ export default function InspectRelayView() {
))}
</TabPanel>
<TabPanel p="0">
{notices.map((notice) => (
<Text fontFamily="monospace" key={notice.date + notice.message}>
[<Timestamp timestamp={notice.date} />] {notice.message}
{notices.map((notice, i) => (
<Text fontFamily="monospace" key={notice.date + i}>
{notice.message} <Timestamp timestamp={notice.date} color="gray.500" />
</Text>
))}
</TabPanel>

View File

@ -6218,6 +6218,20 @@ nostr-idb@^2.1.4:
idb "^8.0.0"
nostr-tools "^2.1.3"
nostr-tools@2.5.2:
version "2.5.2"
resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-2.5.2.tgz#54b445380ac2a7740ad90ed3b044bca93ecf23bd"
integrity sha512-Ls2FKh694eudBye6q89yJ5JhXjQle1MWp1yD2sBZ5j9M3IOBEW8ia9IED5W6daSAjlT/Z/pV77yTkdF45c1Rbg==
dependencies:
"@noble/ciphers" "^0.5.1"
"@noble/curves" "1.2.0"
"@noble/hashes" "1.3.1"
"@scure/base" "1.1.1"
"@scure/bip32" "1.3.1"
"@scure/bip39" "1.2.1"
optionalDependencies:
nostr-wasm v0.1.0
nostr-tools@^1.17.0:
version "1.17.0"
resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-1.17.0.tgz#b6f62e32fedfd9e68ec0a7ce57f74c44fc768e8c"
@ -6258,20 +6272,6 @@ nostr-tools@^2.3.2:
optionalDependencies:
nostr-wasm v0.1.0
nostr-tools@^2.5.2:
version "2.5.2"
resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-2.5.2.tgz#54b445380ac2a7740ad90ed3b044bca93ecf23bd"
integrity sha512-Ls2FKh694eudBye6q89yJ5JhXjQle1MWp1yD2sBZ5j9M3IOBEW8ia9IED5W6daSAjlT/Z/pV77yTkdF45c1Rbg==
dependencies:
"@noble/ciphers" "^0.5.1"
"@noble/curves" "1.2.0"
"@noble/hashes" "1.3.1"
"@scure/base" "1.1.1"
"@scure/bip32" "1.3.1"
"@scure/bip39" "1.2.1"
optionalDependencies:
nostr-wasm v0.1.0
nostr-wasm@^0.1.0, nostr-wasm@v0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/nostr-wasm/-/nostr-wasm-0.1.0.tgz#17af486745feb2b7dd29503fdd81613a24058d94"