From 1282aee6f4a2160b439aefba52cd57bf0b1a6342 Mon Sep 17 00:00:00 2001 From: hzrd149 Date: Mon, 14 Oct 2024 17:46:52 +0100 Subject: [PATCH] use applesauce-content for rendering notes --- package.json | 1 + pnpm-lock.yaml | 30 ++++- src/components/content/index.tsx | 9 ++ src/components/content/links.tsx | 25 ++++ src/components/content/mention.tsx | 23 ++++ .../debug-modal/event-debug-modal.tsx | 9 ++ .../external-embeds/types/common.tsx | 5 +- .../external-embeds/types/twitter.tsx | 14 ++- .../note/timeline-note/text-note-contents.tsx | 115 +++++++++--------- src/hooks/use-user-profile.ts | 7 +- .../components/channel-message-content.tsx | 114 ++++++++++------- .../dms/components/direct-message-content.tsx | 61 ++++++---- 12 files changed, 270 insertions(+), 143 deletions(-) create mode 100644 src/components/content/index.tsx create mode 100644 src/components/content/links.tsx create mode 100644 src/components/content/mention.tsx diff --git a/package.json b/package.json index 82a4fed9b..9f68cc638 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@uiw/react-codemirror": "^4.23.0", "@webscopeio/react-textarea-autocomplete": "^4.9.2", "applesauce-channel": "^0.6.0", + "applesauce-content": "link:../applesauce/packages/content", "applesauce-core": "^0.6.0", "applesauce-react": "^0.6.0", "applesauce-signer": "^0.6.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 91c0ad20e..853386afa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,6 +92,9 @@ importers: applesauce-channel: specifier: ^0.6.0 version: 0.6.0(typescript@5.6.2) + applesauce-content: + specifier: link:../applesauce/packages/content + version: link:../applesauce/packages/content applesauce-core: specifier: ^0.6.0 version: link:../applesauce/packages/core @@ -100,7 +103,7 @@ importers: version: link:../applesauce/packages/react applesauce-signer: specifier: ^0.6.0 - version: link:../applesauce/packages/signer + version: 0.6.0(typescript@5.6.2) bech32: specifier: ^2.0.0 version: 2.0.0 @@ -2703,6 +2706,10 @@ packages: resolution: { integrity: sha512-23W/7P0hzjVGIp51Yp4ppJgbDFNtrJrF3HO3/M36/z+Msdb3HKiSXmt3bMxEVMY6nJTT+zWneq7mAd7VvwhWaA== } + applesauce-signer@0.6.0: + resolution: + { integrity: sha512-BunnObvSqIBJ04MMQnpXIflfYEAwqWROCJqQrFZXHy+yXmZjHzPVMkXQLcJ7oXoNVc4CaxgJwp7w8eSmohJKFg== } + argparse@1.0.10: resolution: { integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== } @@ -3670,9 +3677,9 @@ packages: { integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== } engines: { node: ">= 0.4" } - hast-util-to-jsx-runtime@2.3.0: + hast-util-to-jsx-runtime@2.3.1: resolution: - { integrity: sha512-H/y0+IWPdsLLS738P8tDnrQ8Z+dj12zQQ6WC11TIM21C8WFVoIxcqWXf2H3hiTVZjF1AWqoimGwrTWecWrnmRQ== } + { integrity: sha512-Rbemi1rzrkysSin0FDHZfsxYPoqLGHFfxFm28aOBHPibT7aqjy7kUgY636se9xbuCWUsFpWAYlmtGHQakiqtEA== } hast-util-whitespace@3.0.0: resolution: @@ -8293,6 +8300,19 @@ snapshots: - supports-color - typescript + applesauce-signer@0.6.0(typescript@5.6.2): + dependencies: + "@noble/hashes": 1.5.0 + "@noble/secp256k1": 1.7.1 + "@scure/base": 1.1.9 + "@types/dom-serial": 1.0.6 + applesauce-core: 0.6.0(typescript@5.6.2) + debug: 4.3.7 + nostr-tools: 2.7.2(typescript@5.6.2) + transitivePeerDependencies: + - supports-color + - typescript + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -9177,7 +9197,7 @@ snapshots: dependencies: function-bind: 1.1.2 - hast-util-to-jsx-runtime@2.3.0: + hast-util-to-jsx-runtime@2.3.1: dependencies: "@types/estree": 1.0.6 "@types/hast": 3.0.4 @@ -10263,7 +10283,7 @@ snapshots: "@types/hast": 3.0.4 "@types/react": 18.3.10 devlop: 1.1.0 - hast-util-to-jsx-runtime: 2.3.0 + hast-util-to-jsx-runtime: 2.3.1 html-url-attributes: 3.0.1 mdast-util-to-hast: 13.2.0 react: 18.3.1 diff --git a/src/components/content/index.tsx b/src/components/content/index.tsx new file mode 100644 index 000000000..abfb1f006 --- /dev/null +++ b/src/components/content/index.tsx @@ -0,0 +1,9 @@ +import { Text } from "@chakra-ui/react"; +import { ComponentMap } from "applesauce-react"; + +import Mention from "./mention"; + +export const components: ComponentMap = { + text: ({ node }) => {node.value}, + mention: Mention, +}; diff --git a/src/components/content/links.tsx b/src/components/content/links.tsx new file mode 100644 index 000000000..674786fcf --- /dev/null +++ b/src/components/content/links.tsx @@ -0,0 +1,25 @@ +import { memo, useMemo } from "react"; +import { Link } from "applesauce-content/nast"; +import { ComponentMap } from "applesauce-react"; + +export type LinkRenderer = (url: URL, node: Link) => JSX.Element | false | null; + +export default function buildLinkComponent(handlers: LinkRenderer[]) { + const LinkRenderer: ComponentMap["link"] = ({ node }) => { + const content = useMemo(() => { + try { + const url = new URL(node.href); + for (const handler of handlers) { + try { + const content = handler(url, node); + if (content) return content; + } catch (e) {} + } + } catch (error) {} + }, [node.href, node.value]); + + return content || <>{node.value}; + }; + + return memo(LinkRenderer); +} diff --git a/src/components/content/mention.tsx b/src/components/content/mention.tsx new file mode 100644 index 000000000..64168d55c --- /dev/null +++ b/src/components/content/mention.tsx @@ -0,0 +1,23 @@ +import { ComponentMap } from "applesauce-react"; + +import UserLink from "../user/user-link"; +import { EmbedEventPointer } from "../embed-event"; + +const Mention: ComponentMap["mention"] = ({ node }) => { + switch (node.decoded.type) { + case "npub": + return ; + case "nprofile": + return ; + case "nevent": + case "nrelay": + case "naddr": + case "note": + return ; + + default: + return null; + } +}; + +export default Mention; diff --git a/src/components/debug-modal/event-debug-modal.tsx b/src/components/debug-modal/event-debug-modal.tsx index 4e5390dd4..790d02b25 100644 --- a/src/components/debug-modal/event-debug-modal.tsx +++ b/src/components/debug-modal/event-debug-modal.tsx @@ -17,11 +17,13 @@ import { AccordionPanelProps, Button, Text, + Select, } from "@chakra-ui/react"; import { Link as RouterLink } from "react-router-dom"; import { ModalProps } from "@chakra-ui/react"; import { getSeenRelays } from "applesauce-core/helpers"; import { nip19 } from "nostr-tools"; +import { ParsedTextContentSymbol } from "applesauce-content/text"; import { getContentPointers, getContentTagRefs, getThreadReferences } from "../../helpers/nostr/event"; import { NostrEvent } from "../../types/nostr-event"; @@ -75,6 +77,8 @@ export default function EventDebugModal({ event, ...props }: { event: NostrEvent setLoading(false); }, []); + const nast = Reflect.get(event, ParsedTextContentSymbol); + return ( @@ -155,6 +159,11 @@ export default function EventDebugModal({ event, ...props }: { event: NostrEvent Broadcast + {nast && ( +
+ +
+ )} diff --git a/src/components/external-embeds/types/common.tsx b/src/components/external-embeds/types/common.tsx index 1e75f97d8..e2764b085 100644 --- a/src/components/external-embeds/types/common.tsx +++ b/src/components/external-embeds/types/common.tsx @@ -1,4 +1,5 @@ import { Link } from "@chakra-ui/react"; +import { Link as NastLink } from "applesauce-content/nast"; import OpenGraphCard from "../../open-graph/open-graph-card"; import OpenGraphLink from "../../open-graph/open-graph-link"; @@ -16,6 +17,6 @@ export function renderGenericUrl(match: URL) { ); } -export function renderOpenGraphUrl(match: URL, isEndOfLine: boolean) { - return isEndOfLine ? : ; +export function renderOpenGraphUrl(match: URL, node: NastLink) { + return node.data?.eol ? : ; } diff --git a/src/components/external-embeds/types/twitter.tsx b/src/components/external-embeds/types/twitter.tsx index 3b4e7d3a9..d2caeae8f 100644 --- a/src/components/external-embeds/types/twitter.tsx +++ b/src/components/external-embeds/types/twitter.tsx @@ -1,18 +1,20 @@ +import { Link } from "applesauce-content/nast"; + +import { renderOpenGraphUrl } from "./common"; import { replaceDomain } from "../../../helpers/url"; import useAppSettings from "../../../hooks/use-app-settings"; -import { renderOpenGraphUrl } from "./common"; // copied from https://github.com/SimonBrazell/privacy-redirect/blob/master/src/assets/javascripts/helpers/twitter.js export const TWITTER_DOMAINS = ["x.com", "twitter.com", "www.twitter.com", "mobile.twitter.com", "pbs.twimg.com"]; -function TwitterLink({ url, isLineEnd }: { url: URL; isLineEnd?: boolean }) { +function TwitterLink({ url, node }: { url: URL; node: Link }) { const { twitterRedirect } = useAppSettings(); - if (twitterRedirect) return renderOpenGraphUrl(replaceDomain(url, twitterRedirect), !!isLineEnd); - else return renderOpenGraphUrl(url, !!isLineEnd); + if (twitterRedirect) return renderOpenGraphUrl(replaceDomain(url, twitterRedirect), node); + else return renderOpenGraphUrl(url, node); } -export function renderTwitterUrl(match: URL, isLineEnd: boolean) { +export function renderTwitterUrl(match: URL, node: Link) { if (!TWITTER_DOMAINS.includes(match.hostname)) return null; - return ; + return ; } diff --git a/src/components/note/timeline-note/text-note-contents.tsx b/src/components/note/timeline-note/text-note-contents.tsx index 4bf7dfae8..4be898888 100644 --- a/src/components/note/timeline-note/text-note-contents.tsx +++ b/src/components/note/timeline-note/text-note-contents.tsx @@ -1,13 +1,9 @@ -import React, { Suspense } from "react"; +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 { EmbedableContent, embedUrls, truncateEmbedableContent } from "../../../helpers/embeds"; import { - embedLightningInvoice, - embedNostrLinks, - embedNostrMentions, - embedNostrHashtags, renderWavlakeUrl, renderYoutubeURL, renderImageUrl, @@ -16,17 +12,12 @@ import { renderSpotifyUrl, renderTidalUrl, renderVideoUrl, - embedEmoji, renderOpenGraphUrl, - embedImageGallery, - renderGenericUrl, renderSongDotLinkUrl, - embedCashuTokens, renderStemstrUrl, renderSoundCloudUrl, renderSimpleXLink, renderRedditUrl, - embedNipDefinitions, renderAudioUrl, renderModelUrl, renderCodePenURL, @@ -35,53 +26,31 @@ import { } from "../../external-embeds"; import { LightboxProvider } from "../../lightbox-provider"; import MediaOwnerProvider from "../../../providers/local/media-owner-provider"; -import { embedNostrWikiLinks } from "../../external-embeds/types/wiki"; +import buildLinkComponent from "../../content/links"; +import { components } from "../../content"; -function buildContents(event: NostrEvent | EventTemplate, simpleLinks = false) { - let content: EmbedableContent = [event.content.trim()]; +// function buildContents(event: NostrEvent | EventTemplate, simpleLinks = false) { +// let content: EmbedableContent = [event.content.trim()]; - // image gallery - content = embedImageGallery(content, event as NostrEvent); +// // image gallery +// content = embedImageGallery(content, event as NostrEvent); - // common - content = embedUrls(content, [ - renderSimpleXLink, - renderYoutubeURL, - renderTwitterUrl, - renderRedditUrl, - renderWavlakeUrl, - renderAppleMusicUrl, - renderSpotifyUrl, - renderTidalUrl, - renderSongDotLinkUrl, - renderStemstrUrl, - renderSoundCloudUrl, - renderImageUrl, - renderVideoUrl, - renderStreamUrl, - renderAudioUrl, - renderModelUrl, - renderCodePenURL, - renderArchiveOrgURL, - simpleLinks ? renderGenericUrl : renderOpenGraphUrl, - ]); +// // bitcoin +// content = embedLightningInvoice(content); - // bitcoin - content = embedLightningInvoice(content); +// // cashu +// content = embedCashuTokens(content); - // cashu - content = embedCashuTokens(content); +// // nostr +// content = embedNostrLinks(content); +// content = embedNostrMentions(content, event); +// content = embedNostrHashtags(content, event); +// content = embedNipDefinitions(content); +// content = embedEmoji(content, event); +// content = embedNostrWikiLinks(content); - // nostr - content = embedNostrLinks(content); - content = embedNostrMentions(content, event); - content = embedNostrHashtags(content, event); - content = embedNipDefinitions(content); - content = embedEmoji(content, event); - content = embedNostrWikiLinks(content); - - return content; -} +// return content; +// } export type TextNoteContentsProps = { event: NostrEvent | EventTemplate; @@ -91,11 +60,45 @@ export type TextNoteContentsProps = { export const TextNoteContents = React.memo( ({ event, noOpenGraphLinks, maxLength, ...props }: TextNoteContentsProps & Omit) => { - let content = buildContents(event, noOpenGraphLinks); + // let content = buildContents(event, noOpenGraphLinks); - if (maxLength !== undefined) { - content = truncateEmbedableContent(content, maxLength); - } + // 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); return ( diff --git a/src/hooks/use-user-profile.ts b/src/hooks/use-user-profile.ts index 89a90f82e..6ef1b46b9 100644 --- a/src/hooks/use-user-profile.ts +++ b/src/hooks/use-user-profile.ts @@ -4,8 +4,8 @@ import userMetadataService from "../services/user-metadata"; import { useReadRelays } from "./use-client-relays"; import { RequestOptions } from "../services/replaceable-events"; import { COMMON_CONTACT_RELAYS } from "../const"; -import { queryStore } from "../services/event-store"; -import { useObservable } from "./use-observable"; +import { useStoreQuery } from "applesauce-react"; +import { ProfileQuery } from "applesauce-core/queries"; export default function useUserProfile(pubkey?: string, additionalRelays?: Iterable, opts?: RequestOptions) { const readRelays = useReadRelays( @@ -16,6 +16,5 @@ export default function useUserProfile(pubkey?: string, additionalRelays?: Itera if (pubkey) userMetadataService.requestMetadata(pubkey, readRelays, opts); }, [pubkey, readRelays]); - const observable = pubkey ? queryStore.profile(pubkey) : undefined; - return useObservable(observable); + return useStoreQuery(ProfileQuery, pubkey ? [pubkey] : undefined); } diff --git a/src/views/channels/components/channel-message-content.tsx b/src/views/channels/components/channel-message-content.tsx index bd508a02d..656c1fd5d 100644 --- a/src/views/channels/components/channel-message-content.tsx +++ b/src/views/channels/components/channel-message-content.tsx @@ -1,18 +1,10 @@ import { memo, useMemo } from "react"; import { Box, BoxProps } from "@chakra-ui/react"; +import { useRenderedContent } from "applesauce-react"; import { NostrEvent } from "../../../types/nostr-event"; import { TrustProvider } from "../../../providers/local/trust-provider"; -import { EmbedableContent, embedUrls } from "../../../helpers/embeds"; import { - embedCashuTokens, - embedEmoji, - embedImageGallery, - embedLightningInvoice, - embedNipDefinitions, - embedNostrHashtags, - embedNostrLinks, - embedNostrMentions, renderAppleMusicUrl, renderGenericUrl, renderImageUrl, @@ -31,49 +23,83 @@ import { } from "../../../components/external-embeds"; import { LightboxProvider } from "../../../components/lightbox-provider"; import { renderAudioUrl } from "../../../components/external-embeds/types/audio"; +import buildLinkComponent from "../../../components/content/links"; +import { components } from "../../../components/content"; const ChannelMessageContent = memo(({ message, children, ...props }: BoxProps & { message: NostrEvent }) => { - const content = useMemo(() => { - let c: EmbedableContent = [message.content]; + // const content = useMemo(() => { + // let c: EmbedableContent = [message.content]; - // image gallery - c = embedImageGallery(c, message); + // // 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, - ]); + // // common + // c = embedUrls(c, [ + // renderSimpleXLink, + // renderYoutubeURL, + // renderTwitterUrl, + // renderRedditUrl, + // renderWavlakeUrl, + // renderAppleMusicUrl, + // renderSpotifyUrl, + // renderTidalUrl, + // renderSongDotLinkUrl, + // renderStemstrUrl, + // renderSoundCloudUrl, + // renderImageUrl, + // renderVideoUrl, + // renderStreamUrl, + // renderAudioUrl, + // renderGenericUrl, + // ]); - // bitcoin - c = embedLightningInvoice(c); + // // bitcoin + // c = embedLightningInvoice(c); - // cashu - c = embedCashuTokens(c); + // // cashu + // c = embedCashuTokens(c); - // nostr - c = embedNostrLinks(c); - c = embedNostrMentions(c, message); - c = embedNostrHashtags(c, message); - c = embedNipDefinitions(c); - c = embedEmoji(c, message); + // // nostr + // c = embedNostrLinks(c); + // c = embedNostrMentions(c, message); + // c = embedNostrHashtags(c, message); + // c = embedNipDefinitions(c); + // c = embedEmoji(c, message); - return c; - }, [message.content]); + // 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); return ( diff --git a/src/views/dms/components/direct-message-content.tsx b/src/views/dms/components/direct-message-content.tsx index 9f4223076..495cca07a 100644 --- a/src/views/dms/components/direct-message-content.tsx +++ b/src/views/dms/components/direct-message-content.tsx @@ -1,9 +1,9 @@ +import { useMemo } from "react"; import { Box, BoxProps } from "@chakra-ui/react"; -import { EmbedableContent, embedUrls } from "../../../helpers/embeds"; +import { useRenderedContent } from "applesauce-react"; + import { NostrEvent } from "../../../types/nostr-event"; import { - embedCashuTokens, - embedNostrLinks, renderAppleMusicUrl, renderGenericUrl, renderImageUrl, @@ -23,6 +23,8 @@ import { 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 { components } from "../../../components/content"; export default function DirectMessageContent({ event, @@ -30,30 +32,37 @@ export default function DirectMessageContent({ children, ...props }: { event: NostrEvent; text: string } & BoxProps) { - let content: EmbedableContent = [text]; + 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], + ); - content = embedNostrLinks(content); - content = embedUrls(content, [ - renderSimpleXLink, - renderYoutubeURL, - renderTwitterUrl, - renderRedditUrl, - renderWavlakeUrl, - renderAppleMusicUrl, - renderSpotifyUrl, - renderTidalUrl, - renderSongDotLinkUrl, - renderStemstrUrl, - renderSoundCloudUrl, - renderImageUrl, - renderVideoUrl, - renderStreamUrl, - renderAudioUrl, - renderGenericUrl, - ]); - - // cashu - content = embedCashuTokens(content); + const content = useRenderedContent(event, componentsMap); return (