use applesauce for all content rendering

This commit is contained in:
hzrd149 2024-10-22 10:31:01 +01:00
parent d66ee1e062
commit 44757c471a
44 changed files with 311 additions and 645 deletions

View File

@ -1,18 +1,18 @@
import { useAsync } from "react-use";
import { Box, Button, ButtonGroup, Card, CardProps, Heading, IconButton, Link, Spinner, Text } from "@chakra-ui/react";
import { Token, getEncodedToken } from "@cashu/cashu-ts";
import { Token, getEncodedToken, CashuMint, CashuWallet } from "@cashu/cashu-ts";
import { CopyIconButton } from "../copy-icon-button";
import useUserProfile from "../../hooks/use-user-profile";
import useCurrentAccount from "../../hooks/use-current-account";
import { ECashIcon, WalletIcon } from "../icons";
import { getMint } from "../../services/cashu-mints";
import CurrencyDollar from "../icons/currency-dollar";
import CurrencyEthereum from "../icons/currency-ethereum";
import CurrencyEuro from "../icons/currency-euro";
import CurrencyYen from "../icons/currency-yen";
import CurrencyPound from "../icons/currency-pound";
import CurrencyBitcoin from "../icons/currency-bitcoin";
import { getMintWallet } from "../../services/cashu-mints";
function RedeemButton({ token }: { token: string }) {
const account = useCurrentAccount()!;
@ -40,9 +40,9 @@ export default function InlineCachuCard({
const { value: spendable, loading } = useAsync(async () => {
if (!token) return;
for (const entry of token.token) {
const mint = await getMint(entry.mint);
const spent = await mint.check({ Ys: entry.proofs.map((p) => p.secret) });
if (spent.states.some((v) => v.state === "UNSPENT")) return true;
const wallet = await getMintWallet(entry.mint);
const spent = await wallet.checkProofsSpent(entry.proofs);
if (spent.length !== entry.proofs.length) return true;
}
return false;
}, [token]);
@ -90,7 +90,7 @@ export default function InlineCachuCard({
<Card p="2" flexDirection="column" borderColor="green.500" gap="2" {...props}>
<Box>
<UnitIcon boxSize={10} color={unitColor} float="left" mr="2" mb="1" />
<ButtonGroup float="right">
<ButtonGroup float="right" size="sm">
<CopyIconButton value={encoded} title="Copy Token" aria-label="Copy Token" variant="ghost" />
<IconButton
as={Link}

View File

@ -1,31 +1,15 @@
import React from "react";
import React, { useMemo, useRef } from "react";
import { Box, BoxProps, Text } from "@chakra-ui/react";
import { Root, truncateContent } from "applesauce-content/nast";
import { DraftNostrEvent, NostrEvent } from "../types/nostr-event";
import { EmbedableContent, embedUrls, truncateEmbedableContent } from "../helpers/embeds";
import {
embedNostrLinks,
embedNostrMentions,
embedNostrHashtags,
embedEmoji,
renderGenericUrl,
} from "./external-embeds";
import { LightboxProvider } from "./lightbox-provider";
import { nostrMentions, emojis, hashtags } from "applesauce-content/text";
import { useRenderedContent } from "applesauce-react";
import { components } from "./content";
import { renderGenericUrl } from "./content/links/common";
function buildContents(event: NostrEvent | DraftNostrEvent, textOnly = false) {
let content: EmbedableContent = [event.content.trim().replace(/\n+/g, "\n")];
// common
content = embedUrls(content, [renderGenericUrl]);
// nostr
content = embedNostrLinks(content, textOnly);
content = embedNostrMentions(content, event);
content = embedNostrHashtags(content, event);
content = embedEmoji(content, event);
return content;
}
const linkRenderers = [renderGenericUrl];
export type NoteContentsProps = {
event: NostrEvent | DraftNostrEvent;
@ -35,21 +19,33 @@ export type NoteContentsProps = {
export const CompactNoteContent = React.memo(
({ event, maxLength, textOnly = false, ...props }: NoteContentsProps & Omit<BoxProps, "children">) => {
let content = buildContents(event, textOnly);
let truncated = maxLength !== undefined ? truncateEmbedableContent(content, maxLength) : content;
const truncated = useRef(false);
const transformers = useMemo(
() => [
nostrMentions,
emojis,
hashtags,
() => (tree: Root) => {
const newTree = truncateContent(tree, maxLength);
truncated.current = newTree !== tree;
},
],
[maxLength],
);
const content = useRenderedContent(event, components, { transformers, linkRenderers, maxLength });
return (
<LightboxProvider>
<Box whiteSpace="pre-wrap" {...props}>
{truncated}
{truncated !== content ? (
{content}
{truncated.current && (
<>
<span>...</span>
<Text as="span" fontWeight="bold" ml="4">
Show More
</Text>
</>
) : null}
)}
</Box>
</LightboxProvider>
);

View File

@ -1,5 +1,5 @@
import { Button, ButtonGroup, ButtonGroupProps, Link } from "@chakra-ui/react";
import { ChevronUpIcon, ChevronDownIcon } from "../icons";
import { ChevronUpIcon, ChevronDownIcon } from "../../icons";
import { useCallback, useState } from "react";
export default function EmbedActions({

View File

@ -2,7 +2,7 @@ import { PropsWithChildren, ReactNode } from "react";
import EmbedActions from "./embed-actions";
import { Link, useDisclosure } from "@chakra-ui/react";
import useAppSettings from "../../hooks/use-app-settings";
import useAppSettings from "../../../hooks/use-app-settings";
export default function ExpandableEmbed({
children,

View File

@ -0,0 +1,65 @@
import { forwardRef, MouseEventHandler, MutableRefObject, useCallback, useEffect, useMemo, useRef } from "react";
import { Link } from "@chakra-ui/react";
import { handleImageFallbacks } from "blossom-client-sdk";
import { NostrEvent } from "nostr-tools";
import { EmbeddedImageProps, getPubkeyMediaServers, TrustImage, useImageThumbnail } from "../content/links";
import { useRegisterSlide } from "../lightbox-provider";
import PhotoGallery, { PhotoWithoutSize } from "../photo-gallery";
import { useBreakpointValue } from "../../providers/global/breakpoint-provider";
import ExpandableEmbed from "./components/expandable-embed";
// nevent1qqs8397rp8tt60f3lm8zldt8uqljuqw9axp8z79w0qsmj3r96lmg4tgpz3mhxue69uhhyetvv9ujuerpd46hxtnfduq3zamnwvaz7tmwdaehgun4v5hxxmmd0mkwa9
export const GalleryImage = forwardRef<HTMLImageElement | null, EmbeddedImageProps>(
({ src, event, imageProps, ...props }, ref) => {
const thumbnail = useImageThumbnail(src);
ref = ref || useRef<HTMLImageElement | null>(null);
const { show } = useRegisterSlide(
ref as MutableRefObject<HTMLImageElement | null>,
src ? { type: "image", src, event } : undefined,
);
const handleClick = useCallback<MouseEventHandler<HTMLElement>>(
(e) => {
!e.isPropagationStopped() && show();
e.preventDefault();
},
[show],
);
useEffect(() => {
const el = (ref as MutableRefObject<HTMLImageElement | null>).current;
if (el) handleImageFallbacks(el, getPubkeyMediaServers);
}, []);
return (
<Link href={src} isExternal onClick={handleClick} {...props}>
<TrustImage src={thumbnail} cursor="pointer" ref={ref} onClick={handleClick} w="full" {...imageProps} />
</Link>
);
},
);
export function ImageGallery({ images, event }: { images: string[]; event?: NostrEvent }) {
const photos = useMemo(() => {
return images.map((img) => {
const photo: PhotoWithoutSize = { src: img, key: img };
return photo;
});
}, [images]);
const rowMultiplier = useBreakpointValue({ base: 1.5, sm: 2, md: 3, lg: 4, xl: 5 }) ?? 4;
return (
<ExpandableEmbed label="Image Gallery" hideOnDefaultOpen urls={images}>
<PhotoGallery
layout="rows"
photos={photos}
renderPhoto={({ photo, imageProps, wrapperStyle }) => (
<GalleryImage src={imageProps.src} style={imageProps.style} />
)}
targetRowHeight={(containerWidth) => containerWidth / rowMultiplier}
/>
</ExpandableEmbed>
);
}

View File

@ -1,5 +1,6 @@
import { lazy } from "react";
import { Text } from "@chakra-ui/react";
import { Link, Text } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { ComponentMap } from "applesauce-react";
import Mention from "./mention";
@ -13,4 +14,9 @@ export const components: ComponentMap = {
cashu: Cashu,
fedimint: ({ node }) => <InlineFedimintCard token={node.token} />,
emoji: ({ node }) => <InlineEmoji url={node.url} code={node.code} />,
hashtag: ({ node }) => (
<Link as={RouterLink} to={`/t/${node.hashtag}`} color="blue.500">
#{node.name}
</Link>
),
};

View File

@ -1,5 +1,5 @@
import { Box } from "@chakra-ui/react";
import ExpandableEmbed from "../expandable-embed";
import ExpandableEmbed from "../components/expandable-embed";
export function renderCodePenURL(match: URL) {
if (match.hostname !== "codepen.io") return null;

View File

@ -1,7 +1,7 @@
import { EmbedableContent, embedJSX } from "../../../helpers/embeds";
import { DraftNostrEvent, NostrEvent, isEmojiTag } from "../../../types/nostr-event";
import { getMatchEmoji } from "../../../helpers/regexp";
import { InlineEmoji } from "../../content/ininle-emoji";
import { InlineEmoji } from "../ininle-emoji";
import { getEmojiTag } from "applesauce-core/helpers";
export function embedEmoji(content: EmbedableContent, note: NostrEvent | DraftNostrEvent) {

View File

@ -1,14 +1,4 @@
import {
MouseEvent,
MouseEventHandler,
MutableRefObject,
forwardRef,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { MouseEvent, MouseEventHandler, forwardRef, useCallback, useEffect, useRef, useState } from "react";
import {
Button,
Code,
@ -35,17 +25,13 @@ import {
USER_BLOSSOM_SERVER_LIST_KIND,
} from "blossom-client-sdk";
import { EmbedableContent, defaultGetLocation } from "../../../helpers/embeds";
import { getMatchLink } from "../../../helpers/regexp";
import { useRegisterSlide } from "../../lightbox-provider";
import { isImageURL } from "../../../helpers/url";
import PhotoGallery, { PhotoWithoutSize } from "../../photo-gallery";
import { NostrEvent } from "../../../types/nostr-event";
import useAppSettings from "../../../hooks/use-app-settings";
import { useBreakpointValue } from "../../../providers/global/breakpoint-provider";
import useElementTrustBlur from "../../../hooks/use-element-trust-blur";
import { buildImageProxyURL } from "../../../helpers/image";
import ExpandableEmbed from "../expandable-embed";
import ExpandableEmbed from "../components/expandable-embed";
import { useMediaOwnerContext } from "../../../providers/local/media-owner-provider";
import replaceableEventsService from "../../../services/replaceable-events";
import clientRelaysService from "../../../services/client-relays";
@ -76,7 +62,7 @@ export type EmbeddedImageProps = Omit<LinkProps, "children" | "href" | "onClick"
imageProps?: TrustImageProps;
};
function getPubkeyMediaServers(pubkey?: string) {
export function getPubkeyMediaServers(pubkey?: string) {
if (!pubkey) return;
return new Promise<URL[] | undefined>((res) => {
@ -93,7 +79,7 @@ function getPubkeyMediaServers(pubkey?: string) {
});
}
function useImageThumbnail(src?: string) {
export function useImageThumbnail(src?: string) {
return (src && buildImageProxyURL(src, "512,fit")) ?? src;
}
@ -133,122 +119,6 @@ export function EmbeddedImage({ src, event, imageProps, ...props }: EmbeddedImag
);
}
export const GalleryImage = forwardRef<HTMLImageElement | null, EmbeddedImageProps>(
({ src, event, imageProps, ...props }, ref) => {
const thumbnail = useImageThumbnail(src);
ref = ref || useRef<HTMLImageElement | null>(null);
const { show } = useRegisterSlide(
ref as MutableRefObject<HTMLImageElement | null>,
src ? { type: "image", src, event } : undefined,
);
const handleClick = useCallback<MouseEventHandler<HTMLElement>>(
(e) => {
!e.isPropagationStopped() && show();
e.preventDefault();
},
[show],
);
useEffect(() => {
const el = (ref as MutableRefObject<HTMLImageElement | null>).current;
if (el) handleImageFallbacks(el, getPubkeyMediaServers);
}, []);
return (
<Link href={src} isExternal onClick={handleClick} {...props}>
<TrustImage src={thumbnail} cursor="pointer" ref={ref} onClick={handleClick} w="full" {...imageProps} />
</Link>
);
},
);
export function ImageGallery({ images, event }: { images: string[]; event?: NostrEvent }) {
const photos = useMemo(() => {
return images.map((img) => {
const photo: PhotoWithoutSize = { src: img, key: img };
return photo;
});
}, [images]);
const rowMultiplier = useBreakpointValue({ base: 1.5, sm: 2, md: 3, lg: 4, xl: 5 }) ?? 4;
return (
<ExpandableEmbed label="Image Gallery" hideOnDefaultOpen urls={images}>
<PhotoGallery
layout="rows"
photos={photos}
renderPhoto={({ photo, imageProps, wrapperStyle }) => (
<GalleryImage src={imageProps.src} style={imageProps.style} />
)}
targetRowHeight={(containerWidth) => containerWidth / rowMultiplier}
/>
</ExpandableEmbed>
);
}
// nevent1qqs8397rp8tt60f3lm8zldt8uqljuqw9axp8z79w0qsmj3r96lmg4tgpz3mhxue69uhhyetvv9ujuerpd46hxtnfduq3zamnwvaz7tmwdaehgun4v5hxxmmd0mkwa9
export function embedImageGallery(content: EmbedableContent, event?: NostrEvent): EmbedableContent {
return content
.map((subContent, i) => {
if (typeof subContent === "string") {
const matches = Array.from(subContent.matchAll(getMatchLink()));
const newContent: EmbedableContent = [];
let lastBatchEnd = 0;
let batch: RegExpMatchArray[] = [];
const renderBatch = () => {
if (batch.length > 1) {
// render previous batch
const lastMatchPosition = defaultGetLocation(batch[batch.length - 1]);
const before = subContent.substring(lastBatchEnd, defaultGetLocation(batch[0]).start);
const render = <ImageGallery images={batch.map((m) => m[0])} event={event} />;
newContent.push(before, render);
lastBatchEnd = lastMatchPosition.end;
}
batch = [];
};
for (const match of matches) {
try {
const url = new URL(match[0]);
if (!isImageURL(url)) throw new Error("not an image");
// if this is the first image, add it to the batch
if (batch.length === 0) {
batch = [match];
continue;
}
const last = defaultGetLocation(batch[batch.length - 1]);
const position = defaultGetLocation(match);
const space = subContent.substring(last.end, position.start).trim();
// if there was a non-space between this and the last batch
if (space.length > 0) renderBatch();
batch.push(match);
} catch (e) {
// start a new batch without current match
batch = [];
}
}
renderBatch();
newContent.push(subContent.substring(lastBatchEnd));
return newContent;
}
return subContent;
})
.flat();
}
function VerifyImageButton({ src, original }: { src: URL; original: string }) {
const toast = useToast();
const [loading, setLoading] = useState(false);

View File

@ -0,0 +1,15 @@
export * from "./audio";
export * from "./code";
export * from "./common";
export * from "./emoji";
export * from "./image";
export * from "./lightning";
export * from "./model";
export * from "./music";
export * from "./nostr";
export * from "./reddit";
export * from "./simplex";
export * from "./twitter";
export * from "./video";
export * from "./wiki";
export * from "./youtube";

View File

@ -1,6 +1,6 @@
import { EmbedableContent, embedJSX } from "../../../helpers/embeds";
import { InlineInvoiceCard } from "../../lightning/inline-invoice-card";
import ExpandableEmbed from "../expandable-embed";
import ExpandableEmbed from "../components/expandable-embed";
export function renderLightningInvoice(invoice: string) {
return (

View File

@ -15,7 +15,7 @@ import {
import { Suspense, lazy } from "react";
import { ErrorBoundary } from "../../error-boundary";
import { DownloadIcon, ThingsIcon } from "../../icons";
import ExpandableEmbed from "../expandable-embed";
import ExpandableEmbed from "../components/expandable-embed";
const STLViewer = lazy(() => import("../../stl-viewer"));

View File

@ -2,7 +2,7 @@ import { CSSProperties } from "react";
import { Box, useColorMode } from "@chakra-ui/react";
import { EmbedEventPointer } from "../../embed-event";
import { STEMSTR_RELAY } from "../../../helpers/nostr/stemstr";
import ExpandableEmbed from "../expandable-embed";
import ExpandableEmbed from "../components/expandable-embed";
import useAppSettings from "../../../hooks/use-app-settings";
const setZIndex: CSSProperties = { zIndex: 1, position: "relative" };

View File

@ -0,0 +1,27 @@
import { Link, Tooltip } from "@chakra-ui/react";
import { EmbedableContent, embedJSX } from "../../../helpers/embeds";
import { NIP_NAMES } from "../../../views/relays/components/supported-nips";
export function embedNipDefinitions(content: EmbedableContent) {
return embedJSX(content, {
name: "nip-definition",
regexp: /nip-?(\d\d)/gi,
render: (match) => {
if (NIP_NAMES[match[1]]) {
return (
<Tooltip label={NIP_NAMES[match[1]]} aria-label="NIP Definition">
<Link
isExternal
href={`https://github.com/nostr-protocol/nips/blob/master/${match[1]}.md`}
textDecoration="underline"
>
{match[0]}
</Link>
</Tooltip>
);
}
return null;
},
});
}

View File

@ -4,7 +4,7 @@ import styled from "@emotion/styled";
import { isStreamURL, isVideoURL } from "../../../helpers/url";
import useAppSettings from "../../../hooks/use-app-settings";
import useElementTrustBlur from "../../../hooks/use-element-trust-blur";
import ExpandableEmbed from "../expandable-embed";
import ExpandableEmbed from "../components/expandable-embed";
const LiveVideoPlayer = lazy(() => import("../../live-video-player"));
const StyledVideo = styled.video`

View File

@ -1,5 +1,5 @@
import { AspectRatio } from "@chakra-ui/react";
import ExpandableEmbed from "../expandable-embed";
import ExpandableEmbed from "../components/expandable-embed";
import useAppSettings from "../../../hooks/use-app-settings";
// copied from https://github.com/SimonBrazell/privacy-redirect/blob/master/src/assets/javascripts/helpers/youtube.js

View File

@ -34,6 +34,7 @@ import relayHintService from "../../services/event-relay-hint";
import { usePublishEvent } from "../../providers/global/publish-provider";
import { EditIcon } from "../icons";
import { RelayFavicon } from "../relay-favicon";
import { Root } from "applesauce-content/nast";
function Section({
label,
@ -77,7 +78,7 @@ export default function EventDebugModal({ event, ...props }: { event: NostrEvent
setLoading(false);
}, []);
const nast = Reflect.get(event, ParsedTextContentSymbol);
const nast = Reflect.get(event, ParsedTextContentSymbol) as Root;
return (
<Modal size="6xl" {...props}>
@ -161,7 +162,7 @@ export default function EventDebugModal({ event, ...props }: { event: NostrEvent
</Section>
{nast && (
<Section label="Parsed Content" p="0">
<JsonCode data={nast} />
<JsonCode data={nast.children} />
</Section>
)}
</Accordion>

View File

@ -1,42 +1,29 @@
import { useContext, useMemo } from "react";
import { Box, Button, ButtonGroup, Card, CardBody, CardHeader, CardProps, Text } from "@chakra-ui/react";
import { useRenderedContent } from "applesauce-react";
import { NostrEvent } from "../../../types/nostr-event";
import UserAvatarLink from "../../user/user-avatar-link";
import UserLink from "../../user/user-link";
import UserDnsIdentity from "../../user/user-dns-identity";
import {
embedEmoji,
embedNostrHashtags,
embedNostrLinks,
renderGenericUrl,
renderImageUrl,
renderVideoUrl,
} from "../../external-embeds";
import { EmbedableContent, embedUrls } from "../../../helpers/embeds";
import { renderGenericUrl, renderImageUrl, renderVideoUrl } from "../../content/links";
import Timestamp from "../../timestamp";
import { ExternalLinkIcon } from "../../icons";
import { renderAudioUrl } from "../../external-embeds/types/audio";
import { renderAudioUrl } from "../../content/links/audio";
import DebugEventButton from "../../debug-modal/debug-event-button";
import DebugEventTags from "../../debug-modal/event-tags";
import { AppHandlerContext } from "../../../providers/route/app-handler-provider";
import relayHintService from "../../../services/event-relay-hint";
import { components } from "../../content";
const linkRenderers = [renderImageUrl, renderVideoUrl, renderAudioUrl, renderGenericUrl];
export default function EmbeddedUnknown({ event, ...props }: Omit<CardProps, "children"> & { event: NostrEvent }) {
const address = useMemo(() => relayHintService.getSharableEventAddress(event), [event]);
const { openAddress } = useContext(AppHandlerContext);
const alt = event.tags.find((t) => t[0] === "alt")?.[1];
const content = useMemo(() => {
let jsx: EmbedableContent = [event.content];
jsx = embedNostrLinks(jsx);
jsx = embedNostrHashtags(jsx, event);
jsx = embedEmoji(jsx, event);
jsx = embedUrls(jsx, [renderImageUrl, renderVideoUrl, renderAudioUrl, renderGenericUrl]);
return jsx;
}, [event.content]);
const content = useRenderedContent(event, components, { linkRenderers });
return (
<>

View File

@ -1,15 +0,0 @@
export * from "./types/twitter";
export * from "./types/lightning";
export * from "./types/music";
export * from "./types/common";
export * from "./types/youtube";
export * from "./types/nostr";
export * from "./types/emoji";
export * from "./types/image";
export * from "./types/cashu";
export * from "./types/video";
export * from "./types/simplex";
export * from "./types/reddit";
export * from "./types/model";
export * from "./types/audio";
export * from "./types/code";

View File

@ -1,16 +0,0 @@
import { lazy } from "react";
import { EmbedableContent, embedJSX } from "../../../helpers/embeds";
import { getMatchCashu } from "../../../helpers/regexp";
const InlineCachuCard = lazy(() => import("../../cashu/inline-cashu-card"));
export function embedCashuTokens(content: EmbedableContent) {
return embedJSX(content, {
regexp: getMatchCashu(),
render: (match) => {
// set zIndex and position so link over does not cover card
return <InlineCachuCard token={match[0]} zIndex={1} position="relative" />;
},
name: "emoji",
});
}

View File

@ -1,119 +0,0 @@
import { Link, Tooltip } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { EmbedableContent, embedJSX } from "../../../helpers/embeds";
import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event";
import UserLink from "../../user/user-link";
import { getMatchHashtag, getMatchNostrLink, stripInvisibleChar } from "../../../helpers/regexp";
import { safeDecode } from "../../../helpers/nip19";
import { EmbedEventPointer } from "../../embed-event";
import { NIP_NAMES } from "../../../views/relays/components/supported-nips";
// nostr:nevent1qqsthg2qlxp9l7egtwa92t8lusm7pjknmjwa75ctrrpcjyulr9754fqpz3mhxue69uhhyetvv9ujuerpd46hxtnfduq36amnwvaz7tmwdaehgu3dwp6kytnhv4kxcmmjv3jhytnwv46q2qg5q9
// nostr:nevent1qqsq3wc73lqxd70lg43m5rul57d4mhcanttjat56e30yx5zla48qzlspz9mhxue69uhkummnw3e82efwvdhk6qgdwaehxw309ahx7uewd3hkcq5hsum
export function embedNostrLinks(content: EmbedableContent, inline = false) {
return embedJSX(content, {
name: "nostr-link",
regexp: getMatchNostrLink(),
render: (match) => {
const decoded = safeDecode(match[2]);
if (!decoded) return null;
switch (decoded.type) {
case "npub":
return <UserLink color="blue.500" pubkey={decoded.data} showAt />;
case "nprofile":
return <UserLink color="blue.500" pubkey={decoded.data.pubkey} showAt />;
case "note":
case "nevent":
case "naddr":
case "nrelay":
return inline === false ? <EmbedEventPointer pointer={decoded} /> : null;
default:
return null;
}
},
});
}
/** @deprecated */
export function embedNostrMentions(content: EmbedableContent, event: NostrEvent | DraftNostrEvent) {
return embedJSX(content, {
name: "nostr-mention",
regexp: /#\[(\d+)\]/g,
render: (match) => {
const index = parseInt(match[1]);
const tag = event?.tags[index];
if (tag) {
if (tag[0] === "p" && tag[1]) {
return <UserLink color="blue.500" pubkey={tag[1]} showAt />;
}
if (tag[0] === "e" && tag[1]) {
return (
<EmbedEventPointer
pointer={{ type: "nevent", data: { id: tag[1], relays: tag[2] ? [tag[2]] : undefined } }}
/>
);
}
}
return null;
},
});
}
export function embedNostrHashtags(content: EmbedableContent, event: NostrEvent | DraftNostrEvent) {
const hashtags = event.tags
.filter((t) => t[0] === "t" && t[1])
.map((t) => t[1]?.toLowerCase())
.map(stripInvisibleChar);
return embedJSX(content, {
name: "nostr-hashtag",
regexp: getMatchHashtag(),
getLocation: (match) => {
if (match.index === undefined) throw new Error("match does not have index");
const start = match.index + match[1].length;
const end = start + 1 + match[2].length;
return { start, end };
},
render: (match) => {
const hashtag = match[2].toLowerCase();
if (hashtags.includes(hashtag)) {
return (
<Link as={RouterLink} to={`/t/${hashtag}`} color="blue.500">
#{match[2]}
</Link>
);
}
return null;
},
});
}
export function embedNipDefinitions(content: EmbedableContent) {
return embedJSX(content, {
name: "nip-definition",
regexp: /nip-?(\d\d)/gi,
render: (match) => {
if (NIP_NAMES[match[1]]) {
return (
<Tooltip label={NIP_NAMES[match[1]]} aria-label="NIP Definition">
<Link
isExternal
href={`https://github.com/nostr-protocol/nips/blob/master/${match[1]}.md`}
textDecoration="underline"
>
{match[0]}
</Link>
</Tooltip>
);
}
return null;
},
});
}

View File

@ -2,6 +2,7 @@ import React, { Suspense, useMemo } from "react";
import { Box, BoxProps, Spinner } from "@chakra-ui/react";
import { EventTemplate, NostrEvent } from "nostr-tools";
import { useRenderedContent } from "applesauce-react/hooks";
import { defaultTransformers } from "applesauce-content/text";
import {
renderWavlakeUrl,
@ -23,14 +24,13 @@ import {
renderCodePenURL,
renderArchiveOrgURL,
renderStreamUrl,
} from "../../external-embeds";
} from "../../content/links";
import { LightboxProvider } from "../../lightbox-provider";
import MediaOwnerProvider from "../../../providers/local/media-owner-provider";
import buildLinkComponent from "../../content/links";
import { components } from "../../content";
import { FedimintTokensTransformer } from "../../../helpers/fedimint";
import { fedimintTokens } from "../../../helpers/fedimint";
const transformers = [FedimintTokensTransformer];
const transformers = [...defaultTransformers, fedimintTokens];
export type TextNoteContentsProps = {
event: NostrEvent | EventTemplate;
@ -38,47 +38,31 @@ export type TextNoteContentsProps = {
maxLength?: number;
};
const linkRenderers = [
renderSimpleXLink,
renderYoutubeURL,
renderTwitterUrl,
renderRedditUrl,
renderWavlakeUrl,
renderAppleMusicUrl,
renderSpotifyUrl,
renderTidalUrl,
renderSongDotLinkUrl,
renderStemstrUrl,
renderSoundCloudUrl,
renderImageUrl,
renderVideoUrl,
renderStreamUrl,
renderAudioUrl,
renderModelUrl,
renderCodePenURL,
renderArchiveOrgURL,
renderOpenGraphUrl,
];
export const TextNoteContents = React.memo(
({ event, noOpenGraphLinks, maxLength, ...props }: TextNoteContentsProps & Omit<BoxProps, "children">) => {
// let content = buildContents(event, noOpenGraphLinks);
// if (maxLength !== undefined) {
// content = truncateEmbedableContent(content, maxLength);
// }
const LinkComponent = useMemo(
() =>
buildLinkComponent([
renderSimpleXLink,
renderYoutubeURL,
renderTwitterUrl,
renderRedditUrl,
renderWavlakeUrl,
renderAppleMusicUrl,
renderSpotifyUrl,
renderTidalUrl,
renderSongDotLinkUrl,
renderStemstrUrl,
renderSoundCloudUrl,
renderImageUrl,
renderVideoUrl,
renderStreamUrl,
renderAudioUrl,
renderModelUrl,
renderCodePenURL,
renderArchiveOrgURL,
renderOpenGraphUrl,
]),
[],
);
const componentsMap = useMemo(
() => ({
...components,
link: LinkComponent,
}),
[LinkComponent],
);
const content = useRenderedContent(event, componentsMap, { transformers });
const content = useRenderedContent(event, components, { linkRenderers, transformers, maxLength });
return (
<MediaOwnerProvider owner={(event as NostrEvent).pubkey as string | undefined}>

View File

@ -5,12 +5,13 @@ import { Photo } from "react-photo-album";
import { getMatchLink } from "../../../helpers/regexp";
import { LightboxProvider } from "../../lightbox-provider";
import { isImageURL } from "../../../helpers/url";
import { EmbeddedImageProps, GalleryImage } from "../../external-embeds";
import { EmbeddedImageProps } from "../../content/links";
import { TrustProvider } from "../../../providers/local/trust-provider";
import PhotoGallery, { PhotoWithoutSize } from "../../photo-gallery";
import { NostrEvent } from "../../../types/nostr-event";
import { useBreakpointValue } from "../../../providers/global/breakpoint-provider";
import useEventIntersectionRef from "../../../hooks/use-event-intersection-ref";
import { GalleryImage } from "../../content/gallery";
function CustomGalleryImage({ event, ...props }: EmbeddedImageProps & { event: NostrEvent }) {
const ref = useEventIntersectionRef<HTMLImageElement>(event);

View File

@ -1,24 +1,21 @@
import { useMemo } from "react";
import { Box, BoxProps } from "@chakra-ui/react";
import { useRenderedContent } from "applesauce-react";
import { nostrMentions } from "applesauce-content/text";
import { EmbedableContent, embedUrls } from "../../helpers/embeds";
import useUserProfile from "../../hooks/use-user-profile";
import { embedNostrLinks, renderGenericUrl } from "../external-embeds";
import { renderGenericUrl } from "../content/links";
import { components } from "../content";
const transformers = [nostrMentions];
const linkRenderers = [renderGenericUrl];
export default function UserAbout({ pubkey, ...props }: { pubkey: string } & Omit<BoxProps, "children">) {
const metadata = useUserProfile(pubkey);
const aboutContent = useMemo(() => {
if (!metadata?.about) return null;
let content: EmbedableContent = [metadata.about.trim()];
content = embedNostrLinks(content);
content = embedUrls(content, [renderGenericUrl]);
return content;
}, [metadata?.about]);
const profile = useUserProfile(pubkey);
const content = useRenderedContent(profile?.about, components, { transformers, linkRenderers });
return (
<Box whiteSpace="pre-line" {...props}>
{aboutContent}
{content}
</Box>
);
}

View File

@ -1,7 +1,9 @@
import { cloneElement } from "react";
import { getMatchLink } from "./regexp";
/** @deprecated */
export type EmbedableContent = (string | JSX.Element)[];
/** @deprecated */
export type EmbedType = {
regexp: RegExp;
render: (match: RegExpMatchArray, isEndOfLine: boolean) => JSX.Element | string | null;
@ -17,6 +19,7 @@ export function defaultGetLocation(match: RegExpMatchArray) {
};
}
/** @deprecated */
export function embedJSX(content: EmbedableContent, embed: EmbedType): EmbedableContent {
return content
.map((subContent, i) => {
@ -69,8 +72,10 @@ export function embedJSX(content: EmbedableContent, embed: EmbedType): Embedable
.flat();
}
/** @deprecated */
export type LinkEmbedHandler = (link: URL, isEndOfLine: boolean) => JSX.Element | string | null;
/** @deprecated */
export function embedUrls(content: EmbedableContent, handlers: LinkEmbedHandler[]) {
return embedJSX(content, {
name: "embedUrls",
@ -94,6 +99,7 @@ export function embedUrls(content: EmbedableContent, handlers: LinkEmbedHandler[
});
}
/** @deprecated */
export function truncateEmbedableContent(content: EmbedableContent, maxLength = 256) {
let length = 0;
for (let i = 0; i < content.length; i++) {

View File

@ -12,7 +12,7 @@ declare module "applesauce-content/nast" {
}
}
export function FedimintTokensTransformer(): Transformer<Root> {
export function fedimintTokens(): Transformer<Root> {
return (tree) => {
findAndReplace(tree, [
[

View File

@ -1,37 +1,5 @@
export const convertToUrl = (url: string | URL) => (url instanceof URL ? url : new URL(url));
export const IMAGE_EXT = [".svg", ".gif", ".png", ".jpg", ".jpeg", ".webp", ".avif"];
export const VIDEO_EXT = [".mp4", ".mkv", ".webm", ".mov"];
export const STREAM_EXT = [".m3u8"];
export const AUDIO_EXT = [".mp3", ".wav", ".ogg", ".aac"];
export function isMediaURL(url: string | URL) {
return isImageURL(url) || isVideoURL(url);
}
export function isImageURL(url: string | URL) {
const u = new URL(url);
const ipfsFilename = u.searchParams.get("filename");
return IMAGE_EXT.some((ext) => u.pathname.toLowerCase().endsWith(ext) || ipfsFilename?.toLowerCase().endsWith(ext));
}
export function isVideoURL(url: string | URL) {
const u = new URL(url);
const ipfsFilename = u.searchParams.get("filename");
return VIDEO_EXT.some((ext) => u.pathname.toLowerCase().endsWith(ext) || ipfsFilename?.toLowerCase().endsWith(ext));
}
export function isStreamURL(url: string | URL) {
const u = new URL(url);
const ipfsFilename = u.searchParams.get("filename");
return STREAM_EXT.some((ext) => u.pathname.toLowerCase().endsWith(ext) || ipfsFilename?.toLowerCase().endsWith(ext));
}
export function isAudioURL(url: string | URL) {
const u = new URL(url);
const ipfsFilename = u.searchParams.get("filename");
return AUDIO_EXT.some((ext) => u.pathname.toLowerCase().endsWith(ext) || ipfsFilename?.toLowerCase().endsWith(ext));
}
import { convertToUrl } from "applesauce-core/helpers/url";
export * from "applesauce-core/helpers/url";
export function replaceDomain(url: string | URL, replacementUrl: string | URL) {
const newUrl = new URL(url);

View File

@ -1,12 +1,13 @@
import { CashuMint } from "@cashu/cashu-ts";
import { CashuMint, CashuWallet } from "@cashu/cashu-ts";
const mints = new Map<string, CashuMint>();
const wallets = new Map<string, CashuWallet>();
export async function getMint(url: string) {
export async function getMintWallet(url: string) {
const formatted = new URL(url).toString();
if (!mints.has(formatted)) {
if (!wallets.has(formatted)) {
const mint = new CashuMint(formatted);
mints.set(formatted, mint);
const wallet = new CashuWallet(mint);
wallets.set(formatted, wallet);
}
return mints.get(formatted)!;
return wallets.get(formatted)!;
}

View File

@ -4,4 +4,5 @@ body,
margin: 0;
height: 100%;
width: 100%;
overscroll-behavior: none;
}

View File

@ -1,4 +1,4 @@
import { memo, useMemo } from "react";
import { memo } from "react";
import { Box, BoxProps } from "@chakra-ui/react";
import { useRenderedContent } from "applesauce-react";
@ -20,86 +20,32 @@ import {
renderVideoUrl,
renderWavlakeUrl,
renderYoutubeURL,
} from "../../../components/external-embeds";
} from "../../../components/content/links";
import { LightboxProvider } from "../../../components/lightbox-provider";
import { renderAudioUrl } from "../../../components/external-embeds/types/audio";
import buildLinkComponent from "../../../components/content/links";
import { renderAudioUrl } from "../../../components/content/links/audio";
import { components } from "../../../components/content";
const linkRenderers = [
renderSimpleXLink,
renderYoutubeURL,
renderTwitterUrl,
renderRedditUrl,
renderWavlakeUrl,
renderAppleMusicUrl,
renderSpotifyUrl,
renderTidalUrl,
renderSongDotLinkUrl,
renderStemstrUrl,
renderSoundCloudUrl,
renderImageUrl,
renderVideoUrl,
renderStreamUrl,
renderAudioUrl,
renderGenericUrl,
];
const ChannelMessageContent = memo(({ message, children, ...props }: BoxProps & { message: NostrEvent }) => {
// const content = useMemo(() => {
// let c: EmbedableContent = [message.content];
// // image gallery
// c = embedImageGallery(c, message);
// // common
// c = embedUrls(c, [
// renderSimpleXLink,
// renderYoutubeURL,
// renderTwitterUrl,
// renderRedditUrl,
// renderWavlakeUrl,
// renderAppleMusicUrl,
// renderSpotifyUrl,
// renderTidalUrl,
// renderSongDotLinkUrl,
// renderStemstrUrl,
// renderSoundCloudUrl,
// renderImageUrl,
// renderVideoUrl,
// renderStreamUrl,
// renderAudioUrl,
// renderGenericUrl,
// ]);
// // bitcoin
// c = embedLightningInvoice(c);
// // cashu
// c = embedCashuTokens(c);
// // nostr
// c = embedNostrLinks(c);
// c = embedNostrMentions(c, message);
// c = embedNostrHashtags(c, message);
// c = embedNipDefinitions(c);
// c = embedEmoji(c, message);
// return c;
// }, [message.content]);
const LinkComponent = useMemo(
() =>
buildLinkComponent([
renderSimpleXLink,
renderYoutubeURL,
renderTwitterUrl,
renderRedditUrl,
renderWavlakeUrl,
renderAppleMusicUrl,
renderSpotifyUrl,
renderTidalUrl,
renderSongDotLinkUrl,
renderStemstrUrl,
renderSoundCloudUrl,
renderImageUrl,
renderVideoUrl,
renderStreamUrl,
renderAudioUrl,
renderGenericUrl,
]),
[],
);
const componentsMap = useMemo(
() => ({
...components,
link: LinkComponent,
}),
[LinkComponent],
);
const content = useRenderedContent(message, componentsMap);
const content = useRenderedContent(message, components, { linkRenderers });
return (
<TrustProvider event={message}>

View File

@ -1,10 +1,13 @@
import { useState } from "react";
import { Box, BoxProps, Button } from "@chakra-ui/react";
import { useRenderedContent } from "applesauce-react";
import { NostrEvent } from "../../../types/nostr-event";
import { getCommunityDescription } from "../../../helpers/nostr/communities";
import { EmbedableContent, embedUrls, truncateEmbedableContent } from "../../../helpers/embeds";
import { renderGenericUrl } from "../../../components/external-embeds";
import { components } from "../../../components/content";
import { renderGenericUrl } from "../../../components/content/links";
const linkRenderers = [renderGenericUrl];
export default function CommunityDescription({
community,
@ -13,13 +16,11 @@ export default function CommunityDescription({
...props
}: Omit<BoxProps, "children"> & { community: NostrEvent; maxLength?: number; showExpand?: boolean }) {
const description = getCommunityDescription(community);
let content: EmbedableContent = description ? [description] : [];
const [showAll, setShowAll] = useState(false);
content = embedUrls(content, [renderGenericUrl]);
if (maxLength !== undefined && !showAll) {
content = truncateEmbedableContent(content, maxLength);
}
const content = useRenderedContent(description, components, {
maxLength: showAll ? undefined : maxLength,
linkRenderers,
});
return (
<>

View File

@ -1,6 +1,6 @@
import { useMemo } from "react";
import { Box, BoxProps } from "@chakra-ui/react";
import { useRenderedContent } from "applesauce-react";
import { defaultTransformers } from "applesauce-content/text";
import { NostrEvent } from "../../../types/nostr-event";
import {
@ -19,16 +19,33 @@ import {
renderVideoUrl,
renderWavlakeUrl,
renderYoutubeURL,
} from "../../../components/external-embeds";
} from "../../../components/content/links";
import { TrustProvider } from "../../../providers/local/trust-provider";
import { LightboxProvider } from "../../../components/lightbox-provider";
import { renderAudioUrl } from "../../../components/external-embeds/types/audio";
import buildLinkComponent from "../../../components/content/links";
import { renderAudioUrl } from "../../../components/content/links/audio";
import { components } from "../../../components/content";
import { useKind4Decrypt } from "../../../hooks/use-kind4-decryption";
import { FedimintTokensTransformer } from "../../../helpers/fedimint";
import { fedimintTokens } from "../../../helpers/fedimint";
const transformers = [FedimintTokensTransformer];
const transformers = [...defaultTransformers, fedimintTokens];
const linkRenderers = [
renderSimpleXLink,
renderYoutubeURL,
renderTwitterUrl,
renderRedditUrl,
renderWavlakeUrl,
renderAppleMusicUrl,
renderSpotifyUrl,
renderTidalUrl,
renderSongDotLinkUrl,
renderStemstrUrl,
renderSoundCloudUrl,
renderImageUrl,
renderVideoUrl,
renderStreamUrl,
renderAudioUrl,
renderGenericUrl,
];
export default function DirectMessageContent({
event,
@ -36,38 +53,8 @@ export default function DirectMessageContent({
children,
...props
}: { event: NostrEvent; text: string } & BoxProps) {
const LinkComponent = useMemo(
() =>
buildLinkComponent([
renderSimpleXLink,
renderYoutubeURL,
renderTwitterUrl,
renderRedditUrl,
renderWavlakeUrl,
renderAppleMusicUrl,
renderSpotifyUrl,
renderTidalUrl,
renderSongDotLinkUrl,
renderStemstrUrl,
renderSoundCloudUrl,
renderImageUrl,
renderVideoUrl,
renderStreamUrl,
renderAudioUrl,
renderGenericUrl,
]),
[],
);
const componentsMap = useMemo(
() => ({
...components,
link: LinkComponent,
}),
[LinkComponent],
);
const { plaintext } = useKind4Decrypt(event);
const content = useRenderedContent(event, componentsMap, { overrideContent: plaintext, transformers });
const content = useRenderedContent(plaintext, components, { transformers, linkRenderers });
return (
<TrustProvider event={event}>

View File

@ -1,34 +1,14 @@
import { useMemo } from "react";
import { ParsedStream } from "../../../helpers/nostr/stream";
import { EmbedableContent, embedUrls } from "../../../helpers/embeds";
import {
embedEmoji,
embedNipDefinitions,
embedNostrHashtags,
embedNostrLinks,
embedNostrMentions,
renderGenericUrl,
renderImageUrl,
} from "../../../components/external-embeds";
import { Box, BoxProps } from "@chakra-ui/react";
import { useRenderedContent } from "applesauce-react";
import { ParsedStream } from "../../../helpers/nostr/stream";
import { renderGenericUrl, renderImageUrl } from "../../../components/content/links";
import { components } from "../../../components/content";
const linkRenderers = [renderImageUrl, renderGenericUrl];
export default function StreamSummaryContent({ stream, ...props }: BoxProps & { stream: ParsedStream }) {
const content = useMemo(() => {
if (!stream.summary) return null;
let c: EmbedableContent = [stream.summary];
// general
c = embedUrls(c, [renderImageUrl, renderGenericUrl]);
// nostr
c = embedNostrLinks(c);
c = embedNostrMentions(c, stream.event);
c = embedNostrHashtags(c, stream.event);
c = embedNipDefinitions(c);
c = embedEmoji(c, stream.event);
return c;
}, [stream.summary]);
const content = useRenderedContent(stream.event, components, { linkRenderers });
return (
content && (

View File

@ -1,35 +1,20 @@
import React, { useMemo } from "react";
import React from "react";
import { EmbedableContent, embedUrls } from "../../../../helpers/embeds";
import {
embedEmoji,
embedNipDefinitions,
embedNostrHashtags,
embedNostrLinks,
embedNostrMentions,
renderGenericUrl,
renderImageUrl,
renderSoundCloudUrl,
renderStemstrUrl,
renderWavlakeUrl,
} from "../../../../components/external-embeds";
} from "../../../../components/content/links";
import { NostrEvent } from "../../../../types/nostr-event";
import { useRenderedContent } from "applesauce-react";
import { components } from "../../../../components/content";
const linkRenderers = [renderImageUrl, renderWavlakeUrl, renderStemstrUrl, renderSoundCloudUrl, renderGenericUrl];
const ChatMessageContent = React.memo(({ event }: { event: NostrEvent }) => {
const content = useMemo(() => {
let c: EmbedableContent = [event.content];
c = embedUrls(c, [renderImageUrl, renderWavlakeUrl, renderStemstrUrl, renderSoundCloudUrl, renderGenericUrl]);
// nostr
c = embedNostrLinks(c);
c = embedNostrMentions(c, event);
c = embedNostrHashtags(c, event);
c = embedNipDefinitions(c);
c = embedEmoji(c, event);
return c;
}, [event.content]);
const content = useRenderedContent(event, components, { linkRenderers });
return <>{content}</>;
});

View File

@ -18,18 +18,17 @@ import {
useDisclosure,
} from "@chakra-ui/react";
import { nip19 } from "nostr-tools";
import { useRenderedContent } from "applesauce-react";
import { ChatIcon } from "@chakra-ui/icons";
import { getLudEndpoint } from "../../../helpers/lnurl";
import { EmbedableContent, embedUrls } from "../../../helpers/embeds";
import { truncatedId } from "../../../helpers/nostr/event";
import { parseAddress } from "../../../services/dns-identity";
import { useAdditionalRelayContext } from "../../../providers/local/additional-relay-context";
import useUserProfile from "../../../hooks/use-user-profile";
import { embedNostrLinks, renderGenericUrl } from "../../../components/external-embeds";
import {
ChevronDownIcon,
ChevronUpIcon,
AtIcon,
ExternalLinkIcon,
KeyIcon,
LightningIcon,
@ -39,7 +38,6 @@ import { CopyIconButton } from "../../../components/copy-icon-button";
import { QrIconButton } from "../components/share-qr-button";
import UserDnsIdentity from "../../../components/user/user-dns-identity";
import UserAvatar from "../../../components/user/user-avatar";
import { ChatIcon } from "@chakra-ui/icons";
import { UserFollowButton } from "../../../components/user/user-follow-button";
import UserZapButton from "../components/user-zap-button";
import { UserProfileMenu } from "../components/user-profile-menu";
@ -52,16 +50,8 @@ import UserJoinedChanneled from "./user-joined-channels";
import { getTextColor } from "../../../helpers/color";
import UserName from "../../../components/user/user-name";
import { useUserDNSIdentity } from "../../../hooks/use-user-dns-identity";
import UserDnsIdentityIcon from "../../../components/user/user-dns-identity-icon";
function buildDescriptionContent(description: string) {
let content: EmbedableContent = [description.trim()];
content = embedNostrLinks(content);
content = embedUrls(content, [renderGenericUrl]);
return content;
}
import { components } from "../../../components/content";
import { renderGenericUrl } from "../../../components/content/links/common";
function DNSIdentityWarning({ pubkey }: { pubkey: string }) {
const metadata = useUserProfile(pubkey);
@ -99,6 +89,8 @@ function DNSIdentityWarning({ pubkey }: { pubkey: string }) {
);
}
const linkRenderers = [renderGenericUrl];
export default function UserAboutTab() {
const expanded = useDisclosure();
const { pubkey } = useOutletContext() as { pubkey: string };
@ -110,7 +102,7 @@ export default function UserAboutTab() {
const nprofile = useSharableProfileId(pubkey);
const pubkeyColor = "#" + pubkey.slice(0, 6);
const aboutContent = metadata?.about && buildDescriptionContent(metadata?.about);
const aboutContent = useRenderedContent(metadata?.about, components, { linkRenderers });
const parsedNip05 = metadata?.nip05 ? parseAddress(metadata.nip05) : undefined;
const nip05URL = parsedNip05
? `https://${parsedNip05.domain}/.well-known/nostr.json?name=${parsedNip05.name}`

View File

@ -13,7 +13,6 @@ import { TrustProvider } from "../../providers/local/trust-provider";
import UserAvatar from "../../components/user/user-avatar";
import UserLink from "../../components/user/user-link";
import { EmbedEventPointer } from "../../components/embed-event";
import { embedEmoji } from "../../components/external-embeds";
import VerticalPageLayout from "../../components/vertical-page-layout";
import NoteMenu from "../../components/note/note-menu";
import useEventIntersectionRef from "../../hooks/use-event-intersection-ref";
@ -32,7 +31,7 @@ const Reaction = ({ reaction: reaction }: { reaction: NostrEvent }) => {
<UserAvatar pubkey={reaction.pubkey} size="xs" />
<Text>
<UserLink pubkey={reaction.pubkey} /> {reaction.content === "+" ? "liked " : "reacted with "}
{embedEmoji([reaction.content], reaction)}
{reaction.content}
</Text>
<Spacer />
<NoteMenu event={reaction} aria-label="Note menu" variant="ghost" size="xs" />

View File

@ -1,7 +1,8 @@
import { ReactNode, useCallback, useMemo, useState } from "react";
import { Box, Flex, Select, Text } from "@chakra-ui/react";
import dayjs from "dayjs";
import { useOutletContext } from "react-router-dom";
import { Box, Flex, Select, Text } from "@chakra-ui/react";
import { useRenderedContent } from "applesauce-react";
import dayjs from "dayjs";
import { ErrorBoundary } from "../../components/error-boundary";
import { LightningIcon } from "../../components/icons";
@ -16,13 +17,15 @@ import { useReadRelays } from "../../hooks/use-client-relays";
import TimelineActionAndStatus from "../../components/timeline/timeline-action-and-status";
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import { EmbedableContent, embedUrls } from "../../helpers/embeds";
import { embedNostrLinks, renderGenericUrl } from "../../components/external-embeds";
import Timestamp from "../../components/timestamp";
import { EmbedEventPointer } from "../../components/embed-event";
import { parseCoordinate } from "../../helpers/nostr/event";
import VerticalPageLayout from "../../components/vertical-page-layout";
import useEventIntersectionRef from "../../hooks/use-event-intersection-ref";
import { components } from "../../components/content";
import { renderGenericUrl } from "../../components/content/links/common";
const linkRenderers = [renderGenericUrl];
const Zap = ({ zapEvent }: { zapEvent: NostrEvent }) => {
const ref = useEventIntersectionRef(zapEvent);
@ -51,9 +54,7 @@ const Zap = ({ zapEvent }: { zapEvent: NostrEvent }) => {
eventJSX = <EmbedEventPointer pointer={{ type: "note", data: eventId }} />;
}
let embedContent: EmbedableContent = [request.content];
embedContent = embedNostrLinks(embedContent);
embedContent = embedUrls(embedContent, [renderGenericUrl]);
const content = useRenderedContent(request, components, { linkRenderers });
return (
<Box ref={ref}>
@ -69,7 +70,7 @@ const Zap = ({ zapEvent }: { zapEvent: NostrEvent }) => {
)}
<Timestamp ml="auto" timestamp={request.created_at} />
</Flex>
{embedContent && <Box>{embedContent}</Box>}
{content && <Box whiteSpace="pre">{content}</Box>}
{eventJSX}
</Box>
);