mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-26 03:28:38 +02:00
add embedded wiki page card
improve markdown formatting fix wiki pages when logged out handle wiki naddr
This commit is contained in:
@@ -4,7 +4,7 @@ import { NostrEvent } from "../types/nostr-event";
|
|||||||
import relayPoolService from "../services/relay-pool";
|
import relayPoolService from "../services/relay-pool";
|
||||||
import { isFilterEqual } from "../helpers/nostr/filter";
|
import { isFilterEqual } from "../helpers/nostr/filter";
|
||||||
import ControlledObservable from "./controlled-observable";
|
import ControlledObservable from "./controlled-observable";
|
||||||
import { Filter, Relay, Subscription } from "nostr-tools";
|
import { AbstractRelay, Filter, Subscription } from "nostr-tools";
|
||||||
import { offlineMode } from "../services/offline-mode";
|
import { offlineMode } from "../services/offline-mode";
|
||||||
import RelaySet from "./relay-set";
|
import RelaySet from "./relay-set";
|
||||||
|
|
||||||
@@ -17,8 +17,8 @@ export default class NostrMultiSubscription {
|
|||||||
name?: string;
|
name?: string;
|
||||||
filters: Filter[] = [];
|
filters: Filter[] = [];
|
||||||
|
|
||||||
relays: Relay[] = [];
|
relays: AbstractRelay[] = [];
|
||||||
subscriptions = new Map<Relay, Subscription>();
|
subscriptions = new Map<AbstractRelay, Subscription>();
|
||||||
|
|
||||||
state = NostrMultiSubscription.INIT;
|
state = NostrMultiSubscription.INIT;
|
||||||
onEvent = new ControlledObservable<NostrEvent>();
|
onEvent = new ControlledObservable<NostrEvent>();
|
||||||
@@ -34,10 +34,10 @@ export default class NostrMultiSubscription {
|
|||||||
this.seenEvents.add(event.id);
|
this.seenEvents.add(event.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleAddRelay(relay: Relay) {
|
private handleAddRelay(relay: AbstractRelay) {
|
||||||
relayPoolService.addClaim(relay.url, this);
|
relayPoolService.addClaim(relay.url, this);
|
||||||
}
|
}
|
||||||
private handleRemoveRelay(relay: Relay) {
|
private handleRemoveRelay(relay: AbstractRelay) {
|
||||||
relayPoolService.removeClaim(relay.url, this);
|
relayPoolService.removeClaim(relay.url, this);
|
||||||
|
|
||||||
// close subscription
|
// close subscription
|
||||||
@@ -96,7 +96,8 @@ export default class NostrMultiSubscription {
|
|||||||
subscription.filters = filters;
|
subscription.filters = filters;
|
||||||
subscription.fire();
|
subscription.fire();
|
||||||
} else {
|
} else {
|
||||||
if (filters.length === 0) debugger;
|
if (!relay.connected) relayPoolService.requestConnect(relay);
|
||||||
|
|
||||||
subscription = relay.subscribe(filters, {
|
subscription = relay.subscribe(filters, {
|
||||||
onevent: (event) => this.handleEvent(event),
|
onevent: (event) => this.handleEvent(event),
|
||||||
onclose: () => {
|
onclose: () => {
|
||||||
@@ -112,7 +113,12 @@ export default class NostrMultiSubscription {
|
|||||||
}
|
}
|
||||||
|
|
||||||
publish(event: NostrEvent) {
|
publish(event: NostrEvent) {
|
||||||
return Promise.allSettled(this.relays.map((r) => r.publish(event)));
|
return Promise.allSettled(
|
||||||
|
this.relays.map(async (r) => {
|
||||||
|
if (!r.connected) await relayPoolService.requestConnect(r);
|
||||||
|
return await r.publish(event);
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
open() {
|
open() {
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { AbstractRelay } from "nostr-tools";
|
import { AbstractRelay } from "nostr-tools";
|
||||||
|
|
||||||
import { logger } from "../helpers/debug";
|
import { logger } from "../helpers/debug";
|
||||||
import { validateRelayURL } from "../helpers/relay";
|
import { validateRelayURL } from "../helpers/relay";
|
||||||
import { offlineMode } from "../services/offline-mode";
|
import { offlineMode } from "../services/offline-mode";
|
||||||
@@ -35,7 +36,20 @@ export default class RelayPool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const relay = this.relays.get(key) as AbstractRelay;
|
const relay = this.relays.get(key) as AbstractRelay;
|
||||||
if (connect && !relay.connected && !offlineMode.value) {
|
if (connect) this.requestConnect(relay);
|
||||||
|
return relay;
|
||||||
|
}
|
||||||
|
|
||||||
|
async requestConnect(relayOrUrl: string | URL | AbstractRelay) {
|
||||||
|
let relay: AbstractRelay | undefined = undefined;
|
||||||
|
|
||||||
|
if (typeof relayOrUrl === "string") relay = this.relays.get(relayOrUrl);
|
||||||
|
else if (relayOrUrl instanceof URL) relay = this.relays.get(relayOrUrl.toString());
|
||||||
|
else relay = relayOrUrl;
|
||||||
|
|
||||||
|
if (!relay) return;
|
||||||
|
|
||||||
|
if (!relay.connected && !offlineMode.value) {
|
||||||
try {
|
try {
|
||||||
relay.connect();
|
relay.connect();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -43,7 +57,6 @@ export default class RelayPool {
|
|||||||
this.log(e);
|
this.log(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return relay;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pruneRelays() {
|
pruneRelays() {
|
||||||
|
@@ -0,0 +1,66 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardProps,
|
||||||
|
Heading,
|
||||||
|
LinkBox,
|
||||||
|
Text,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
|
import { nip19 } from "nostr-tools";
|
||||||
|
|
||||||
|
import { NostrEvent } from "../../../types/nostr-event";
|
||||||
|
import UserLink from "../../user/user-link";
|
||||||
|
import { getPageForks, getPageSummary, getPageTitle } from "../../../helpers/nostr/wiki";
|
||||||
|
import HoverLinkOverlay from "../../hover-link-overlay";
|
||||||
|
import { getSharableEventAddress } from "../../../helpers/nip19";
|
||||||
|
import Timestamp from "../../timestamp";
|
||||||
|
import GitBranch01 from "../../icons/git-branch-01";
|
||||||
|
import UserName from "../../user/user-name";
|
||||||
|
|
||||||
|
export default function EmbeddedWikiPage({ page: page, ...props }: Omit<CardProps, "children"> & { page: NostrEvent }) {
|
||||||
|
const { address } = useMemo(() => getPageForks(page), [page]);
|
||||||
|
const showFooter = !!address;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card as={LinkBox} {...props}>
|
||||||
|
<CardHeader p="2" pb="0" display="flex" gap="2" alignItems="center">
|
||||||
|
<Heading size="md">
|
||||||
|
<HoverLinkOverlay as={RouterLink} to={`/wiki/page/${getSharableEventAddress(page)}`}>
|
||||||
|
{getPageTitle(page)}
|
||||||
|
</HoverLinkOverlay>
|
||||||
|
</Heading>
|
||||||
|
<Text>
|
||||||
|
by <UserLink pubkey={page.pubkey} fontWeight="bold " /> - <Timestamp timestamp={page.created_at} />
|
||||||
|
</Text>
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody p="2" overflow="hidden">
|
||||||
|
<Text color="GrayText" noOfLines={2}>
|
||||||
|
{getPageSummary(page)}
|
||||||
|
</Text>
|
||||||
|
</CardBody>
|
||||||
|
{showFooter && (
|
||||||
|
<CardFooter>
|
||||||
|
<ButtonGroup variant="link" mt="auto">
|
||||||
|
{address && (
|
||||||
|
<Button
|
||||||
|
as={RouterLink}
|
||||||
|
to={`/wiki/page/${nip19.naddrEncode(address)}`}
|
||||||
|
p="2"
|
||||||
|
colorScheme="blue"
|
||||||
|
leftIcon={<GitBranch01 />}
|
||||||
|
>
|
||||||
|
<UserName pubkey={address.pubkey} />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ButtonGroup>
|
||||||
|
</CardFooter>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
@@ -41,6 +41,8 @@ import { FLARE_VIDEO_KIND } from "../../helpers/nostr/flare";
|
|||||||
import EmbeddedFlareVideo from "./event-types/embedded-flare-video";
|
import EmbeddedFlareVideo from "./event-types/embedded-flare-video";
|
||||||
import LoadingNostrLink from "../loading-nostr-link";
|
import LoadingNostrLink from "../loading-nostr-link";
|
||||||
import EmbeddedRepost from "./event-types/embedded-repost";
|
import EmbeddedRepost from "./event-types/embedded-repost";
|
||||||
|
import { WIKI_PAGE_KIND } from "../../helpers/nostr/wiki";
|
||||||
|
import EmbeddedWikiPage from "./event-types/embedded-wiki-page";
|
||||||
const EmbeddedStemstrTrack = lazy(() => import("./event-types/embedded-stemstr-track"));
|
const EmbeddedStemstrTrack = lazy(() => import("./event-types/embedded-stemstr-track"));
|
||||||
|
|
||||||
export type EmbedProps = {
|
export type EmbedProps = {
|
||||||
@@ -93,6 +95,8 @@ export function EmbedEvent({
|
|||||||
case kinds.Repost:
|
case kinds.Repost:
|
||||||
case kinds.GenericRepost:
|
case kinds.GenericRepost:
|
||||||
return <EmbeddedRepost repost={event} {...cardProps} />;
|
return <EmbeddedRepost repost={event} {...cardProps} />;
|
||||||
|
case WIKI_PAGE_KIND:
|
||||||
|
return <EmbeddedWikiPage page={event} {...cardProps} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <EmbeddedUnknown event={event} {...cardProps} />;
|
return <EmbeddedUnknown event={event} {...cardProps} />;
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Flex, Tag, TagLabel } from "@chakra-ui/react";
|
import { Flex, FlexProps, Tag, TagLabel } from "@chakra-ui/react";
|
||||||
import { NostrEvent } from "nostr-tools";
|
import { NostrEvent } from "nostr-tools";
|
||||||
import { getEventUID } from "nostr-idb";
|
import { getEventUID } from "nostr-idb";
|
||||||
import styled from "@emotion/styled";
|
import styled from "@emotion/styled";
|
||||||
@@ -16,7 +16,7 @@ const HiddenScrollbar = styled(Flex)`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default function ZapBubbles({ event }: { event: NostrEvent }) {
|
export default function ZapBubbles({ event, ...props }: { event: NostrEvent } & Omit<FlexProps, "children">) {
|
||||||
const zaps = useEventZaps(getEventUID(event));
|
const zaps = useEventZaps(getEventUID(event));
|
||||||
|
|
||||||
if (zaps.length === 0) return null;
|
if (zaps.length === 0) return null;
|
||||||
@@ -24,10 +24,10 @@ export default function ZapBubbles({ event }: { event: NostrEvent }) {
|
|||||||
const sorted = zaps.sort((a, b) => (b.payment.amount ?? 0) - (a.payment.amount ?? 0));
|
const sorted = zaps.sort((a, b) => (b.payment.amount ?? 0) - (a.payment.amount ?? 0));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HiddenScrollbar overflowY="hidden" overflowX="auto" gap="2">
|
<HiddenScrollbar overflowY="hidden" overflowX="auto" gap="2" {...props}>
|
||||||
{sorted.map((zap) => (
|
{sorted.map((zap) => (
|
||||||
<Tag key={zap.event.id} borderRadius="full" py="1" flexShrink={0} variant="outline">
|
<Tag key={zap.event.id} borderRadius="full" py="1" flexShrink={0} variant="outline">
|
||||||
<LightningIcon mr="1" />
|
<LightningIcon mr="1" color="yellow.400" />
|
||||||
<TagLabel fontWeight="bold">{readablizeSats((zap.payment.amount ?? 0) / 1000)}</TagLabel>
|
<TagLabel fontWeight="bold">{readablizeSats((zap.payment.amount ?? 0) / 1000)}</TagLabel>
|
||||||
<UserAvatar pubkey={zap.request.pubkey} size="xs" square={false} ml="2" />
|
<UserAvatar pubkey={zap.request.pubkey} size="xs" square={false} ml="2" />
|
||||||
</Tag>
|
</Tag>
|
||||||
|
@@ -19,8 +19,8 @@ export function getPageSummary(page: NostrEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getPageForks(page: NostrEvent) {
|
export function getPageForks(page: NostrEvent) {
|
||||||
const addressFork = page.tags.find((t) => t[0] === "a" && t[1] && t[3]);
|
const addressFork = page.tags.find((t) => t[0] === "a" && t[1] && t[3] === "fork");
|
||||||
const eventFork = page.tags.find((t) => t[0] === "a" && t[1] && t[3]);
|
const eventFork = page.tags.find((t) => t[0] === "a" && t[1] && t[3] === "fork");
|
||||||
|
|
||||||
const address = addressFork ? parseCoordinate(addressFork[1], true) ?? undefined : undefined;
|
const address = addressFork ? parseCoordinate(addressFork[1], true) ?? undefined : undefined;
|
||||||
const event: nip19.EventPointer | undefined = eventFork ? { id: eventFork[1] } : undefined;
|
const event: nip19.EventPointer | undefined = eventFork ? { id: eventFork[1] } : undefined;
|
||||||
@@ -28,6 +28,16 @@ export function getPageForks(page: NostrEvent) {
|
|||||||
return { event, address };
|
return { event, address };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getPageDefer(page: NostrEvent) {
|
||||||
|
const addressTag = page.tags.find((t) => t[0] === "a" && t[1] && t[3] === "defer");
|
||||||
|
const eventTag = page.tags.find((t) => t[0] === "a" && t[1] && t[3] === "defer");
|
||||||
|
|
||||||
|
const address = addressTag ? parseCoordinate(addressTag[1], true) ?? undefined : undefined;
|
||||||
|
const event: nip19.EventPointer | undefined = eventTag ? { id: eventTag[1] } : undefined;
|
||||||
|
|
||||||
|
if (event || address) return { event, address };
|
||||||
|
}
|
||||||
|
|
||||||
export function isPageFork(page: NostrEvent) {
|
export function isPageFork(page: NostrEvent) {
|
||||||
return page.tags.some((t) => (t[0] === "a" || t[0] === "e") && t[3] === "fork");
|
return page.tags.some((t) => (t[0] === "a" || t[0] === "e") && t[3] === "fork");
|
||||||
}
|
}
|
||||||
|
@@ -158,7 +158,7 @@ export function subscribeMany(relays: string[], filters: Filter[], params: Subsc
|
|||||||
let relay: AbstractRelay;
|
let relay: AbstractRelay;
|
||||||
try {
|
try {
|
||||||
relay = relayPoolService.requestRelay(url);
|
relay = relayPoolService.requestRelay(url);
|
||||||
await relay.connect();
|
await relayPoolService.requestConnect(relay);
|
||||||
// changed from nostr-tools
|
// changed from nostr-tools
|
||||||
// relay = await this.ensureRelay(url, {
|
// relay = await this.ensureRelay(url, {
|
||||||
// connectionTimeout: params.maxWait ? Math.max(params.maxWait * 0.8, params.maxWait - 1000) : undefined,
|
// connectionTimeout: params.maxWait ? Math.max(params.maxWait * 0.8, params.maxWait - 1000) : undefined,
|
||||||
|
@@ -9,7 +9,7 @@ import replaceableEventsService from "./replaceable-events";
|
|||||||
import { getPubkeysFromList } from "../helpers/nostr/lists";
|
import { getPubkeysFromList } from "../helpers/nostr/lists";
|
||||||
|
|
||||||
const log = logger.extend("web-of-trust");
|
const log = logger.extend("web-of-trust");
|
||||||
let webOfTrust: PubkeyGraph;
|
let webOfTrust = new PubkeyGraph("");
|
||||||
|
|
||||||
let newEvents = 0;
|
let newEvents = 0;
|
||||||
const throttleUpdateWebOfTrust = _throttle(() => {
|
const throttleUpdateWebOfTrust = _throttle(() => {
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { Alert, AlertIcon, AlertTitle } from "@chakra-ui/react";
|
import { Alert, AlertIcon, AlertTitle } from "@chakra-ui/react";
|
||||||
import { Navigate, useParams } from "react-router-dom";
|
import { Navigate, useParams } from "react-router-dom";
|
||||||
import { kinds, nip19 } from "nostr-tools";
|
import { kinds, nip19 } from "nostr-tools";
|
||||||
|
|
||||||
import { STREAM_KIND } from "../../helpers/nostr/stream";
|
import { STREAM_KIND } from "../../helpers/nostr/stream";
|
||||||
import { EMOJI_PACK_KIND } from "../../helpers/nostr/emoji-packs";
|
import { EMOJI_PACK_KIND } from "../../helpers/nostr/emoji-packs";
|
||||||
import { NOTE_LIST_KIND, PEOPLE_LIST_KIND } from "../../helpers/nostr/lists";
|
import { NOTE_LIST_KIND, PEOPLE_LIST_KIND } from "../../helpers/nostr/lists";
|
||||||
@@ -8,6 +9,8 @@ import { ErrorBoundary } from "../../components/error-boundary";
|
|||||||
import { COMMUNITY_DEFINITION_KIND } from "../../helpers/nostr/communities";
|
import { COMMUNITY_DEFINITION_KIND } from "../../helpers/nostr/communities";
|
||||||
import { TORRENT_KIND } from "../../helpers/nostr/torrents";
|
import { TORRENT_KIND } from "../../helpers/nostr/torrents";
|
||||||
import { FLARE_VIDEO_KIND } from "../../helpers/nostr/flare";
|
import { FLARE_VIDEO_KIND } from "../../helpers/nostr/flare";
|
||||||
|
import { WIKI_PAGE_KIND } from "../../helpers/nostr/wiki";
|
||||||
|
import { EmbedEventPointer } from "../../components/embed-event";
|
||||||
|
|
||||||
function NostrLinkPage() {
|
function NostrLinkPage() {
|
||||||
const { link } = useParams() as { link?: string };
|
const { link } = useParams() as { link?: string };
|
||||||
@@ -41,15 +44,17 @@ function NostrLinkPage() {
|
|||||||
if (decoded.data.kind === FLARE_VIDEO_KIND) return <Navigate to={`/videos/${cleanLink}`} replace />;
|
if (decoded.data.kind === FLARE_VIDEO_KIND) return <Navigate to={`/videos/${cleanLink}`} replace />;
|
||||||
if (decoded.data.kind === kinds.ChannelCreation) return <Navigate to={`/channels/${cleanLink}`} replace />;
|
if (decoded.data.kind === kinds.ChannelCreation) return <Navigate to={`/channels/${cleanLink}`} replace />;
|
||||||
if (decoded.data.kind === kinds.ShortTextNote) return <Navigate to={`/n/${cleanLink}`} replace />;
|
if (decoded.data.kind === kinds.ShortTextNote) return <Navigate to={`/n/${cleanLink}`} replace />;
|
||||||
// if there is no kind redirect to the thread view
|
if (decoded.data.kind === WIKI_PAGE_KIND) return <Navigate to={`/wiki/page/${cleanLink}`} replace />;
|
||||||
return <Navigate to={`/n/${cleanLink}`} replace />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert status="warning">
|
<>
|
||||||
<AlertIcon />
|
<Alert status="warning">
|
||||||
<AlertTitle>Unknown type {JSON.stringify(decoded.data)}</AlertTitle>
|
<AlertIcon />
|
||||||
</Alert>
|
<AlertTitle>Unknown event kind</AlertTitle>
|
||||||
|
</Alert>
|
||||||
|
<EmbedEventPointer pointer={decoded} />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,8 +1,81 @@
|
|||||||
import { Image, Link, LinkProps, Text, TextProps } from "@chakra-ui/react";
|
import {
|
||||||
|
Code,
|
||||||
|
CodeProps,
|
||||||
|
Heading,
|
||||||
|
HeadingProps,
|
||||||
|
Image,
|
||||||
|
Link,
|
||||||
|
LinkProps,
|
||||||
|
ListItem,
|
||||||
|
OrderedList,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableProps,
|
||||||
|
Tbody,
|
||||||
|
Td,
|
||||||
|
Text,
|
||||||
|
TextProps,
|
||||||
|
Tfoot,
|
||||||
|
Th,
|
||||||
|
Thead,
|
||||||
|
Tr,
|
||||||
|
UnorderedList,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import styled from "@emotion/styled";
|
||||||
import { NostrEvent } from "nostr-tools";
|
import { NostrEvent } from "nostr-tools";
|
||||||
import Markdown, { Components } from "react-markdown";
|
import Markdown, { Components } from "react-markdown";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
|
|
||||||
|
const StyledMarkdown = styled(Markdown)`
|
||||||
|
pre > code {
|
||||||
|
display: block;
|
||||||
|
padding-block: var(--chakra-space-2);
|
||||||
|
padding-inline: var(--chakra-space-4);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function H1({ children, ...props }: HeadingProps) {
|
||||||
|
return (
|
||||||
|
<Heading as="h1" size="xl" mt="6" mb="2" {...props}>
|
||||||
|
{children}
|
||||||
|
</Heading>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function H2({ children, ...props }: HeadingProps) {
|
||||||
|
return (
|
||||||
|
<Heading as="h2" size="lg" mt="6" mb="2" {...props}>
|
||||||
|
{children}
|
||||||
|
</Heading>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function H3({ children, ...props }: HeadingProps) {
|
||||||
|
return (
|
||||||
|
<Heading as="h3" size="md" mt="4" mb="2" {...props}>
|
||||||
|
{children}
|
||||||
|
</Heading>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function H4({ children, ...props }: HeadingProps) {
|
||||||
|
return (
|
||||||
|
<Heading as="h4" size="sm" my="2" {...props}>
|
||||||
|
{children}
|
||||||
|
</Heading>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function H5({ children, ...props }: HeadingProps) {
|
||||||
|
return (
|
||||||
|
<Heading as="h5" size="xs" my="2" {...props}>
|
||||||
|
{children}
|
||||||
|
</Heading>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function H6({ children, ...props }: HeadingProps) {
|
||||||
|
return (
|
||||||
|
<Heading as="h6" size="xs" my="2" {...props}>
|
||||||
|
{children}
|
||||||
|
</Heading>
|
||||||
|
);
|
||||||
|
}
|
||||||
function A({ children, ...props }: LinkProps) {
|
function A({ children, ...props }: LinkProps) {
|
||||||
return (
|
return (
|
||||||
<Link color="blue.500" isExternal {...props}>
|
<Link color="blue.500" isExternal {...props}>
|
||||||
@@ -12,22 +85,48 @@ function A({ children, ...props }: LinkProps) {
|
|||||||
}
|
}
|
||||||
function P({ children, ...props }: TextProps) {
|
function P({ children, ...props }: TextProps) {
|
||||||
return (
|
return (
|
||||||
<Text py="2" {...props}>
|
<Text my="2" {...props}>
|
||||||
{children}
|
{children}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
function TableWithContainer({ children, ...props }: TableProps) {
|
||||||
|
return (
|
||||||
|
<TableContainer>
|
||||||
|
<Table size="sm" mb="4" {...props}>
|
||||||
|
{children}
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const components: Partial<Components> = {
|
const components: Partial<Components> = {
|
||||||
|
h1: H1,
|
||||||
|
h2: H2,
|
||||||
|
h3: H3,
|
||||||
|
h4: H4,
|
||||||
|
h5: H5,
|
||||||
|
h6: H6,
|
||||||
a: A,
|
a: A,
|
||||||
img: Image,
|
img: Image,
|
||||||
p: P,
|
p: P,
|
||||||
|
ul: UnorderedList,
|
||||||
|
ol: OrderedList,
|
||||||
|
li: ListItem,
|
||||||
|
table: TableWithContainer,
|
||||||
|
thead: Thead,
|
||||||
|
tbody: Tbody,
|
||||||
|
tfoot: Tfoot,
|
||||||
|
tr: Tr,
|
||||||
|
td: Td,
|
||||||
|
th: Th,
|
||||||
|
code: Code,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function MarkdownContent({ event }: { event: NostrEvent }) {
|
export default function MarkdownContent({ event }: { event: NostrEvent }) {
|
||||||
return (
|
return (
|
||||||
<Markdown remarkPlugins={[remarkGfm]} components={components}>
|
<StyledMarkdown remarkPlugins={[remarkGfm]} components={components}>
|
||||||
{event.content}
|
{event.content}
|
||||||
</Markdown>
|
</StyledMarkdown>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -19,21 +19,19 @@ export default function WikiPageResult({ page, compare }: { page: NostrEvent; co
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex as={LinkBox} py="2" px="4" direction="column">
|
<Flex as={LinkBox} py="2" px="4" direction="column">
|
||||||
<Flex gap="2" alignItems="center">
|
<Box overflow="hidden">
|
||||||
<Box overflow="hidden">
|
<Heading size="md">
|
||||||
<Heading size="md">
|
<HoverLinkOverlay as={RouterLink} to={`/wiki/page/${getSharableEventAddress(page)}`}>
|
||||||
<HoverLinkOverlay as={RouterLink} to={`/wiki/page/${getSharableEventAddress(page)}`}>
|
{getPageTitle(page)}
|
||||||
{getPageTitle(page)}
|
</HoverLinkOverlay>
|
||||||
</HoverLinkOverlay>
|
</Heading>
|
||||||
</Heading>
|
<Text>
|
||||||
<Text>
|
by <UserLink pubkey={page.pubkey} fontWeight="bold " /> - <Timestamp timestamp={page.created_at} />
|
||||||
by <UserLink pubkey={page.pubkey} fontWeight="bold " /> - <Timestamp timestamp={page.created_at} />
|
</Text>
|
||||||
</Text>
|
<Text color="GrayText" noOfLines={2}>
|
||||||
<Text color="GrayText" noOfLines={2}>
|
{getPageSummary(page)}
|
||||||
{getPageSummary(page)}
|
</Text>
|
||||||
</Text>
|
</Box>
|
||||||
</Box>
|
|
||||||
</Flex>
|
|
||||||
<ButtonGroup variant="link" mt="auto">
|
<ButtonGroup variant="link" mt="auto">
|
||||||
{address && (
|
{address && (
|
||||||
<Button
|
<Button
|
||||||
|
@@ -7,8 +7,7 @@ export default function WikiSearchForm({ ...props }: Omit<FlexProps, "children">
|
|||||||
const { register, handleSubmit } = useForm({ defaultValues: { search: "" } });
|
const { register, handleSubmit } = useForm({ defaultValues: { search: "" } });
|
||||||
|
|
||||||
const onSubmit = handleSubmit((values) => {
|
const onSubmit = handleSubmit((values) => {
|
||||||
// navigate(`/wiki/search?q=${encodeURIComponent(values.search)}`);
|
navigate(`/wiki/search?q=${encodeURIComponent(values.search)}`);
|
||||||
navigate(`/wiki/topic/${values.search}`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
24
src/views/wiki/components/wioki-page-menu.tsx
Normal file
24
src/views/wiki/components/wioki-page-menu.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { NostrEvent } from "nostr-tools";
|
||||||
|
|
||||||
|
import { DotsMenuButton, MenuIconButtonProps } from "../../../components/dots-menu-button";
|
||||||
|
import OpenInAppMenuItem from "../../../components/common-menu-items/open-in-app";
|
||||||
|
import CopyShareLinkMenuItem from "../../../components/common-menu-items/copy-share-link";
|
||||||
|
import CopyEmbedCodeMenuItem from "../../../components/common-menu-items/copy-embed-code";
|
||||||
|
import DebugEventMenuItem from "../../../components/debug-modal/debug-event-menu-item";
|
||||||
|
|
||||||
|
export default function WikiPageMenu({
|
||||||
|
page,
|
||||||
|
...props
|
||||||
|
}: { page: NostrEvent } & Omit<MenuIconButtonProps, "children">) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DotsMenuButton {...props}>
|
||||||
|
<OpenInAppMenuItem event={page} />
|
||||||
|
<CopyShareLinkMenuItem event={page} />
|
||||||
|
<CopyEmbedCodeMenuItem event={page} />
|
||||||
|
|
||||||
|
<DebugEventMenuItem event={page} />
|
||||||
|
</DotsMenuButton>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -14,6 +14,7 @@ import WikiPageResult from "./components/wiki-page-result";
|
|||||||
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
|
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
|
||||||
import { ErrorBoundary } from "../../components/error-boundary";
|
import { ErrorBoundary } from "../../components/error-boundary";
|
||||||
import { WIKI_RELAYS } from "../../const";
|
import { WIKI_RELAYS } from "../../const";
|
||||||
|
import { ExternalLinkIcon } from "../../components/icons";
|
||||||
|
|
||||||
function eventFilter(event: NostrEvent) {
|
function eventFilter(event: NostrEvent) {
|
||||||
if (!validatePage(event)) return false;
|
if (!validatePage(event)) return false;
|
||||||
@@ -29,13 +30,16 @@ export default function WikiHomeView() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<VerticalPageLayout>
|
<VerticalPageLayout>
|
||||||
<Flex mx="auto" mt="10vh" mb="10vh" direction="column" alignItems="center" maxW="full" gap="4">
|
<Flex mx="auto" mt="10vh" mb="10vh" direction="column" alignItems="center" maxW="full">
|
||||||
<Heading>
|
<Heading>
|
||||||
<Link as={RouterLink} to="/wiki/topic/wikifreedia">
|
<Link as={RouterLink} to="/wiki/topic/wikifreedia">
|
||||||
Wikifreedia
|
Wikifreedia
|
||||||
</Link>
|
</Link>
|
||||||
</Heading>
|
</Heading>
|
||||||
<WikiSearchForm maxW="full" />
|
<Link isExternal color="blue.500" href="https://wikifreedia.xyz/">
|
||||||
|
wikifreedia.xyz <ExternalLinkIcon />
|
||||||
|
</Link>
|
||||||
|
<WikiSearchForm maxW="full" mt="4" />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Heading size="md" mt="4">
|
<Heading size="md" mt="4">
|
||||||
|
@@ -16,7 +16,7 @@ import { Link as RouterLink } from "react-router-dom";
|
|||||||
import useParamsAddressPointer from "../../hooks/use-params-address-pointer";
|
import useParamsAddressPointer from "../../hooks/use-params-address-pointer";
|
||||||
import useReplaceableEvent from "../../hooks/use-replaceable-event";
|
import useReplaceableEvent from "../../hooks/use-replaceable-event";
|
||||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||||
import { getPageForks, getPageTitle, getPageTopic } from "../../helpers/nostr/wiki";
|
import { getPageDefer, getPageForks, getPageTitle, getPageTopic } from "../../helpers/nostr/wiki";
|
||||||
import MarkdownContent from "./components/markdown";
|
import MarkdownContent from "./components/markdown";
|
||||||
import UserLink from "../../components/user/user-link";
|
import UserLink from "../../components/user/user-link";
|
||||||
import { getWebOfTrust } from "../../services/web-of-trust";
|
import { getWebOfTrust } from "../../services/web-of-trust";
|
||||||
@@ -33,6 +33,54 @@ import FileSearch01 from "../../components/icons/file-search-01";
|
|||||||
import NoteZapButton from "../../components/note/note-zap-button";
|
import NoteZapButton from "../../components/note/note-zap-button";
|
||||||
import ZapBubbles from "../../components/note/timeline-note/components/zap-bubbles";
|
import ZapBubbles from "../../components/note/timeline-note/components/zap-bubbles";
|
||||||
import QuoteRepostButton from "../../components/note/quote-repost-button";
|
import QuoteRepostButton from "../../components/note/quote-repost-button";
|
||||||
|
import WikiPageMenu from "./components/wioki-page-menu";
|
||||||
|
|
||||||
|
function ForkAlert({ page, address }: { page: NostrEvent; address: nip19.AddressPointer }) {
|
||||||
|
const topic = getPageTopic(page);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Alert status="info" display="flex" flexWrap="wrap">
|
||||||
|
<AlertIcon>
|
||||||
|
<GitBranch01 boxSize={5} />
|
||||||
|
</AlertIcon>
|
||||||
|
<Text>
|
||||||
|
This page was forked from <UserLink pubkey={address.pubkey} fontWeight="bold" /> version
|
||||||
|
</Text>
|
||||||
|
<ButtonGroup variant="link" ml="auto">
|
||||||
|
<Button leftIcon={<ExternalLinkIcon />} as={RouterLink} to={`/wiki/page/${nip19.naddrEncode(address)}`}>
|
||||||
|
Original
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
leftIcon={<FileSearch01 />}
|
||||||
|
as={RouterLink}
|
||||||
|
to={`/wiki/compare/${topic}/${address.pubkey}/${page.pubkey}`}
|
||||||
|
>
|
||||||
|
Compare
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DeferAlert({ page, address }: { page: NostrEvent; address: nip19.AddressPointer }) {
|
||||||
|
return (
|
||||||
|
<Alert status="warning" display="flex" flexWrap="wrap">
|
||||||
|
<AlertIcon />
|
||||||
|
<Text>
|
||||||
|
The author of this page has deferred to <UserLink pubkey={address.pubkey} fontWeight="bold" /> version
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
leftIcon={<ExternalLinkIcon />}
|
||||||
|
as={RouterLink}
|
||||||
|
to={`/wiki/page/${nip19.naddrEncode(address)}`}
|
||||||
|
variant="link"
|
||||||
|
ml="4"
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</Button>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function WikiPagePage({ page }: { page: NostrEvent }) {
|
function WikiPagePage({ page }: { page: NostrEvent }) {
|
||||||
const topic = getPageTopic(page);
|
const topic = getPageTopic(page);
|
||||||
@@ -40,6 +88,7 @@ function WikiPagePage({ page }: { page: NostrEvent }) {
|
|||||||
|
|
||||||
const pages = useSubject(timeline.timeline).filter((p) => p.pubkey !== page.pubkey);
|
const pages = useSubject(timeline.timeline).filter((p) => p.pubkey !== page.pubkey);
|
||||||
const { address } = getPageForks(page);
|
const { address } = getPageForks(page);
|
||||||
|
const defer = getPageDefer(page);
|
||||||
|
|
||||||
const forks = getWebOfTrust().sortByDistanceAndConnections(
|
const forks = getWebOfTrust().sortByDistanceAndConnections(
|
||||||
pages.filter((p) => getPageForks(p).address?.pubkey === page.pubkey),
|
pages.filter((p) => getPageForks(p).address?.pubkey === page.pubkey),
|
||||||
@@ -58,37 +107,17 @@ function WikiPagePage({ page }: { page: NostrEvent }) {
|
|||||||
<ButtonGroup float="right">
|
<ButtonGroup float="right">
|
||||||
<QuoteRepostButton event={page} />
|
<QuoteRepostButton event={page} />
|
||||||
<NoteZapButton event={page} showEventPreview={false} />
|
<NoteZapButton event={page} showEventPreview={false} />
|
||||||
<DebugEventButton event={page} />
|
<WikiPageMenu page={page} aria-label="Page Options" />
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
<Heading>{getPageTitle(page)}</Heading>
|
<Heading>{getPageTitle(page)}</Heading>
|
||||||
<Text>
|
<Text>
|
||||||
by <UserLink pubkey={page.pubkey} /> - <Timestamp timestamp={page.created_at} />
|
by <UserLink pubkey={page.pubkey} /> - <Timestamp timestamp={page.created_at} />
|
||||||
</Text>
|
</Text>
|
||||||
{address && (
|
{address && <ForkAlert page={page} address={address} />}
|
||||||
<Alert status="info" display="flex" flexWrap="wrap">
|
{defer?.address && <DeferAlert page={page} address={defer.address} />}
|
||||||
<AlertIcon>
|
|
||||||
<GitBranch01 boxSize={5} />
|
|
||||||
</AlertIcon>
|
|
||||||
<Text>
|
|
||||||
This page was forked from <UserLink pubkey={address.pubkey} fontWeight="bold" /> version
|
|
||||||
</Text>
|
|
||||||
<ButtonGroup variant="link" ml="auto">
|
|
||||||
<Button leftIcon={<ExternalLinkIcon />} as={RouterLink} to={`/wiki/page/${nip19.naddrEncode(address)}`}>
|
|
||||||
Original
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
leftIcon={<FileSearch01 />}
|
|
||||||
as={RouterLink}
|
|
||||||
to={`/wiki/compare/${topic}/${address.pubkey}/${page.pubkey}`}
|
|
||||||
>
|
|
||||||
Compare
|
|
||||||
</Button>
|
|
||||||
</ButtonGroup>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
<Divider my="2" />
|
<Divider my="2" />
|
||||||
<MarkdownContent event={page} />
|
<MarkdownContent event={page} />
|
||||||
<ZapBubbles event={page} />
|
<ZapBubbles event={page} mt="4" />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{forks.length > 0 && (
|
{forks.length > 0 && (
|
||||||
|
@@ -2,14 +2,18 @@ import { useEffect, useState } from "react";
|
|||||||
import { Navigate } from "react-router-dom";
|
import { Navigate } from "react-router-dom";
|
||||||
import { Button, Flex, Heading, Input, Link } from "@chakra-ui/react";
|
import { Button, Flex, Heading, Input, Link } from "@chakra-ui/react";
|
||||||
import { Link as RouterLink } from "react-router-dom";
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
import { NostrEvent } from "nostr-tools";
|
import { Filter, NostrEvent } from "nostr-tools";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
import { Subscription, getEventUID } from "nostr-idb";
|
||||||
|
|
||||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||||
import useRouteSearchValue from "../../hooks/use-route-search-value";
|
import useRouteSearchValue from "../../hooks/use-route-search-value";
|
||||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
|
||||||
import { subscribeMany } from "../../helpers/relay";
|
import { subscribeMany } from "../../helpers/relay";
|
||||||
import { SEARCH_RELAYS } from "../../const";
|
import { SEARCH_RELAYS, WIKI_RELAYS } from "../../const";
|
||||||
|
import { WIKI_PAGE_KIND } from "../../helpers/nostr/wiki";
|
||||||
|
import { localRelay } from "../../services/local-relay";
|
||||||
|
import { getWebOfTrust } from "../../services/web-of-trust";
|
||||||
|
import WikiPageResult from "./components/wiki-page-result";
|
||||||
|
|
||||||
export default function WikiSearchView() {
|
export default function WikiSearchView() {
|
||||||
const { value: query, setValue: setQuery } = useRouteSearchValue("q");
|
const { value: query, setValue: setQuery } = useRouteSearchValue("q");
|
||||||
@@ -22,9 +26,32 @@ export default function WikiSearchView() {
|
|||||||
|
|
||||||
const [results, setResults] = useState<NostrEvent[]>([]);
|
const [results, setResults] = useState<NostrEvent[]>([]);
|
||||||
|
|
||||||
// useEffect(() => {
|
useEffect(() => {
|
||||||
// const sub = subscribeMany([SEARCH_RELAYS]);
|
setResults([]);
|
||||||
// }, [query]);
|
|
||||||
|
const filter: Filter = { kinds: [WIKI_PAGE_KIND], search: query };
|
||||||
|
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const handleEvent = (event: NostrEvent) => {
|
||||||
|
if (seen.has(getEventUID(event))) return;
|
||||||
|
setResults((arr) => arr.concat(event));
|
||||||
|
seen.add(getEventUID(event));
|
||||||
|
};
|
||||||
|
|
||||||
|
const remoteSearchSub = subscribeMany([...SEARCH_RELAYS, ...WIKI_RELAYS], [filter], {
|
||||||
|
onevent: handleEvent,
|
||||||
|
oneose: () => remoteSearchSub.close(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (localRelay) {
|
||||||
|
const localSearchSub: Subscription = localRelay.subscribe([filter], {
|
||||||
|
onevent: handleEvent,
|
||||||
|
oneose: () => localSearchSub.close(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [query, setResults]);
|
||||||
|
|
||||||
|
const sorted = getWebOfTrust().sortByDistanceAndConnections(results, (p) => p.pubkey);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VerticalPageLayout>
|
<VerticalPageLayout>
|
||||||
@@ -49,6 +76,9 @@ export default function WikiSearchView() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
{sorted.map((page) => (
|
||||||
|
<WikiPageResult key={page.id} page={page} />
|
||||||
|
))}
|
||||||
</VerticalPageLayout>
|
</VerticalPageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user