From 44757c471a623ecdc834f13fed9e33b81bca3118 Mon Sep 17 00:00:00 2001 From: hzrd149 Date: Tue, 22 Oct 2024 10:31:01 +0100 Subject: [PATCH] use applesauce for all content rendering --- src/components/cashu/inline-cashu-card.tsx | 12 +- src/components/compact-note-content.tsx | 52 +++---- .../components}/embed-actions.tsx | 2 +- .../components}/expandable-embed.tsx | 2 +- src/components/content/gallery.tsx | 65 +++++++++ src/components/content/index.tsx | 8 +- .../content/{links.tsx => links-old.tsx} | 0 .../types => content/links}/audio.tsx | 0 .../types => content/links}/code.tsx | 2 +- .../types => content/links}/common.tsx | 0 .../types => content/links}/emoji.tsx | 2 +- .../types => content/links}/image.tsx | 138 +----------------- src/components/content/links/index.ts | 15 ++ .../types => content/links}/lightning.tsx | 2 +- .../types => content/links}/model.tsx | 2 +- .../types => content/links}/music.tsx | 2 +- src/components/content/links/nostr.tsx | 27 ++++ .../types => content/links}/reddit.tsx | 0 .../types => content/links}/simplex.tsx | 0 .../types => content/links}/twitter.tsx | 0 .../types => content/links}/video.tsx | 2 +- .../types => content/links}/wiki.tsx | 0 .../types => content/links}/youtube.tsx | 2 +- .../debug-modal/event-debug-modal.tsx | 5 +- .../event-types/embedded-unknown.tsx | 27 +--- src/components/external-embeds/index.ts | 15 -- .../external-embeds/types/cashu.tsx | 16 -- .../external-embeds/types/nostr.tsx | 119 --------------- .../note/timeline-note/text-note-contents.tsx | 70 ++++----- .../timeline-page/media-timeline/index.tsx | 3 +- src/components/user/user-about.tsx | 23 ++- src/helpers/embeds.ts | 6 + src/helpers/fedimint.ts | 2 +- src/helpers/url.ts | 36 +---- src/services/cashu-mints.ts | 13 +- src/styles.css | 1 + .../components/channel-message-content.tsx | 100 +++---------- .../components/community-description.tsx | 17 ++- .../dms/components/direct-message-content.tsx | 61 +++----- .../components/stream-summary-content.tsx | 36 +---- .../stream-chat/chat-message-content.tsx | 29 +--- src/views/user/about/index.tsx | 22 +-- src/views/user/reactions.tsx | 3 +- src/views/user/zaps.tsx | 17 ++- 44 files changed, 311 insertions(+), 645 deletions(-) rename src/components/{external-embeds => content/components}/embed-actions.tsx (94%) rename src/components/{external-embeds => content/components}/expandable-embed.tsx (95%) create mode 100644 src/components/content/gallery.tsx rename src/components/content/{links.tsx => links-old.tsx} (100%) rename src/components/{external-embeds/types => content/links}/audio.tsx (100%) rename src/components/{external-embeds/types => content/links}/code.tsx (95%) rename src/components/{external-embeds/types => content/links}/common.tsx (100%) rename src/components/{external-embeds/types => content/links}/emoji.tsx (91%) rename src/components/{external-embeds/types => content/links}/image.tsx (56%) create mode 100644 src/components/content/links/index.ts rename src/components/{external-embeds/types => content/links}/lightning.tsx (91%) rename src/components/{external-embeds/types => content/links}/model.tsx (96%) rename src/components/{external-embeds/types => content/links}/music.tsx (99%) create mode 100644 src/components/content/links/nostr.tsx rename src/components/{external-embeds/types => content/links}/reddit.tsx (100%) rename src/components/{external-embeds/types => content/links}/simplex.tsx (100%) rename src/components/{external-embeds/types => content/links}/twitter.tsx (100%) rename src/components/{external-embeds/types => content/links}/video.tsx (95%) rename src/components/{external-embeds/types => content/links}/wiki.tsx (100%) rename src/components/{external-embeds/types => content/links}/youtube.tsx (98%) delete mode 100644 src/components/external-embeds/index.ts delete mode 100644 src/components/external-embeds/types/cashu.tsx delete mode 100644 src/components/external-embeds/types/nostr.tsx diff --git a/src/components/cashu/inline-cashu-card.tsx b/src/components/cashu/inline-cashu-card.tsx index 1652b89ba..a07226d1a 100644 --- a/src/components/cashu/inline-cashu-card.tsx +++ b/src/components/cashu/inline-cashu-card.tsx @@ -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({ - + ) => { - 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 ( - {truncated} - {truncated !== content ? ( + {content} + {truncated.current && ( <> ... Show More - ) : null} + )} ); diff --git a/src/components/external-embeds/embed-actions.tsx b/src/components/content/components/embed-actions.tsx similarity index 94% rename from src/components/external-embeds/embed-actions.tsx rename to src/components/content/components/embed-actions.tsx index 0e45670c1..6d04e8d24 100644 --- a/src/components/external-embeds/embed-actions.tsx +++ b/src/components/content/components/embed-actions.tsx @@ -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({ diff --git a/src/components/external-embeds/expandable-embed.tsx b/src/components/content/components/expandable-embed.tsx similarity index 95% rename from src/components/external-embeds/expandable-embed.tsx rename to src/components/content/components/expandable-embed.tsx index 8e046ab0e..227b6062f 100644 --- a/src/components/external-embeds/expandable-embed.tsx +++ b/src/components/content/components/expandable-embed.tsx @@ -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, diff --git a/src/components/content/gallery.tsx b/src/components/content/gallery.tsx new file mode 100644 index 000000000..982b60153 --- /dev/null +++ b/src/components/content/gallery.tsx @@ -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( + ({ src, event, imageProps, ...props }, ref) => { + const thumbnail = useImageThumbnail(src); + + ref = ref || useRef(null); + const { show } = useRegisterSlide( + ref as MutableRefObject, + src ? { type: "image", src, event } : undefined, + ); + const handleClick = useCallback>( + (e) => { + !e.isPropagationStopped() && show(); + e.preventDefault(); + }, + [show], + ); + + useEffect(() => { + const el = (ref as MutableRefObject).current; + if (el) handleImageFallbacks(el, getPubkeyMediaServers); + }, []); + + return ( + + + + ); + }, +); + +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 ( + + ( + + )} + targetRowHeight={(containerWidth) => containerWidth / rowMultiplier} + /> + + ); +} diff --git a/src/components/content/index.tsx b/src/components/content/index.tsx index d226eef2b..36e34336f 100644 --- a/src/components/content/index.tsx +++ b/src/components/content/index.tsx @@ -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 }) => , emoji: ({ node }) => , + hashtag: ({ node }) => ( + + #{node.name} + + ), }; diff --git a/src/components/content/links.tsx b/src/components/content/links-old.tsx similarity index 100% rename from src/components/content/links.tsx rename to src/components/content/links-old.tsx diff --git a/src/components/external-embeds/types/audio.tsx b/src/components/content/links/audio.tsx similarity index 100% rename from src/components/external-embeds/types/audio.tsx rename to src/components/content/links/audio.tsx diff --git a/src/components/external-embeds/types/code.tsx b/src/components/content/links/code.tsx similarity index 95% rename from src/components/external-embeds/types/code.tsx rename to src/components/content/links/code.tsx index 3b2a8e59a..54cb56edf 100644 --- a/src/components/external-embeds/types/code.tsx +++ b/src/components/content/links/code.tsx @@ -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; diff --git a/src/components/external-embeds/types/common.tsx b/src/components/content/links/common.tsx similarity index 100% rename from src/components/external-embeds/types/common.tsx rename to src/components/content/links/common.tsx diff --git a/src/components/external-embeds/types/emoji.tsx b/src/components/content/links/emoji.tsx similarity index 91% rename from src/components/external-embeds/types/emoji.tsx rename to src/components/content/links/emoji.tsx index 86c57ee37..05f2dc2b8 100644 --- a/src/components/external-embeds/types/emoji.tsx +++ b/src/components/content/links/emoji.tsx @@ -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) { diff --git a/src/components/external-embeds/types/image.tsx b/src/components/content/links/image.tsx similarity index 56% rename from src/components/external-embeds/types/image.tsx rename to src/components/content/links/image.tsx index aad326228..2b26c8356 100644 --- a/src/components/external-embeds/types/image.tsx +++ b/src/components/content/links/image.tsx @@ -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((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( - ({ src, event, imageProps, ...props }, ref) => { - const thumbnail = useImageThumbnail(src); - - ref = ref || useRef(null); - const { show } = useRegisterSlide( - ref as MutableRefObject, - src ? { type: "image", src, event } : undefined, - ); - const handleClick = useCallback>( - (e) => { - !e.isPropagationStopped() && show(); - e.preventDefault(); - }, - [show], - ); - - useEffect(() => { - const el = (ref as MutableRefObject).current; - if (el) handleImageFallbacks(el, getPubkeyMediaServers); - }, []); - - return ( - - - - ); - }, -); - -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 ( - - ( - - )} - targetRowHeight={(containerWidth) => containerWidth / rowMultiplier} - /> - - ); -} - -// 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 = 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); diff --git a/src/components/content/links/index.ts b/src/components/content/links/index.ts new file mode 100644 index 000000000..b594398d1 --- /dev/null +++ b/src/components/content/links/index.ts @@ -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"; diff --git a/src/components/external-embeds/types/lightning.tsx b/src/components/content/links/lightning.tsx similarity index 91% rename from src/components/external-embeds/types/lightning.tsx rename to src/components/content/links/lightning.tsx index bdba90723..90450ab6b 100644 --- a/src/components/external-embeds/types/lightning.tsx +++ b/src/components/content/links/lightning.tsx @@ -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 ( diff --git a/src/components/external-embeds/types/model.tsx b/src/components/content/links/model.tsx similarity index 96% rename from src/components/external-embeds/types/model.tsx rename to src/components/content/links/model.tsx index 16c8f97d9..5ef2188e4 100644 --- a/src/components/external-embeds/types/model.tsx +++ b/src/components/content/links/model.tsx @@ -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")); diff --git a/src/components/external-embeds/types/music.tsx b/src/components/content/links/music.tsx similarity index 99% rename from src/components/external-embeds/types/music.tsx rename to src/components/content/links/music.tsx index 804dd38ff..bd402fbd2 100644 --- a/src/components/external-embeds/types/music.tsx +++ b/src/components/content/links/music.tsx @@ -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" }; diff --git a/src/components/content/links/nostr.tsx b/src/components/content/links/nostr.tsx new file mode 100644 index 000000000..a0437a2d1 --- /dev/null +++ b/src/components/content/links/nostr.tsx @@ -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 ( + + + {match[0]} + + + ); + } + return null; + }, + }); +} diff --git a/src/components/external-embeds/types/reddit.tsx b/src/components/content/links/reddit.tsx similarity index 100% rename from src/components/external-embeds/types/reddit.tsx rename to src/components/content/links/reddit.tsx diff --git a/src/components/external-embeds/types/simplex.tsx b/src/components/content/links/simplex.tsx similarity index 100% rename from src/components/external-embeds/types/simplex.tsx rename to src/components/content/links/simplex.tsx diff --git a/src/components/external-embeds/types/twitter.tsx b/src/components/content/links/twitter.tsx similarity index 100% rename from src/components/external-embeds/types/twitter.tsx rename to src/components/content/links/twitter.tsx diff --git a/src/components/external-embeds/types/video.tsx b/src/components/content/links/video.tsx similarity index 95% rename from src/components/external-embeds/types/video.tsx rename to src/components/content/links/video.tsx index 334e5b944..67b815140 100644 --- a/src/components/external-embeds/types/video.tsx +++ b/src/components/content/links/video.tsx @@ -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` diff --git a/src/components/external-embeds/types/wiki.tsx b/src/components/content/links/wiki.tsx similarity index 100% rename from src/components/external-embeds/types/wiki.tsx rename to src/components/content/links/wiki.tsx diff --git a/src/components/external-embeds/types/youtube.tsx b/src/components/content/links/youtube.tsx similarity index 98% rename from src/components/external-embeds/types/youtube.tsx rename to src/components/content/links/youtube.tsx index 4ab5466c8..9b6100370 100644 --- a/src/components/external-embeds/types/youtube.tsx +++ b/src/components/content/links/youtube.tsx @@ -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 diff --git a/src/components/debug-modal/event-debug-modal.tsx b/src/components/debug-modal/event-debug-modal.tsx index 790d02b25..f4a58bc48 100644 --- a/src/components/debug-modal/event-debug-modal.tsx +++ b/src/components/debug-modal/event-debug-modal.tsx @@ -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 ( @@ -161,7 +162,7 @@ export default function EventDebugModal({ event, ...props }: { event: NostrEvent {nast && (
- +
)} diff --git a/src/components/embed-event/event-types/embedded-unknown.tsx b/src/components/embed-event/event-types/embedded-unknown.tsx index ddb61de4a..0a2f0ffe6 100644 --- a/src/components/embed-event/event-types/embedded-unknown.tsx +++ b/src/components/embed-event/event-types/embedded-unknown.tsx @@ -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 & { 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 ( <> diff --git a/src/components/external-embeds/index.ts b/src/components/external-embeds/index.ts deleted file mode 100644 index 4d1fddbbc..000000000 --- a/src/components/external-embeds/index.ts +++ /dev/null @@ -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"; diff --git a/src/components/external-embeds/types/cashu.tsx b/src/components/external-embeds/types/cashu.tsx deleted file mode 100644 index 4dcce4563..000000000 --- a/src/components/external-embeds/types/cashu.tsx +++ /dev/null @@ -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 ; - }, - name: "emoji", - }); -} diff --git a/src/components/external-embeds/types/nostr.tsx b/src/components/external-embeds/types/nostr.tsx deleted file mode 100644 index f5c93c63a..000000000 --- a/src/components/external-embeds/types/nostr.tsx +++ /dev/null @@ -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 ; - case "nprofile": - return ; - case "note": - case "nevent": - case "naddr": - case "nrelay": - return inline === false ? : 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 ; - } - if (tag[0] === "e" && tag[1]) { - return ( - - ); - } - } - - 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 ( - - #{match[2]} - - ); - } - - 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 ( - - - {match[0]} - - - ); - } - return null; - }, - }); -} diff --git a/src/components/note/timeline-note/text-note-contents.tsx b/src/components/note/timeline-note/text-note-contents.tsx index 29e59ddfa..aad5c842a 100644 --- a/src/components/note/timeline-note/text-note-contents.tsx +++ b/src/components/note/timeline-note/text-note-contents.tsx @@ -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) => { - // 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 ( diff --git a/src/components/timeline-page/media-timeline/index.tsx b/src/components/timeline-page/media-timeline/index.tsx index 09cec7326..412424455 100644 --- a/src/components/timeline-page/media-timeline/index.tsx +++ b/src/components/timeline-page/media-timeline/index.tsx @@ -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(event); diff --git a/src/components/user/user-about.tsx b/src/components/user/user-about.tsx index 3141c5cd2..c029a891b 100644 --- a/src/components/user/user-about.tsx +++ b/src/components/user/user-about.tsx @@ -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) { - 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 ( - {aboutContent} + {content} ); } diff --git a/src/helpers/embeds.ts b/src/helpers/embeds.ts index e023fdb24..34ef4b95a 100644 --- a/src/helpers/embeds.ts +++ b/src/helpers/embeds.ts @@ -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++) { diff --git a/src/helpers/fedimint.ts b/src/helpers/fedimint.ts index 3bbd6ddde..043eccd67 100644 --- a/src/helpers/fedimint.ts +++ b/src/helpers/fedimint.ts @@ -12,7 +12,7 @@ declare module "applesauce-content/nast" { } } -export function FedimintTokensTransformer(): Transformer { +export function fedimintTokens(): Transformer { return (tree) => { findAndReplace(tree, [ [ diff --git a/src/helpers/url.ts b/src/helpers/url.ts index 6fc727123..6f5b6767a 100644 --- a/src/helpers/url.ts +++ b/src/helpers/url.ts @@ -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); diff --git a/src/services/cashu-mints.ts b/src/services/cashu-mints.ts index e2d5b0bb6..d94c23866 100644 --- a/src/services/cashu-mints.ts +++ b/src/services/cashu-mints.ts @@ -1,12 +1,13 @@ -import { CashuMint } from "@cashu/cashu-ts"; +import { CashuMint, CashuWallet } from "@cashu/cashu-ts"; -const mints = new Map(); +const wallets = new Map(); -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)!; } diff --git a/src/styles.css b/src/styles.css index 5f9c9adc9..34c686c71 100644 --- a/src/styles.css +++ b/src/styles.css @@ -4,4 +4,5 @@ body, margin: 0; height: 100%; width: 100%; + overscroll-behavior: none; } diff --git a/src/views/channels/components/channel-message-content.tsx b/src/views/channels/components/channel-message-content.tsx index 656c1fd5d..7e93ce7a7 100644 --- a/src/views/channels/components/channel-message-content.tsx +++ b/src/views/channels/components/channel-message-content.tsx @@ -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 ( diff --git a/src/views/communities/components/community-description.tsx b/src/views/communities/components/community-description.tsx index 0aef767c7..ad1f7a952 100644 --- a/src/views/communities/components/community-description.tsx +++ b/src/views/communities/components/community-description.tsx @@ -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 & { 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 ( <> diff --git a/src/views/dms/components/direct-message-content.tsx b/src/views/dms/components/direct-message-content.tsx index ec86abdbf..8c0ad8835 100644 --- a/src/views/dms/components/direct-message-content.tsx +++ b/src/views/dms/components/direct-message-content.tsx @@ -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 ( diff --git a/src/views/streams/components/stream-summary-content.tsx b/src/views/streams/components/stream-summary-content.tsx index 242f541e7..db4dc8e43 100644 --- a/src/views/streams/components/stream-summary-content.tsx +++ b/src/views/streams/components/stream-summary-content.tsx @@ -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 && ( diff --git a/src/views/streams/stream/stream-chat/chat-message-content.tsx b/src/views/streams/stream/stream-chat/chat-message-content.tsx index a24d056d0..b056842dd 100644 --- a/src/views/streams/stream/stream-chat/chat-message-content.tsx +++ b/src/views/streams/stream/stream-chat/chat-message-content.tsx @@ -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}; }); diff --git a/src/views/user/about/index.tsx b/src/views/user/about/index.tsx index 978500272..98279622f 100644 --- a/src/views/user/about/index.tsx +++ b/src/views/user/about/index.tsx @@ -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}` diff --git a/src/views/user/reactions.tsx b/src/views/user/reactions.tsx index b666d39e6..8ef4f9532 100644 --- a/src/views/user/reactions.tsx +++ b/src/views/user/reactions.tsx @@ -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 }) => { {reaction.content === "+" ? "liked " : "reacted with "} - {embedEmoji([reaction.content], reaction)} + {reaction.content} diff --git a/src/views/user/zaps.tsx b/src/views/user/zaps.tsx index d6e484e2c..dd46e634c 100644 --- a/src/views/user/zaps.tsx +++ b/src/views/user/zaps.tsx @@ -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 = ; } - let embedContent: EmbedableContent = [request.content]; - embedContent = embedNostrLinks(embedContent); - embedContent = embedUrls(embedContent, [renderGenericUrl]); + const content = useRenderedContent(request, components, { linkRenderers }); return ( @@ -69,7 +70,7 @@ const Zap = ({ zapEvent }: { zapEvent: NostrEvent }) => { )} - {embedContent && {embedContent}} + {content && {content}} {eventJSX} );