mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-22 22:44:08 +02:00
show relay auth requests
add default relay auth option
This commit is contained in:
parent
0b887ac495
commit
b4c4c7a9f2
5
.changeset/rotten-icons-end.md
Normal file
5
.changeset/rotten-icons-end.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Show relay authentication requests
|
@ -68,7 +68,7 @@
|
|||||||
"nanoid": "^5.0.4",
|
"nanoid": "^5.0.4",
|
||||||
"ngeohash": "^0.6.3",
|
"ngeohash": "^0.6.3",
|
||||||
"nostr-idb": "^2.1.4",
|
"nostr-idb": "^2.1.4",
|
||||||
"nostr-tools": "^2.5.2",
|
"nostr-tools": "2.5.2",
|
||||||
"nostr-wasm": "^0.1.0",
|
"nostr-wasm": "^0.1.0",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
@ -54,6 +54,13 @@ export default class ChunkedRequest {
|
|||||||
|
|
||||||
async loadNextChunk() {
|
async loadNextChunk() {
|
||||||
if (this.loading) return;
|
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;
|
this.loading = true;
|
||||||
|
|
||||||
if (!this.relay.connected) {
|
if (!this.relay.connected) {
|
||||||
@ -93,6 +100,9 @@ export default class ChunkedRequest {
|
|||||||
this.process.active = false;
|
this.process.active = false;
|
||||||
res(gotEvents);
|
res(gotEvents);
|
||||||
},
|
},
|
||||||
|
onclose: (reason) => {
|
||||||
|
relayPoolService.handleRelayNotice(this.relay, reason);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,6 @@ export default class MultiSubscription {
|
|||||||
this.relays.add(relay);
|
this.relays.add(relay);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.process.relays = new Set(this.relays);
|
|
||||||
this.updateSubscriptions();
|
this.updateSubscriptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,6 +44,9 @@ export default class PersistentSubscription {
|
|||||||
// this.subscription.close();
|
// this.subscription.close();
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
// check if its possible to subscribe to this relay
|
||||||
|
if (!relayPoolService.canSubscribe(this.relay)) return;
|
||||||
|
|
||||||
this.closed = false;
|
this.closed = false;
|
||||||
this.eosed = false;
|
this.eosed = false;
|
||||||
this.process.active = true;
|
this.process.active = true;
|
||||||
@ -59,7 +62,9 @@ export default class PersistentSubscription {
|
|||||||
onclose: (reason) => {
|
onclose: (reason) => {
|
||||||
if (!this.closed) {
|
if (!this.closed) {
|
||||||
// unexpected close, reconnect?
|
// 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.closed = true;
|
||||||
this.process.active = false;
|
this.process.active = false;
|
||||||
|
@ -8,6 +8,8 @@ import Subject, { PersistentSubject } from "./subject";
|
|||||||
import verifyEventMethod from "../services/verify-event";
|
import verifyEventMethod from "../services/verify-event";
|
||||||
import SuperMap from "./super-map";
|
import SuperMap from "./super-map";
|
||||||
import processManager from "../services/process-manager";
|
import processManager from "../services/process-manager";
|
||||||
|
import signingService from "../services/signing";
|
||||||
|
import accountService from "../services/account";
|
||||||
|
|
||||||
export type Notice = {
|
export type Notice = {
|
||||||
message: string;
|
message: string;
|
||||||
@ -15,6 +17,8 @@ export type Notice = {
|
|||||||
relay: AbstractRelay;
|
relay: AbstractRelay;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type RelayAuthMode = "always" | "ask" | "never";
|
||||||
|
|
||||||
export default class RelayPool {
|
export default class RelayPool {
|
||||||
relays = new Map<string, AbstractRelay>();
|
relays = new Map<string, AbstractRelay>();
|
||||||
onRelayCreated = new Subject<AbstractRelay>();
|
onRelayCreated = new Subject<AbstractRelay>();
|
||||||
@ -25,9 +29,12 @@ export default class RelayPool {
|
|||||||
connectionErrors = new SuperMap<AbstractRelay, Error[]>(() => []);
|
connectionErrors = new SuperMap<AbstractRelay, Error[]>(() => []);
|
||||||
connecting = new SuperMap<AbstractRelay, PersistentSubject<boolean>>(() => new PersistentSubject(false));
|
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());
|
authForPublish = new SuperMap<AbstractRelay, Subject<boolean>>(() => new Subject());
|
||||||
authForSubscribe = 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");
|
log = logger.extend("RelayPool");
|
||||||
|
|
||||||
getRelay(relayOrUrl: string | URL | AbstractRelay) {
|
getRelay(relayOrUrl: string | URL | AbstractRelay) {
|
||||||
@ -62,7 +69,10 @@ export default class RelayPool {
|
|||||||
const key = url.toString();
|
const key = url.toString();
|
||||||
if (!this.relays.has(key)) {
|
if (!this.relays.has(key)) {
|
||||||
const r = new AbstractRelay(key, { verifyEvent: verifyEventMethod });
|
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);
|
r.onnotice = (notice) => this.handleRelayNotice(r, notice);
|
||||||
|
|
||||||
this.relays.set(key, r);
|
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) {
|
handleRelayNotice(relay: AbstractRelay, message: string) {
|
||||||
const subject = this.notices.get(relay);
|
const subject = this.notices.get(relay);
|
||||||
subject.next([...subject.value, { message, date: dayjs().unix(), relay }]);
|
subject.next([...subject.value, { message, date: dayjs().unix(), relay }]);
|
||||||
|
|
||||||
|
if (message.includes("auth-required")) {
|
||||||
const authForSubscribe = this.authForSubscribe.get(relay);
|
const authForSubscribe = this.authForSubscribe.get(relay);
|
||||||
if (!authForSubscribe.value) authForSubscribe.next(true);
|
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() {
|
disconnectFromUnused() {
|
||||||
|
@ -12,7 +12,7 @@ import useSubject from "../../hooks/use-subject";
|
|||||||
import { offlineMode } from "../../services/offline-mode";
|
import { offlineMode } from "../../services/offline-mode";
|
||||||
import WifiOff from "../icons/wifi-off";
|
import WifiOff from "../icons/wifi-off";
|
||||||
import { useTaskManagerContext } from "../../views/task-manager/provider";
|
import { useTaskManagerContext } from "../../views/task-manager/provider";
|
||||||
import TaskManagerButton from "./task-manager-button";
|
import TaskManagerButtons from "./task-manager-buttons";
|
||||||
|
|
||||||
const hideScrollbar = css`
|
const hideScrollbar = css`
|
||||||
-ms-overflow-style: none;
|
-ms-overflow-style: none;
|
||||||
@ -89,7 +89,7 @@ export default function DesktopSideNav(props: Omit<FlexProps, "children">) {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
<TaskManagerButton mt="auto" flexShrink={0} py="2" />
|
<TaskManagerButtons mt="auto" flexShrink={0} />
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ import { Link as RouterLink } from "react-router-dom";
|
|||||||
import AccountSwitcher from "./account-switcher";
|
import AccountSwitcher from "./account-switcher";
|
||||||
import useCurrentAccount from "../../hooks/use-current-account";
|
import useCurrentAccount from "../../hooks/use-current-account";
|
||||||
import NavItems from "./nav-items";
|
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">) {
|
export default function MobileSideDrawer({ ...props }: Omit<DrawerProps, "children">) {
|
||||||
const account = useCurrentAccount();
|
const account = useCurrentAccount();
|
||||||
@ -40,7 +40,7 @@ export default function MobileSideDrawer({ ...props }: Omit<DrawerProps, "childr
|
|||||||
Sign in
|
Sign in
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<TaskManagerButton mt="auto" flexShrink={0} py="2" />
|
<TaskManagerButtons mt="auto" flexShrink={0} />
|
||||||
</DrawerBody>
|
</DrawerBody>
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
46
src/components/layout/task-manager-buttons.tsx
Normal file
46
src/components/layout/task-manager-buttons.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -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 { AbstractRelay } from "nostr-tools";
|
||||||
import { Button, useToast } from "@chakra-ui/react";
|
|
||||||
|
|
||||||
import relayPoolService from "../../services/relay-pool";
|
import relayPoolService from "../../services/relay-pool";
|
||||||
import { useSigningContext } from "../../providers/global/signing-provider";
|
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 toast = useToast();
|
||||||
const { requestSignature } = useSigningContext();
|
const { requestSignature } = useSigningContext();
|
||||||
const r = relayPoolService.getRelay(relay);
|
const challenge = useRelayChallenge(relay);
|
||||||
if (!r) return null;
|
|
||||||
|
|
||||||
// @ts-expect-error
|
const authenticated = useSubject(relayPoolService.authenticated.get(relay));
|
||||||
const [challenge, setChallenge] = useState(r.challenge ?? "");
|
|
||||||
useEffect(() => {
|
|
||||||
const sub = relayPoolService.onRelayChallenge.subscribe(([relay, challenge]) => {
|
|
||||||
if (r === relay) setChallenge(challenge);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => sub.unsubscribe();
|
|
||||||
}, [r]);
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const auth = useCallback(async () => {
|
const auth = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const message = await r.auth(requestSignature);
|
const message = await relayPoolService.authenticate(relay, requestSignature, false);
|
||||||
toast({ description: message || "Success", status: "success" });
|
toast({ description: message || "Success", status: "success" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) toast({ status: "error", description: error.message });
|
if (error instanceof Error) toast({ status: "error", description: error.message });
|
||||||
}
|
}
|
||||||
setLoading(false);
|
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 (
|
return (
|
||||||
<Button onClick={auth} isLoading={loading}>
|
<IconButton
|
||||||
Authenticate
|
icon={<CheckCircleBroken boxSize={6} />}
|
||||||
</Button>
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
36
src/components/relays/relay-connect-switch.tsx
Normal file
36
src/components/relays/relay-connect-switch.tsx
Normal 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"}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -1,9 +1,9 @@
|
|||||||
import { Badge, useForceUpdate } from "@chakra-ui/react";
|
import { Badge, useForceUpdate } from "@chakra-ui/react";
|
||||||
import { useInterval } from "react-use";
|
import { useInterval } from "react-use";
|
||||||
|
|
||||||
import relayPoolService from "../services/relay-pool";
|
import relayPoolService from "../../services/relay-pool";
|
||||||
import { AbstractRelay } from "nostr-tools";
|
import { AbstractRelay } from "nostr-tools";
|
||||||
import useSubject from "../hooks/use-subject";
|
import useSubject from "../../hooks/use-subject";
|
||||||
|
|
||||||
const getStatusText = (relay: AbstractRelay, connecting = false) => {
|
const getStatusText = (relay: AbstractRelay, connecting = false) => {
|
||||||
if (connecting) return "Connecting...";
|
if (connecting) return "Connecting...";
|
@ -6,7 +6,7 @@ import { ChevronLeftIcon } from "../icons";
|
|||||||
export default function BackButton({ ...props }: Omit<IconButtonProps, "onClick" | "children" | "aria-label">) {
|
export default function BackButton({ ...props }: Omit<IconButtonProps, "onClick" | "children" | "aria-label">) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
return (
|
return (
|
||||||
<IconButton icon={<ChevronLeftIcon />} aria-label="Back" {...props} onClick={() => navigate(-1)}>
|
<IconButton icon={<ChevronLeftIcon boxSize={6} />} aria-label="Back" {...props} onClick={() => navigate(-1)}>
|
||||||
Back
|
Back
|
||||||
</IconButton>
|
</IconButton>
|
||||||
);
|
);
|
||||||
@ -14,5 +14,7 @@ export default function BackButton({ ...props }: Omit<IconButtonProps, "onClick"
|
|||||||
|
|
||||||
export function BackIconButton({ ...props }: Omit<IconButtonProps, "onClick" | "children" | "aria-label">) {
|
export function BackIconButton({ ...props }: Omit<IconButtonProps, "onClick" | "children" | "aria-label">) {
|
||||||
const navigate = useNavigate();
|
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)} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ export function getPageTopic(page: NostrEvent) {
|
|||||||
|
|
||||||
export function getPageSummary(page: NostrEvent, fallback = true) {
|
export function getPageSummary(page: NostrEvent, fallback = true) {
|
||||||
const summary = page.tags.find((t) => t[0] === "summary")?.[1];
|
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) {
|
export function getPageForks(page: NostrEvent) {
|
||||||
|
@ -97,27 +97,6 @@ export function splitQueryByPubkeys(query: NostrQuery, relayPubkeyMap: Record<st
|
|||||||
return filtersByRelay;
|
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
|
// copied from nostr-tools, SimplePool#subscribeMany
|
||||||
export function subscribeMany(relays: string[], filters: Filter[], params: SubscribeManyParams): SubCloser {
|
export function subscribeMany(relays: string[], filters: Filter[], params: SubscribeManyParams): SubCloser {
|
||||||
const _knownIds = new Set<string>();
|
const _knownIds = new Set<string>();
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||||
import { createMemoryRouter } from 'react-router-dom';
|
import { createMemoryRouter } from "react-router-dom";
|
||||||
|
|
||||||
type Router = ReturnType<typeof createMemoryRouter>;
|
type Router = ReturnType<typeof createMemoryRouter>;
|
||||||
|
|
||||||
@ -11,8 +11,8 @@ export default function useRouterMarker(router: Router) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return router.subscribe((event) => {
|
return router.subscribe((event) => {
|
||||||
if (index.current === null) return;
|
if (index.current === null) return;
|
||||||
if (event.historyAction === 'PUSH') index.current++;
|
if (event.historyAction === "PUSH") index.current++;
|
||||||
else if (event.historyAction === 'POP') index.current--;
|
else if (event.historyAction === "POP") index.current--;
|
||||||
});
|
});
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
|
@ -1 +0,0 @@
|
|||||||
|
|
8
src/views/relays/cache/index.tsx
vendored
8
src/views/relays/cache/index.tsx
vendored
@ -302,13 +302,7 @@ export default function CacheRelayView() {
|
|||||||
</Text>
|
</Text>
|
||||||
<InternalRelay />
|
<InternalRelay />
|
||||||
{WasmRelay.SUPPORTED && <WasmWorkerRelay />}
|
{WasmRelay.SUPPORTED && <WasmWorkerRelay />}
|
||||||
{
|
{navigator.userAgent.includes("Android") ? <CitrineRelay /> : <NostrRelayTray />}
|
||||||
navigator.userAgent.includes("Android") ? (
|
|
||||||
<CitrineRelay />
|
|
||||||
) : (
|
|
||||||
<NostrRelayTray />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{window.satellite && <SatelliteRelay />}
|
{window.satellite && <SatelliteRelay />}
|
||||||
{window.CACHE_RELAY_ENABLED && <HostedRelay />}
|
{window.CACHE_RELAY_ENABLED && <HostedRelay />}
|
||||||
<Button w="full" variant="link" p="4" onClick={showAdvanced.onToggle}>
|
<Button w="full" variant="link" p="4" onClick={showAdvanced.onToggle}>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { useLocalStorage } from "react-use";
|
||||||
import {
|
import {
|
||||||
Flex,
|
Flex,
|
||||||
FormControl,
|
FormControl,
|
||||||
@ -13,12 +14,14 @@ import {
|
|||||||
FormErrorMessage,
|
FormErrorMessage,
|
||||||
Code,
|
Code,
|
||||||
Switch,
|
Switch,
|
||||||
|
Select,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
import { safeUrl } from "../../helpers/parse";
|
import { safeUrl } from "../../helpers/parse";
|
||||||
import { AppSettings } from "../../services/settings/migrations";
|
import { AppSettings } from "../../services/settings/migrations";
|
||||||
import { createRequestProxyUrl } from "../../helpers/request";
|
import { createRequestProxyUrl } from "../../helpers/request";
|
||||||
import { SpyIcon } from "../../components/icons";
|
import { SpyIcon } from "../../components/icons";
|
||||||
|
import { RelayAuthMode } from "../../classes/relay-pool";
|
||||||
|
|
||||||
async function validateInvidiousUrl(url?: string) {
|
async function validateInvidiousUrl(url?: string) {
|
||||||
if (!url) return true;
|
if (!url) return true;
|
||||||
@ -45,6 +48,10 @@ async function validateRequestProxy(url?: string) {
|
|||||||
export default function PrivacySettings() {
|
export default function PrivacySettings() {
|
||||||
const { register, formState } = useFormContext<AppSettings>();
|
const { register, formState } = useFormContext<AppSettings>();
|
||||||
|
|
||||||
|
const [defaultAuthMode, setDefaultAuthMode] = useLocalStorage<RelayAuthMode>("default-relay-auth-mode", "ask", {
|
||||||
|
raw: true,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccordionItem>
|
<AccordionItem>
|
||||||
<h2>
|
<h2>
|
||||||
@ -58,6 +65,23 @@ export default function PrivacySettings() {
|
|||||||
</h2>
|
</h2>
|
||||||
<AccordionPanel>
|
<AccordionPanel>
|
||||||
<Flex direction="column" gap="4">
|
<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}>
|
<FormControl isInvalid={!!formState.errors.twitterRedirect}>
|
||||||
<FormLabel>Nitter instance</FormLabel>
|
<FormLabel>Nitter instance</FormLabel>
|
||||||
<Input
|
<Input
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
|
Badge,
|
||||||
Box,
|
Box,
|
||||||
Flex,
|
Flex,
|
||||||
Heading,
|
Link,
|
||||||
LinkBox,
|
LinkBox,
|
||||||
|
Select,
|
||||||
|
SimpleGrid,
|
||||||
Spacer,
|
Spacer,
|
||||||
Tab,
|
Tab,
|
||||||
TabIndicator,
|
TabIndicator,
|
||||||
@ -16,32 +19,82 @@ import {
|
|||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { Link as RouterLink } from "react-router-dom";
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
import { AbstractRelay } from "nostr-tools";
|
import { AbstractRelay } from "nostr-tools";
|
||||||
|
import { useLocalStorage } from "react-use";
|
||||||
|
|
||||||
import relayPoolService from "../../../services/relay-pool";
|
import relayPoolService from "../../../services/relay-pool";
|
||||||
import { RelayFavicon } from "../../../components/relay-favicon";
|
import { RelayFavicon } from "../../../components/relay-favicon";
|
||||||
import { RelayStatus } from "../../../components/relay-status";
|
|
||||||
import HoverLinkOverlay from "../../../components/hover-link-overlay";
|
import HoverLinkOverlay from "../../../components/hover-link-overlay";
|
||||||
import { localRelay } from "../../../services/local-relay";
|
import { localRelay } from "../../../services/local-relay";
|
||||||
import useSubjects from "../../../hooks/use-subjects";
|
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";
|
import Timestamp from "../../../components/timestamp";
|
||||||
|
|
||||||
function RelayRow({ relay }: { relay: AbstractRelay }) {
|
function RelayCard({ relay }: { relay: AbstractRelay }) {
|
||||||
return (
|
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" />
|
<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}
|
{relay.url}
|
||||||
</HoverLinkOverlay>
|
</Link>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<RelayStatus relay={relay} />
|
<IconRelayAuthButton relay={relay} size="sm" variant="ghost" />
|
||||||
</LinkBox>
|
<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() {
|
export default function TaskManagerRelays() {
|
||||||
const update = useForceUpdate();
|
const update = useForceUpdate();
|
||||||
useInterval(update, 2000);
|
useInterval(update, 2000);
|
||||||
|
|
||||||
|
const { value: tab, setValue: setTab } = useRouteSearchValue("tab", TABS[0]);
|
||||||
|
const tabIndex = TABS.indexOf(tab);
|
||||||
|
|
||||||
const relays = Array.from(relayPoolService.relays.values())
|
const relays = Array.from(relayPoolService.relays.values())
|
||||||
.filter((r) => r !== localRelay)
|
.filter((r) => r !== localRelay)
|
||||||
.sort((a, b) => +b.connected - +a.connected || a.url.localeCompare(b.url));
|
.sort((a, b) => +b.connected - +a.connected || a.url.localeCompare(b.url));
|
||||||
@ -50,34 +103,40 @@ export default function TaskManagerRelays() {
|
|||||||
.flat()
|
.flat()
|
||||||
.sort((a, b) => b.date - a.date);
|
.sort((a, b) => b.date - a.date);
|
||||||
|
|
||||||
|
const challenges = Array.from(relayPoolService.challenges.entries()).filter(([r, c]) => r.connected && !!c.value);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs position="relative" variant="unstyled">
|
<Tabs position="relative" variant="unstyled" index={tabIndex} onChange={(i) => setTab(TABS[i])} isLazy>
|
||||||
<TabList>
|
<TabList>
|
||||||
<Tab>Relays ({relays.length})</Tab>
|
<Tab>Relays ({relays.length})</Tab>
|
||||||
|
<Tab>Authentication ({challenges.length})</Tab>
|
||||||
<Tab>Notices ({notices.length})</Tab>
|
<Tab>Notices ({notices.length})</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabIndicator mt="-1.5px" height="2px" bg="primary.500" borderRadius="1px" />
|
<TabIndicator mt="-1.5px" height="2px" bg="primary.500" borderRadius="1px" />
|
||||||
|
|
||||||
<TabPanels>
|
<TabPanels>
|
||||||
<TabPanel p="0">
|
<TabPanel p="0">
|
||||||
<Flex direction="column">
|
<SimpleGrid spacing="2" columns={{ base: 1, md: 2 }} p="2">
|
||||||
{localRelay instanceof AbstractRelay && <RelayRow relay={localRelay} />}
|
{localRelay instanceof AbstractRelay && <RelayCard relay={localRelay} />}
|
||||||
{relays.map((relay) => (
|
{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>
|
||||||
<TabPanel p="0">
|
<TabPanel p="0">
|
||||||
{notices.map((notice) => (
|
{notices.map((notice) => (
|
||||||
<LinkBox key={notice.date + notice.message} px="2" py="1">
|
<LinkBox key={notice.date + notice.message} px="2" py="1" fontFamily="monospace">
|
||||||
<HoverLinkOverlay
|
<HoverLinkOverlay as={RouterLink} to={`/r/${encodeURIComponent(notice.relay.url)}`} fontWeight="bold">
|
||||||
as={RouterLink}
|
|
||||||
to={`/r/${encodeURIComponent(notice.relay.url)}`}
|
|
||||||
fontFamily="monospace"
|
|
||||||
fontWeight="bold"
|
|
||||||
>
|
|
||||||
{notice.relay.url}
|
{notice.relay.url}
|
||||||
</HoverLinkOverlay>
|
</HoverLinkOverlay>
|
||||||
|
<Timestamp timestamp={notice.date} ml={2} />
|
||||||
<Text fontFamily="monospace">{notice.message}</Text>
|
<Text fontFamily="monospace">{notice.message}</Text>
|
||||||
</LinkBox>
|
</LinkBox>
|
||||||
))}
|
))}
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import { useCallback, useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
Button,
|
|
||||||
ButtonGroup,
|
|
||||||
Flex,
|
Flex,
|
||||||
Heading,
|
Heading,
|
||||||
Spacer,
|
Spacer,
|
||||||
@ -14,7 +12,6 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
useForceUpdate,
|
useForceUpdate,
|
||||||
useInterval,
|
useInterval,
|
||||||
useToast,
|
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
|
|
||||||
@ -25,12 +22,12 @@ import useSubject from "../../../hooks/use-subject";
|
|||||||
|
|
||||||
import ProcessBranch from "../processes/process/process-tree";
|
import ProcessBranch from "../processes/process/process-tree";
|
||||||
import processManager from "../../../services/process-manager";
|
import processManager from "../../../services/process-manager";
|
||||||
import RelayAuthButton from "../../../components/relays/relay-auth-button";
|
import { IconRelayAuthButton } from "../../../components/relays/relay-auth-button";
|
||||||
import { RelayStatus } from "../../../components/relay-status";
|
import { RelayStatus } from "../../../components/relays/relay-status";
|
||||||
import Timestamp from "../../../components/timestamp";
|
import Timestamp from "../../../components/timestamp";
|
||||||
|
import RelayConnectSwitch from "../../../components/relays/relay-connect-switch";
|
||||||
|
|
||||||
export default function InspectRelayView() {
|
export default function InspectRelayView() {
|
||||||
const toast = useToast();
|
|
||||||
const { url } = useParams();
|
const { url } = useParams();
|
||||||
if (!url) throw new Error("Missing url param");
|
if (!url) throw new Error("Missing url param");
|
||||||
|
|
||||||
@ -38,15 +35,6 @@ export default function InspectRelayView() {
|
|||||||
useInterval(update, 500);
|
useInterval(update, 500);
|
||||||
|
|
||||||
const relay = useMemo(() => relayPoolService.requestRelay(url, false), [url]);
|
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 rootProcesses = processManager.getRootProcessesForRelay(relay);
|
||||||
const notices = useSubject(relayPoolService.notices.get(relay));
|
const notices = useSubject(relayPoolService.notices.get(relay));
|
||||||
@ -58,12 +46,8 @@ export default function InspectRelayView() {
|
|||||||
<Heading size="md">{url}</Heading>
|
<Heading size="md">{url}</Heading>
|
||||||
<RelayStatus relay={relay} />
|
<RelayStatus relay={relay} />
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<ButtonGroup size="sm">
|
<IconRelayAuthButton relay={relay} size="sm" variant="ghost" />
|
||||||
<RelayAuthButton relay={relay} />
|
<RelayConnectSwitch relay={relay} />
|
||||||
<Button variant="outline" colorScheme={connecting ? "orange" : "green"} onClick={connect}>
|
|
||||||
{connecting ? "Connecting..." : relay.connected ? "Connected" : "Connect"}
|
|
||||||
</Button>
|
|
||||||
</ButtonGroup>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Tabs position="relative" variant="unstyled">
|
<Tabs position="relative" variant="unstyled">
|
||||||
@ -84,9 +68,9 @@ export default function InspectRelayView() {
|
|||||||
))}
|
))}
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel p="0">
|
<TabPanel p="0">
|
||||||
{notices.map((notice) => (
|
{notices.map((notice, i) => (
|
||||||
<Text fontFamily="monospace" key={notice.date + notice.message}>
|
<Text fontFamily="monospace" key={notice.date + i}>
|
||||||
[<Timestamp timestamp={notice.date} />] {notice.message}
|
{notice.message} <Timestamp timestamp={notice.date} color="gray.500" />
|
||||||
</Text>
|
</Text>
|
||||||
))}
|
))}
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
28
yarn.lock
28
yarn.lock
@ -6218,6 +6218,20 @@ nostr-idb@^2.1.4:
|
|||||||
idb "^8.0.0"
|
idb "^8.0.0"
|
||||||
nostr-tools "^2.1.3"
|
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:
|
nostr-tools@^1.17.0:
|
||||||
version "1.17.0"
|
version "1.17.0"
|
||||||
resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-1.17.0.tgz#b6f62e32fedfd9e68ec0a7ce57f74c44fc768e8c"
|
resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-1.17.0.tgz#b6f62e32fedfd9e68ec0a7ce57f74c44fc768e8c"
|
||||||
@ -6258,20 +6272,6 @@ nostr-tools@^2.3.2:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
nostr-wasm v0.1.0
|
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:
|
nostr-wasm@^0.1.0, nostr-wasm@v0.1.0:
|
||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/nostr-wasm/-/nostr-wasm-0.1.0.tgz#17af486745feb2b7dd29503fdd81613a24058d94"
|
resolved "https://registry.yarnpkg.com/nostr-wasm/-/nostr-wasm-0.1.0.tgz#17af486745feb2b7dd29503fdd81613a24058d94"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user