use applesauce-content for rendering notes

This commit is contained in:
hzrd149 2024-10-14 17:46:52 +01:00
parent fc8c758e82
commit 1282aee6f4
12 changed files with 270 additions and 143 deletions

View File

@ -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",

30
pnpm-lock.yaml generated
View File

@ -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

View File

@ -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 }) => <Text as="span">{node.value}</Text>,
mention: Mention,
};

View File

@ -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);
}

View File

@ -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 <UserLink showAt pubkey={node.decoded.data} color="blue.500" />;
case "nprofile":
return <UserLink showAt pubkey={node.decoded.data.pubkey} color="blue.500" />;
case "nevent":
case "nrelay":
case "naddr":
case "note":
return <EmbedEventPointer pointer={node.decoded} />;
default:
return null;
}
};
export default Mention;

View File

@ -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 (
<Modal size="6xl" {...props}>
<ModalOverlay />
@ -155,6 +159,11 @@ export default function EventDebugModal({ event, ...props }: { event: NostrEvent
Broadcast
</Button>
</Section>
{nast && (
<Section label="Parsed Content" p="0">
<JsonCode data={nast} />
</Section>
)}
</Accordion>
</ModalBody>
</ModalContent>

View File

@ -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 ? <OpenGraphCard url={match} /> : <OpenGraphLink url={match} />;
export function renderOpenGraphUrl(match: URL, node: NastLink) {
return node.data?.eol ? <OpenGraphCard url={match} /> : <OpenGraphLink url={match} />;
}

View File

@ -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 <TwitterLink url={match} isLineEnd={isLineEnd} />;
return <TwitterLink url={match} node={node} />;
}

View File

@ -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<BoxProps, "children">) => {
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 (
<MediaOwnerProvider owner={(event as NostrEvent).pubkey as string | undefined}>

View File

@ -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<string>, 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);
}

View File

@ -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 (
<TrustProvider event={message}>

View File

@ -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 (
<TrustProvider event={event}>