mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-09 20:29:17 +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",
|
||||
"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",
|
||||
|
@ -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);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -76,7 +76,6 @@ export default class MultiSubscription {
|
||||
this.relays.add(relay);
|
||||
}
|
||||
|
||||
this.process.relays = new Set(this.relays);
|
||||
this.updateSubscriptions();
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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 { 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;
|
||||
}
|
||||
|
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 { 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...";
|
@ -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)} />
|
||||
);
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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>();
|
||||
|
@ -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]);
|
||||
}
|
||||
|
@ -1 +0,0 @@
|
||||
|
16
src/views/relays/cache/index.tsx
vendored
16
src/views/relays/cache/index.tsx
vendored
@ -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}>
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
))}
|
||||
|
@ -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>
|
||||
|
28
yarn.lock
28
yarn.lock
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user