mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-10 12:49:29 +02:00
show relay notices in task manager
This commit is contained in:
parent
b7572d373e
commit
2e6b79ef6a
@ -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);
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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)) {
|
||||
|
@ -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}
|
||||
|
@ -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>;
|
||||
};
|
||||
|
@ -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;
|
@ -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");
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
115
src/views/wiki/components/wiki-link.tsx
Normal file
115
src/views/wiki/components/wiki-link.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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 />
|
||||
|
@ -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";
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user