show relay notices in task manager

This commit is contained in:
hzrd149 2024-05-03 11:04:31 -05:00
parent b7572d373e
commit 2e6b79ef6a
16 changed files with 249 additions and 149 deletions

View File

@ -4,7 +4,7 @@ import _throttle from "lodash.throttle";
import debug, { Debugger } from "debug";
import EventStore from "./event-store";
import { getEventCoordinate } from "../helpers/nostr/event";
import { getEventUID } from "../helpers/nostr/event";
import PersistentSubscription from "./persistent-subscription";
import Process from "./process";
import BracketsX from "../components/icons/brackets-x";
@ -39,7 +39,7 @@ export default class BatchKindLoader {
}
private handleEvent(event: NostrEvent) {
const key = getEventCoordinate(event);
const key = getEventUID(event);
// remove the key from the waiting list
this.requested.delete(key);

View File

@ -38,6 +38,12 @@ export default class PersistentSubscription {
if (!(await relayPoolService.waitForOpen(this.relay))) return;
// recreate the subscription since strfry and other relays reject subscription updates
// if (this.subscription?.closed === false) {
// this.closed = true;
// this.subscription.close();
// }
this.closed = false;
this.process.active = true;

View File

@ -1,4 +1,5 @@
import { AbstractRelay } from "nostr-tools";
import dayjs from "dayjs";
import { logger } from "../helpers/debug";
import { safeRelayUrl, validateRelayURL } from "../helpers/relay";
@ -8,27 +9,34 @@ import verifyEventMethod from "../services/verify-event";
import SuperMap from "./super-map";
import processManager from "../services/process-manager";
export type Notice = {
message: string;
date: number;
};
export default class RelayPool {
relays = new Map<string, AbstractRelay>();
onRelayCreated = new Subject<AbstractRelay>();
onRelayChallenge = new Subject<[AbstractRelay, string]>();
notices = new SuperMap<AbstractRelay, PersistentSubject<Notice[]>>(() => new PersistentSubject<Notice[]>([]));
connectionErrors = new SuperMap<AbstractRelay, Error[]>(() => []);
connecting = new SuperMap<AbstractRelay, PersistentSubject<boolean>>(() => new PersistentSubject(false));
log = logger.extend("RelayPool");
getRelay(relayOrUrl: string | URL | AbstractRelay) {
let relay: AbstractRelay | undefined = undefined;
if (typeof relayOrUrl === "string") {
const safeURL = safeRelayUrl(relayOrUrl);
if (safeURL) relay = this.relays.get(safeURL) || this.requestRelay(safeURL);
if (safeURL) {
return this.relays.get(safeURL) || this.requestRelay(safeURL);
} else return;
} else if (relayOrUrl instanceof URL) {
relay = this.relays.get(relayOrUrl.toString()) || this.requestRelay(relayOrUrl.toString());
} else relay = relayOrUrl;
return this.relays.get(relayOrUrl.toString()) || this.requestRelay(relayOrUrl.toString());
}
return relay;
return relayOrUrl;
}
getRelays(urls?: Iterable<string | URL | AbstractRelay>) {
@ -49,14 +57,16 @@ export default class RelayPool {
const key = url.toString();
if (!this.relays.has(key)) {
const newRelay = new AbstractRelay(key, { verifyEvent: verifyEventMethod });
newRelay._onauth = (challenge) => this.onRelayChallenge.next([newRelay, challenge]);
this.relays.set(key, newRelay);
this.onRelayCreated.next(newRelay);
const r = new AbstractRelay(key, { verifyEvent: verifyEventMethod });
r._onauth = (challenge) => this.onRelayChallenge.next([r, challenge]);
r.onnotice = (notice) => this.handleRelayNotice(r, notice);
this.relays.set(key, r);
this.onRelayCreated.next(r);
}
const relay = this.relays.get(key) as AbstractRelay;
if (connect) this.requestConnect(relay);
if (connect && !relay.connected) this.requestConnect(relay);
return relay;
}
@ -98,8 +108,15 @@ export default class RelayPool {
}
}
handleRelayNotice(relay: AbstractRelay, message: string) {
const subject = this.notices.get(relay);
subject.next([...subject.value, { message, date: dayjs().unix() }]);
}
disconnectFromUnused() {
for (const [url, relay] of this.relays) {
if (!relay.connected) continue;
let disconnect = true;
for (const process of processManager.processes) {
if (process.active && process.relays.has(relay)) {

View File

@ -18,7 +18,7 @@ import { AddIcon } from "../icons";
import { normalizeToHexPubkey } from "../../helpers/nip19";
import UserAvatar from "../user/user-avatar";
import UserLink from "../user/user-link";
import NpubAutocomplete from "../npub-autocomplete";
import UserAutocomplete from "../user-autocomplete";
function getRemainingPercent(split: EventSplit) {
return Math.round((1 - split.reduce((v, p) => v + p.percent, 0)) * 100) / 100;
@ -67,7 +67,7 @@ function AddUserForm({
return (
<Flex as="form" gap="2" onSubmit={submit}>
<NpubAutocomplete {...register("pubkey", { required: true, validate: validateNpub })} />
<UserAutocomplete {...register("pubkey", { required: true, validate: validateNpub })} />
<NumberInput
step={1}
min={1}

View File

@ -22,13 +22,16 @@ const getStatusColor = (relay: AbstractRelay, connecting = false) => {
return "red";
};
export const RelayStatus = ({ url }: { url: string }) => {
export const RelayStatus = ({ url, relay }: { url?: string; relay?: AbstractRelay }) => {
const update = useForceUpdate();
const relay = relayPoolService.requestRelay(url, false);
const connecting = useSubject(relayPoolService.connecting.get(relay));
useInterval(() => update(), 500);
return <Badge colorScheme={getStatusColor(relay, connecting)}>{getStatusText(relay, connecting)}</Badge>;
if (!relay) {
if (url) relay = relayPoolService.getRelay(url);
else throw Error("Missing url or relay");
}
const connecting = useSubject(relayPoolService.connecting.get(relay!));
return <Badge colorScheme={getStatusColor(relay!, connecting)}>{getStatusText(relay!, connecting)}</Badge>;
};

View File

@ -8,7 +8,7 @@ import userMetadataService from "../services/user-metadata";
import { getDisplayName } from "../helpers/nostr/user-metadata";
import useAppSettings from "../hooks/use-app-settings";
const NpubAutocomplete = forwardRef<HTMLInputElement, InputProps>(({ value, ...props }, ref) => {
const UserAutocomplete = forwardRef<HTMLInputElement, InputProps>(({ value, ...props }, ref) => {
const getDirectory = useUserSearchDirectoryContext();
const { removeEmojisInUsernames } = useAppSettings();
@ -19,7 +19,7 @@ const NpubAutocomplete = forwardRef<HTMLInputElement, InputProps>(({ value, ...p
return (
<>
<Input placeholder="npub..." list="users" value={value} {...props} ref={ref} />
<Input placeholder="Users" list="users" value={value} {...props} ref={ref} />
{users && (
<datalist id="users">
{users
@ -35,4 +35,4 @@ const NpubAutocomplete = forwardRef<HTMLInputElement, InputProps>(({ value, ...p
);
});
export default NpubAutocomplete;
export default UserAutocomplete;

View File

@ -1,10 +1,11 @@
import { CacheRelay, openDB } from "nostr-idb";
import { AbstractRelay, NostrEvent, VerifiedEvent, verifiedSymbol } from "nostr-tools";
import { AbstractRelay } from "nostr-tools";
import { logger } from "../helpers/debug";
import { safeRelayUrl } from "../helpers/relay";
import WasmRelay from "./wasm-relay";
import MemoryRelay from "../classes/memory-relay";
import { fakeVerifyEvent } from "./verify-event";
import relayPoolService from "./relay-pool";
// save the local relay from query params to localStorage
const params = new URLSearchParams(location.search);
@ -74,6 +75,12 @@ async function connectRelay() {
try {
await relay.connect();
log("Connected");
if (relay instanceof AbstractRelay) {
relayPoolService.relays.set(relay.url, relay);
relay.onnotice = (notice) => relayPoolService.handleRelayNotice(relay, notice);
}
return relay;
} catch (e) {
log("Failed to connect to local relay, falling back to internal");

View File

@ -34,7 +34,7 @@ import { nostrBuildUploadImage } from "../../../helpers/media-upload/nostr-build
import { useSigningContext } from "../../../providers/global/signing-provider";
import { RelayUrlInput } from "../../../components/relay-url-input";
import { RelayFavicon } from "../../../components/relay-favicon";
import NpubAutocomplete from "../../../components/npub-autocomplete";
import UserAutocomplete from "../../../components/user-autocomplete";
import { normalizeToHexPubkey } from "../../../helpers/nip19";
import { safeUrl } from "../../../helpers/parse";
import { safeRelayUrl } from "../../../helpers/relay";
@ -268,7 +268,7 @@ export default function CommunityCreateModal({
))}
</Flex>
<Flex gap="2">
<NpubAutocomplete value={modInput} onChange={(e) => setModInput(e.target.value)} />
<UserAutocomplete value={modInput} onChange={(e) => setModInput(e.target.value)} />
<Button isDisabled={!modInput} onClick={addMod}>
Add
</Button>

View File

@ -1,24 +1,35 @@
import { Flex, LinkBox, Spacer } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { AbstractRelay } from "nostr-tools";
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";
function RelayRow({ relay }: { relay: AbstractRelay }) {
return (
<LinkBox display="flex" gap="2" p="2" alignItems="center">
<RelayFavicon relay={relay.url} size="sm" mr="2" />
<HoverLinkOverlay as={RouterLink} to={`/r/${encodeURIComponent(relay.url)}`} isTruncated fontWeight="bold">
{relay.url}
</HoverLinkOverlay>
<Spacer />
<RelayStatus relay={relay} />
</LinkBox>
);
}
export default function TaskManagerRelays() {
return (
<Flex direction="column">
{Array.from(relayPoolService.relays.values()).map((relay) => (
<LinkBox key={relay.url} display="flex" gap="2" p="2" alignItems="center">
<RelayFavicon relay={relay.url} size="sm" mr="2" />
<HoverLinkOverlay as={RouterLink} to={`/r/${encodeURIComponent(relay.url)}`} isTruncated fontWeight="bold">
{relay.url}
</HoverLinkOverlay>
<Spacer />
<RelayStatus url={relay.url} />
</LinkBox>
))}
{localRelay instanceof AbstractRelay && <RelayRow relay={localRelay} />}
{Array.from(relayPoolService.relays.values())
.filter((r) => r !== localRelay)
.map((relay) => (
<RelayRow key={relay.url} relay={relay} />
))}
</Flex>
);
}

View File

@ -1,5 +1,21 @@
import { useCallback, useMemo } from "react";
import { Button, ButtonGroup, Flex, Heading, Spacer, useForceUpdate, useInterval, useToast } from "@chakra-ui/react";
import {
Button,
ButtonGroup,
Flex,
Heading,
Spacer,
Tab,
TabIndicator,
TabList,
TabPanel,
TabPanels,
Tabs,
Text,
useForceUpdate,
useInterval,
useToast,
} from "@chakra-ui/react";
import { useParams } from "react-router-dom";
import VerticalPageLayout from "../../../components/vertical-page-layout";
@ -10,6 +26,7 @@ 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 Timestamp from "../../../components/timestamp";
export default function InspectRelayView() {
const toast = useToast();
@ -31,6 +48,7 @@ export default function InspectRelayView() {
}, [toast]);
const rootProcesses = processManager.getRootProcessesForRelay(relay);
const notices = useSubject(relayPoolService.notices.get(relay));
return (
<VerticalPageLayout>
@ -50,15 +68,32 @@ export default function InspectRelayView() {
</ButtonGroup>
</Flex>
<Flex direction="column">
{Array.from(rootProcesses).map((process) => (
<ProcessBranch
key={process.id}
process={process}
filter={(p) => (p.relays.size > 0 ? p.relays.has(relay) : p.children.size > 0)}
/>
))}
</Flex>
<Tabs position="relative" variant="unstyled">
<TabList>
<Tab>Processes ({rootProcesses.size})</Tab>
<Tab>Notices ({notices.length})</Tab>
</TabList>
<TabIndicator mt="-1.5px" height="2px" bg="primary.500" borderRadius="1px" />
<TabPanels>
<TabPanel p="0">
{Array.from(rootProcesses).map((process) => (
<ProcessBranch
key={process.id}
process={process}
filter={(p) => (p.relays.size > 0 ? p.relays.has(relay) : p.children.size > 0)}
/>
))}
</TabPanel>
<TabPanel p="0">
{notices.map((notice) => (
<Text fontFamily="monospace" key={notice.date + notice.message}>
[<Timestamp timestamp={notice.date} />] {notice.message}
</Text>
))}
</TabPanel>
</TabPanels>
</Tabs>
</VerticalPageLayout>
);
}

View File

@ -23,7 +23,7 @@ import { useBreakpointValue } from "../../providers/global/breakpoint-provider";
import MarkdownContent from "./components/markdown";
import { WIKI_RELAYS } from "../../const";
import UserName from "../../components/user/user-name";
import WikiPageMenu from "./components/wioki-page-menu";
import WikiPageMenu from "./components/wiki-page-menu";
import { getSharableEventAddress } from "../../helpers/nip19";
import { ExternalLinkIcon } from "../../components/icons";

View File

@ -1,3 +1,4 @@
import { forwardRef } from "react";
import {
Code,
Heading,
@ -7,15 +8,6 @@ import {
LinkProps,
ListItem,
OrderedList,
Popover,
PopoverArrow,
PopoverBody,
PopoverCloseButton,
PopoverContent,
PopoverHeader,
PopoverTrigger,
Portal,
Spinner,
Table,
TableContainer,
TableProps,
@ -28,23 +20,13 @@ import {
Thead,
Tr,
UnorderedList,
useDisclosure,
} from "@chakra-ui/react";
import Markdown, { Components, ExtraProps } from "react-markdown";
import styled from "@emotion/styled";
import { NostrEvent } from "nostr-tools";
import { forwardRef, useCallback, useMemo, useState } from "react";
import Markdown, { Components, ExtraProps } from "react-markdown";
import remarkGfm from "remark-gfm";
import wikiLinkPlugin from "remark-wiki-link";
import { Link as RouterLink } from "react-router-dom";
import { useReadRelays } from "../../../hooks/use-client-relays";
import { subscribeMany } from "../../../helpers/relay";
import { WIKI_PAGE_KIND, getPageSummary } from "../../../helpers/nostr/wiki";
import replaceableEventsService from "../../../services/replaceable-events";
import { getEventUID } from "nostr-idb";
import UserName from "../../../components/user/user-name";
import { getWebOfTrust } from "../../../services/web-of-trust";
import { getSharableEventAddress } from "../../../helpers/nip19";
import WikiLink from "./wiki-link";
const StyledMarkdown = styled(Markdown)`
pre > code {
@ -97,83 +79,6 @@ function H6({ children, node, ...props }: HeadingProps & ExtraProps) {
);
}
const MAX_VERSIONS = 4;
function WikiLink({ children, node, href, ...props }: LinkProps & ExtraProps) {
const { isOpen, onClose, onOpen } = useDisclosure();
const readRelays = useReadRelays();
const properties = node!.properties as { className: string; href: string };
const topic = properties.href.replace(/^#\/page\//, "");
const [events, setEvents] = useState<NostrEvent[]>();
const load = useCallback(() => {
const arr: NostrEvent[] = [];
const sub = subscribeMany(Array.from(readRelays), [{ kinds: [WIKI_PAGE_KIND], "#d": [topic] }], {
onevent: (event) => {
replaceableEventsService.handleEvent(event);
if (event.content) arr.push(event);
},
oneose: () => {
setEvents(arr);
sub.close();
},
});
}, [topic, setEvents, readRelays]);
const open = useCallback(() => {
if (!events) load();
onOpen();
}, [onOpen, events]);
const sorted = useMemo(() => {
if (!events) return [];
const arr = getWebOfTrust().sortByDistanceAndConnections(events, (e) => e.pubkey);
const seen = new Set<string>();
const unique: NostrEvent[] = [];
for (const event of arr) {
const summary = getPageSummary(event);
if (!seen.has(summary)) {
seen.add(summary);
unique.push(event);
if (unique.length >= MAX_VERSIONS) break;
}
}
return unique;
}, [events]);
// if there is only one result, redirect to it
const to = events?.length === 1 ? "/wiki/page/" + getSharableEventAddress(events[0]) : "/wiki/topic/" + topic;
return (
<Popover returnFocusOnClose={false} isOpen={isOpen} onClose={onClose} placement="top" closeOnBlur={true}>
<PopoverTrigger>
<Link as={RouterLink} color="blue.500" {...props} to={to} onMouseEnter={open} onMouseLeave={onClose}>
{children}
</Link>
</PopoverTrigger>
<Portal>
<PopoverContent w="lg">
<PopoverArrow />
<PopoverCloseButton />
<PopoverHeader fontWeight="bold">{children}</PopoverHeader>
<PopoverBody>
{events === undefined && <Spinner />}
{sorted.map((page) => (
<Text key={getEventUID(page)} noOfLines={2} mb="2">
<UserName pubkey={page.pubkey} />: {getPageSummary(page)}
</Text>
))}
{events?.length === 0 && <Text fontStyle="italic">There is no entry for this topic</Text>}
</PopoverBody>
</PopoverContent>
</Portal>
</Popover>
);
}
function A({ children, node, href, ...props }: LinkProps & ExtraProps) {
const properties: { className?: string; href?: string } | undefined = node?.properties;

View File

@ -0,0 +1,115 @@
import { useCallback, useMemo, useState } from "react";
import {
Link,
LinkProps,
Popover,
PopoverArrow,
PopoverBody,
PopoverCloseButton,
PopoverContent,
PopoverHeader,
PopoverTrigger,
Portal,
Spinner,
Text,
useDisclosure,
} from "@chakra-ui/react";
import { NostrEvent } from "nostr-tools";
import { ExtraProps } from "react-markdown";
import { getEventUID } from "nostr-idb";
import { Link as RouterLink } from "react-router-dom";
import { useReadRelays } from "../../../hooks/use-client-relays";
import { subscribeMany } from "../../../helpers/relay";
import { WIKI_PAGE_KIND, getPageSummary } from "../../../helpers/nostr/wiki";
import replaceableEventsService from "../../../services/replaceable-events";
import UserName from "../../../components/user/user-name";
import { getWebOfTrust } from "../../../services/web-of-trust";
import { getSharableEventAddress } from "../../../helpers/nip19";
export default function WikiLink({
children,
node,
href,
maxVersions = 4,
topic,
...props
}: LinkProps & ExtraProps & { maxVersions?: number; topic?: string }) {
const { isOpen, onClose, onOpen } = useDisclosure();
const readRelays = useReadRelays();
if (node) {
const properties = node.properties as { className: string; href: string };
topic = properties.href.replace(/^#\/page\//, "");
}
const [events, setEvents] = useState<NostrEvent[]>();
const load = useCallback(() => {
if (!topic) return;
const arr: NostrEvent[] = [];
const sub = subscribeMany(Array.from(readRelays), [{ kinds: [WIKI_PAGE_KIND], "#d": [topic] }], {
onevent: (event) => {
replaceableEventsService.handleEvent(event);
if (event.content) arr.push(event);
},
oneose: () => {
setEvents(arr);
sub.close();
},
});
}, [topic, setEvents, readRelays]);
const open = useCallback(() => {
if (!events) load();
onOpen();
}, [onOpen, events]);
const sorted = useMemo(() => {
if (!events) return [];
const arr = getWebOfTrust().sortByDistanceAndConnections(events, (e) => e.pubkey);
const seen = new Set<string>();
const unique: NostrEvent[] = [];
for (const event of arr) {
const summary = getPageSummary(event);
if (!seen.has(summary)) {
seen.add(summary);
unique.push(event);
if (unique.length >= maxVersions) break;
}
}
return unique;
}, [events]);
// if there is only one result, redirect to it
const to = events?.length === 1 ? "/wiki/page/" + getSharableEventAddress(events[0]) : "/wiki/topic/" + topic;
return (
<Popover returnFocusOnClose={false} isOpen={isOpen} onClose={onClose} placement="top" closeOnBlur={true}>
<PopoverTrigger>
<Link as={RouterLink} color="blue.500" {...props} to={to} onMouseEnter={open} onMouseLeave={onClose}>
{children}
</Link>
</PopoverTrigger>
<Portal>
<PopoverContent w="lg">
<PopoverArrow />
<PopoverCloseButton />
<PopoverHeader fontWeight="bold">{children}</PopoverHeader>
<PopoverBody>
{events === undefined && <Spinner />}
{sorted.map((page) => (
<Text key={getEventUID(page)} noOfLines={2} mb="2">
<UserName pubkey={page.pubkey} />: {getPageSummary(page)}
</Text>
))}
{events?.length === 0 && <Text fontStyle="italic">There is no entry for this topic</Text>}
</PopoverBody>
</PopoverContent>
</Portal>
</Popover>
);
}

View File

@ -14,6 +14,7 @@ import TimelineActionAndStatus from "../../components/timeline-page/timeline-act
import { ErrorBoundary } from "../../components/error-boundary";
import { WIKI_RELAYS } from "../../const";
import { ExternalLinkIcon } from "../../components/icons";
import WikiLink from "./components/wiki-link";
function eventFilter(event: NostrEvent) {
if (!validatePage(event)) return false;
@ -30,9 +31,9 @@ export default function WikiHomeView() {
<VerticalPageLayout>
<Flex mx="auto" mt="10vh" mb="10vh" direction="column" alignItems="center" maxW="full">
<Heading>
<Link as={RouterLink} to="/wiki/topic/wikifreedia">
<WikiLink topic="wikifreedia" color="inherit">
Wikifreedia
</Link>
</WikiLink>
</Heading>
<Link isExternal color="blue.500" href="https://wikifreedia.xyz/">
wikifreedia.xyz <ExternalLinkIcon />

View File

@ -33,7 +33,7 @@ import FileSearch01 from "../../components/icons/file-search-01";
import NoteZapButton from "../../components/note/note-zap-button";
import ZapBubbles from "../../components/note/timeline-note/components/zap-bubbles";
import QuoteRepostButton from "../../components/note/quote-repost-button";
import WikiPageMenu from "./components/wioki-page-menu";
import WikiPageMenu from "./components/wiki-page-menu";
import EventVoteButtons from "../../components/reactions/event-vote-buttions";
import useCurrentAccount from "../../hooks/use-current-account";