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 (