mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-26 17:52:18 +01:00
use applesauce for all content rendering
This commit is contained in:
parent
d66ee1e062
commit
44757c471a
@ -1,18 +1,18 @@
|
||||
import { useAsync } from "react-use";
|
||||
import { Box, Button, ButtonGroup, Card, CardProps, Heading, IconButton, Link, Spinner, Text } from "@chakra-ui/react";
|
||||
import { Token, getEncodedToken } from "@cashu/cashu-ts";
|
||||
import { Token, getEncodedToken, CashuMint, CashuWallet } from "@cashu/cashu-ts";
|
||||
|
||||
import { CopyIconButton } from "../copy-icon-button";
|
||||
import useUserProfile from "../../hooks/use-user-profile";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import { ECashIcon, WalletIcon } from "../icons";
|
||||
import { getMint } from "../../services/cashu-mints";
|
||||
import CurrencyDollar from "../icons/currency-dollar";
|
||||
import CurrencyEthereum from "../icons/currency-ethereum";
|
||||
import CurrencyEuro from "../icons/currency-euro";
|
||||
import CurrencyYen from "../icons/currency-yen";
|
||||
import CurrencyPound from "../icons/currency-pound";
|
||||
import CurrencyBitcoin from "../icons/currency-bitcoin";
|
||||
import { getMintWallet } from "../../services/cashu-mints";
|
||||
|
||||
function RedeemButton({ token }: { token: string }) {
|
||||
const account = useCurrentAccount()!;
|
||||
@ -40,9 +40,9 @@ export default function InlineCachuCard({
|
||||
const { value: spendable, loading } = useAsync(async () => {
|
||||
if (!token) return;
|
||||
for (const entry of token.token) {
|
||||
const mint = await getMint(entry.mint);
|
||||
const spent = await mint.check({ Ys: entry.proofs.map((p) => p.secret) });
|
||||
if (spent.states.some((v) => v.state === "UNSPENT")) return true;
|
||||
const wallet = await getMintWallet(entry.mint);
|
||||
const spent = await wallet.checkProofsSpent(entry.proofs);
|
||||
if (spent.length !== entry.proofs.length) return true;
|
||||
}
|
||||
return false;
|
||||
}, [token]);
|
||||
@ -90,7 +90,7 @@ export default function InlineCachuCard({
|
||||
<Card p="2" flexDirection="column" borderColor="green.500" gap="2" {...props}>
|
||||
<Box>
|
||||
<UnitIcon boxSize={10} color={unitColor} float="left" mr="2" mb="1" />
|
||||
<ButtonGroup float="right">
|
||||
<ButtonGroup float="right" size="sm">
|
||||
<CopyIconButton value={encoded} title="Copy Token" aria-label="Copy Token" variant="ghost" />
|
||||
<IconButton
|
||||
as={Link}
|
||||
|
@ -1,31 +1,15 @@
|
||||
import React from "react";
|
||||
import React, { useMemo, useRef } from "react";
|
||||
import { Box, BoxProps, Text } from "@chakra-ui/react";
|
||||
import { Root, truncateContent } from "applesauce-content/nast";
|
||||
|
||||
import { DraftNostrEvent, NostrEvent } from "../types/nostr-event";
|
||||
import { EmbedableContent, embedUrls, truncateEmbedableContent } from "../helpers/embeds";
|
||||
import {
|
||||
embedNostrLinks,
|
||||
embedNostrMentions,
|
||||
embedNostrHashtags,
|
||||
embedEmoji,
|
||||
renderGenericUrl,
|
||||
} from "./external-embeds";
|
||||
import { LightboxProvider } from "./lightbox-provider";
|
||||
import { nostrMentions, emojis, hashtags } from "applesauce-content/text";
|
||||
import { useRenderedContent } from "applesauce-react";
|
||||
import { components } from "./content";
|
||||
import { renderGenericUrl } from "./content/links/common";
|
||||
|
||||
function buildContents(event: NostrEvent | DraftNostrEvent, textOnly = false) {
|
||||
let content: EmbedableContent = [event.content.trim().replace(/\n+/g, "\n")];
|
||||
|
||||
// common
|
||||
content = embedUrls(content, [renderGenericUrl]);
|
||||
|
||||
// nostr
|
||||
content = embedNostrLinks(content, textOnly);
|
||||
content = embedNostrMentions(content, event);
|
||||
content = embedNostrHashtags(content, event);
|
||||
content = embedEmoji(content, event);
|
||||
|
||||
return content;
|
||||
}
|
||||
const linkRenderers = [renderGenericUrl];
|
||||
|
||||
export type NoteContentsProps = {
|
||||
event: NostrEvent | DraftNostrEvent;
|
||||
@ -35,21 +19,33 @@ export type NoteContentsProps = {
|
||||
|
||||
export const CompactNoteContent = React.memo(
|
||||
({ event, maxLength, textOnly = false, ...props }: NoteContentsProps & Omit<BoxProps, "children">) => {
|
||||
let content = buildContents(event, textOnly);
|
||||
let truncated = maxLength !== undefined ? truncateEmbedableContent(content, maxLength) : content;
|
||||
const truncated = useRef(false);
|
||||
const transformers = useMemo(
|
||||
() => [
|
||||
nostrMentions,
|
||||
emojis,
|
||||
hashtags,
|
||||
() => (tree: Root) => {
|
||||
const newTree = truncateContent(tree, maxLength);
|
||||
truncated.current = newTree !== tree;
|
||||
},
|
||||
],
|
||||
[maxLength],
|
||||
);
|
||||
const content = useRenderedContent(event, components, { transformers, linkRenderers, maxLength });
|
||||
|
||||
return (
|
||||
<LightboxProvider>
|
||||
<Box whiteSpace="pre-wrap" {...props}>
|
||||
{truncated}
|
||||
{truncated !== content ? (
|
||||
{content}
|
||||
{truncated.current && (
|
||||
<>
|
||||
<span>...</span>
|
||||
<Text as="span" fontWeight="bold" ml="4">
|
||||
Show More
|
||||
</Text>
|
||||
</>
|
||||
) : null}
|
||||
)}
|
||||
</Box>
|
||||
</LightboxProvider>
|
||||
);
|
||||
|
@ -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({
|
@ -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,
|
65
src/components/content/gallery.tsx
Normal file
65
src/components/content/gallery.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import { forwardRef, MouseEventHandler, MutableRefObject, useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { Link } from "@chakra-ui/react";
|
||||
import { handleImageFallbacks } from "blossom-client-sdk";
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
|
||||
import { EmbeddedImageProps, getPubkeyMediaServers, TrustImage, useImageThumbnail } from "../content/links";
|
||||
import { useRegisterSlide } from "../lightbox-provider";
|
||||
import PhotoGallery, { PhotoWithoutSize } from "../photo-gallery";
|
||||
import { useBreakpointValue } from "../../providers/global/breakpoint-provider";
|
||||
import ExpandableEmbed from "./components/expandable-embed";
|
||||
|
||||
// nevent1qqs8397rp8tt60f3lm8zldt8uqljuqw9axp8z79w0qsmj3r96lmg4tgpz3mhxue69uhhyetvv9ujuerpd46hxtnfduq3zamnwvaz7tmwdaehgun4v5hxxmmd0mkwa9
|
||||
export const GalleryImage = forwardRef<HTMLImageElement | null, EmbeddedImageProps>(
|
||||
({ src, event, imageProps, ...props }, ref) => {
|
||||
const thumbnail = useImageThumbnail(src);
|
||||
|
||||
ref = ref || useRef<HTMLImageElement | null>(null);
|
||||
const { show } = useRegisterSlide(
|
||||
ref as MutableRefObject<HTMLImageElement | null>,
|
||||
src ? { type: "image", src, event } : undefined,
|
||||
);
|
||||
const handleClick = useCallback<MouseEventHandler<HTMLElement>>(
|
||||
(e) => {
|
||||
!e.isPropagationStopped() && show();
|
||||
e.preventDefault();
|
||||
},
|
||||
[show],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const el = (ref as MutableRefObject<HTMLImageElement | null>).current;
|
||||
if (el) handleImageFallbacks(el, getPubkeyMediaServers);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Link href={src} isExternal onClick={handleClick} {...props}>
|
||||
<TrustImage src={thumbnail} cursor="pointer" ref={ref} onClick={handleClick} w="full" {...imageProps} />
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export function ImageGallery({ images, event }: { images: string[]; event?: NostrEvent }) {
|
||||
const photos = useMemo(() => {
|
||||
return images.map((img) => {
|
||||
const photo: PhotoWithoutSize = { src: img, key: img };
|
||||
return photo;
|
||||
});
|
||||
}, [images]);
|
||||
|
||||
const rowMultiplier = useBreakpointValue({ base: 1.5, sm: 2, md: 3, lg: 4, xl: 5 }) ?? 4;
|
||||
|
||||
return (
|
||||
<ExpandableEmbed label="Image Gallery" hideOnDefaultOpen urls={images}>
|
||||
<PhotoGallery
|
||||
layout="rows"
|
||||
photos={photos}
|
||||
renderPhoto={({ photo, imageProps, wrapperStyle }) => (
|
||||
<GalleryImage src={imageProps.src} style={imageProps.style} />
|
||||
)}
|
||||
targetRowHeight={(containerWidth) => containerWidth / rowMultiplier}
|
||||
/>
|
||||
</ExpandableEmbed>
|
||||
);
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import { lazy } from "react";
|
||||
import { Text } from "@chakra-ui/react";
|
||||
import { Link, Text } from "@chakra-ui/react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { ComponentMap } from "applesauce-react";
|
||||
|
||||
import Mention from "./mention";
|
||||
@ -13,4 +14,9 @@ export const components: ComponentMap = {
|
||||
cashu: Cashu,
|
||||
fedimint: ({ node }) => <InlineFedimintCard token={node.token} />,
|
||||
emoji: ({ node }) => <InlineEmoji url={node.url} code={node.code} />,
|
||||
hashtag: ({ node }) => (
|
||||
<Link as={RouterLink} to={`/t/${node.hashtag}`} color="blue.500">
|
||||
#{node.name}
|
||||
</Link>
|
||||
),
|
||||
};
|
||||
|
@ -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;
|
@ -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) {
|
@ -1,14 +1,4 @@
|
||||
import {
|
||||
MouseEvent,
|
||||
MouseEventHandler,
|
||||
MutableRefObject,
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { MouseEvent, MouseEventHandler, forwardRef, useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Code,
|
||||
@ -35,17 +25,13 @@ import {
|
||||
USER_BLOSSOM_SERVER_LIST_KIND,
|
||||
} from "blossom-client-sdk";
|
||||
|
||||
import { EmbedableContent, defaultGetLocation } from "../../../helpers/embeds";
|
||||
import { getMatchLink } from "../../../helpers/regexp";
|
||||
import { useRegisterSlide } from "../../lightbox-provider";
|
||||
import { isImageURL } from "../../../helpers/url";
|
||||
import PhotoGallery, { PhotoWithoutSize } from "../../photo-gallery";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import useAppSettings from "../../../hooks/use-app-settings";
|
||||
import { useBreakpointValue } from "../../../providers/global/breakpoint-provider";
|
||||
import useElementTrustBlur from "../../../hooks/use-element-trust-blur";
|
||||
import { buildImageProxyURL } from "../../../helpers/image";
|
||||
import ExpandableEmbed from "../expandable-embed";
|
||||
import ExpandableEmbed from "../components/expandable-embed";
|
||||
import { useMediaOwnerContext } from "../../../providers/local/media-owner-provider";
|
||||
import replaceableEventsService from "../../../services/replaceable-events";
|
||||
import clientRelaysService from "../../../services/client-relays";
|
||||
@ -76,7 +62,7 @@ export type EmbeddedImageProps = Omit<LinkProps, "children" | "href" | "onClick"
|
||||
imageProps?: TrustImageProps;
|
||||
};
|
||||
|
||||
function getPubkeyMediaServers(pubkey?: string) {
|
||||
export function getPubkeyMediaServers(pubkey?: string) {
|
||||
if (!pubkey) return;
|
||||
|
||||
return new Promise<URL[] | undefined>((res) => {
|
||||
@ -93,7 +79,7 @@ function getPubkeyMediaServers(pubkey?: string) {
|
||||
});
|
||||
}
|
||||
|
||||
function useImageThumbnail(src?: string) {
|
||||
export function useImageThumbnail(src?: string) {
|
||||
return (src && buildImageProxyURL(src, "512,fit")) ?? src;
|
||||
}
|
||||
|
||||
@ -133,122 +119,6 @@ export function EmbeddedImage({ src, event, imageProps, ...props }: EmbeddedImag
|
||||
);
|
||||
}
|
||||
|
||||
export const GalleryImage = forwardRef<HTMLImageElement | null, EmbeddedImageProps>(
|
||||
({ src, event, imageProps, ...props }, ref) => {
|
||||
const thumbnail = useImageThumbnail(src);
|
||||
|
||||
ref = ref || useRef<HTMLImageElement | null>(null);
|
||||
const { show } = useRegisterSlide(
|
||||
ref as MutableRefObject<HTMLImageElement | null>,
|
||||
src ? { type: "image", src, event } : undefined,
|
||||
);
|
||||
const handleClick = useCallback<MouseEventHandler<HTMLElement>>(
|
||||
(e) => {
|
||||
!e.isPropagationStopped() && show();
|
||||
e.preventDefault();
|
||||
},
|
||||
[show],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const el = (ref as MutableRefObject<HTMLImageElement | null>).current;
|
||||
if (el) handleImageFallbacks(el, getPubkeyMediaServers);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Link href={src} isExternal onClick={handleClick} {...props}>
|
||||
<TrustImage src={thumbnail} cursor="pointer" ref={ref} onClick={handleClick} w="full" {...imageProps} />
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export function ImageGallery({ images, event }: { images: string[]; event?: NostrEvent }) {
|
||||
const photos = useMemo(() => {
|
||||
return images.map((img) => {
|
||||
const photo: PhotoWithoutSize = { src: img, key: img };
|
||||
return photo;
|
||||
});
|
||||
}, [images]);
|
||||
|
||||
const rowMultiplier = useBreakpointValue({ base: 1.5, sm: 2, md: 3, lg: 4, xl: 5 }) ?? 4;
|
||||
|
||||
return (
|
||||
<ExpandableEmbed label="Image Gallery" hideOnDefaultOpen urls={images}>
|
||||
<PhotoGallery
|
||||
layout="rows"
|
||||
photos={photos}
|
||||
renderPhoto={({ photo, imageProps, wrapperStyle }) => (
|
||||
<GalleryImage src={imageProps.src} style={imageProps.style} />
|
||||
)}
|
||||
targetRowHeight={(containerWidth) => containerWidth / rowMultiplier}
|
||||
/>
|
||||
</ExpandableEmbed>
|
||||
);
|
||||
}
|
||||
|
||||
// nevent1qqs8397rp8tt60f3lm8zldt8uqljuqw9axp8z79w0qsmj3r96lmg4tgpz3mhxue69uhhyetvv9ujuerpd46hxtnfduq3zamnwvaz7tmwdaehgun4v5hxxmmd0mkwa9
|
||||
export function embedImageGallery(content: EmbedableContent, event?: NostrEvent): EmbedableContent {
|
||||
return content
|
||||
.map((subContent, i) => {
|
||||
if (typeof subContent === "string") {
|
||||
const matches = Array.from(subContent.matchAll(getMatchLink()));
|
||||
|
||||
const newContent: EmbedableContent = [];
|
||||
let lastBatchEnd = 0;
|
||||
let batch: RegExpMatchArray[] = [];
|
||||
|
||||
const renderBatch = () => {
|
||||
if (batch.length > 1) {
|
||||
// render previous batch
|
||||
const lastMatchPosition = defaultGetLocation(batch[batch.length - 1]);
|
||||
const before = subContent.substring(lastBatchEnd, defaultGetLocation(batch[0]).start);
|
||||
const render = <ImageGallery images={batch.map((m) => m[0])} event={event} />;
|
||||
|
||||
newContent.push(before, render);
|
||||
lastBatchEnd = lastMatchPosition.end;
|
||||
}
|
||||
|
||||
batch = [];
|
||||
};
|
||||
|
||||
for (const match of matches) {
|
||||
try {
|
||||
const url = new URL(match[0]);
|
||||
if (!isImageURL(url)) throw new Error("not an image");
|
||||
|
||||
// if this is the first image, add it to the batch
|
||||
if (batch.length === 0) {
|
||||
batch = [match];
|
||||
continue;
|
||||
}
|
||||
|
||||
const last = defaultGetLocation(batch[batch.length - 1]);
|
||||
const position = defaultGetLocation(match);
|
||||
const space = subContent.substring(last.end, position.start).trim();
|
||||
|
||||
// if there was a non-space between this and the last batch
|
||||
if (space.length > 0) renderBatch();
|
||||
|
||||
batch.push(match);
|
||||
} catch (e) {
|
||||
// start a new batch without current match
|
||||
batch = [];
|
||||
}
|
||||
}
|
||||
|
||||
renderBatch();
|
||||
|
||||
newContent.push(subContent.substring(lastBatchEnd));
|
||||
|
||||
return newContent;
|
||||
}
|
||||
|
||||
return subContent;
|
||||
})
|
||||
.flat();
|
||||
}
|
||||
|
||||
function VerifyImageButton({ src, original }: { src: URL; original: string }) {
|
||||
const toast = useToast();
|
||||
const [loading, setLoading] = useState(false);
|
15
src/components/content/links/index.ts
Normal file
15
src/components/content/links/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export * from "./audio";
|
||||
export * from "./code";
|
||||
export * from "./common";
|
||||
export * from "./emoji";
|
||||
export * from "./image";
|
||||
export * from "./lightning";
|
||||
export * from "./model";
|
||||
export * from "./music";
|
||||
export * from "./nostr";
|
||||
export * from "./reddit";
|
||||
export * from "./simplex";
|
||||
export * from "./twitter";
|
||||
export * from "./video";
|
||||
export * from "./wiki";
|
||||
export * from "./youtube";
|
@ -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 (
|
@ -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"));
|
||||
|
@ -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" };
|
27
src/components/content/links/nostr.tsx
Normal file
27
src/components/content/links/nostr.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { Link, Tooltip } from "@chakra-ui/react";
|
||||
|
||||
import { EmbedableContent, embedJSX } from "../../../helpers/embeds";
|
||||
import { NIP_NAMES } from "../../../views/relays/components/supported-nips";
|
||||
|
||||
export function embedNipDefinitions(content: EmbedableContent) {
|
||||
return embedJSX(content, {
|
||||
name: "nip-definition",
|
||||
regexp: /nip-?(\d\d)/gi,
|
||||
render: (match) => {
|
||||
if (NIP_NAMES[match[1]]) {
|
||||
return (
|
||||
<Tooltip label={NIP_NAMES[match[1]]} aria-label="NIP Definition">
|
||||
<Link
|
||||
isExternal
|
||||
href={`https://github.com/nostr-protocol/nips/blob/master/${match[1]}.md`}
|
||||
textDecoration="underline"
|
||||
>
|
||||
{match[0]}
|
||||
</Link>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
});
|
||||
}
|
@ -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`
|
@ -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
|
@ -34,6 +34,7 @@ import relayHintService from "../../services/event-relay-hint";
|
||||
import { usePublishEvent } from "../../providers/global/publish-provider";
|
||||
import { EditIcon } from "../icons";
|
||||
import { RelayFavicon } from "../relay-favicon";
|
||||
import { Root } from "applesauce-content/nast";
|
||||
|
||||
function Section({
|
||||
label,
|
||||
@ -77,7 +78,7 @@ export default function EventDebugModal({ event, ...props }: { event: NostrEvent
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
const nast = Reflect.get(event, ParsedTextContentSymbol);
|
||||
const nast = Reflect.get(event, ParsedTextContentSymbol) as Root;
|
||||
|
||||
return (
|
||||
<Modal size="6xl" {...props}>
|
||||
@ -161,7 +162,7 @@ export default function EventDebugModal({ event, ...props }: { event: NostrEvent
|
||||
</Section>
|
||||
{nast && (
|
||||
<Section label="Parsed Content" p="0">
|
||||
<JsonCode data={nast} />
|
||||
<JsonCode data={nast.children} />
|
||||
</Section>
|
||||
)}
|
||||
</Accordion>
|
||||
|
@ -1,42 +1,29 @@
|
||||
import { useContext, useMemo } from "react";
|
||||
import { Box, Button, ButtonGroup, Card, CardBody, CardHeader, CardProps, Text } from "@chakra-ui/react";
|
||||
import { useRenderedContent } from "applesauce-react";
|
||||
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import UserAvatarLink from "../../user/user-avatar-link";
|
||||
import UserLink from "../../user/user-link";
|
||||
import UserDnsIdentity from "../../user/user-dns-identity";
|
||||
import {
|
||||
embedEmoji,
|
||||
embedNostrHashtags,
|
||||
embedNostrLinks,
|
||||
renderGenericUrl,
|
||||
renderImageUrl,
|
||||
renderVideoUrl,
|
||||
} from "../../external-embeds";
|
||||
import { EmbedableContent, embedUrls } from "../../../helpers/embeds";
|
||||
import { renderGenericUrl, renderImageUrl, renderVideoUrl } from "../../content/links";
|
||||
import Timestamp from "../../timestamp";
|
||||
import { ExternalLinkIcon } from "../../icons";
|
||||
import { renderAudioUrl } from "../../external-embeds/types/audio";
|
||||
import { renderAudioUrl } from "../../content/links/audio";
|
||||
import DebugEventButton from "../../debug-modal/debug-event-button";
|
||||
import DebugEventTags from "../../debug-modal/event-tags";
|
||||
import { AppHandlerContext } from "../../../providers/route/app-handler-provider";
|
||||
import relayHintService from "../../../services/event-relay-hint";
|
||||
import { components } from "../../content";
|
||||
|
||||
const linkRenderers = [renderImageUrl, renderVideoUrl, renderAudioUrl, renderGenericUrl];
|
||||
|
||||
export default function EmbeddedUnknown({ event, ...props }: Omit<CardProps, "children"> & { event: NostrEvent }) {
|
||||
const address = useMemo(() => relayHintService.getSharableEventAddress(event), [event]);
|
||||
const { openAddress } = useContext(AppHandlerContext);
|
||||
|
||||
const alt = event.tags.find((t) => t[0] === "alt")?.[1];
|
||||
const content = useMemo(() => {
|
||||
let jsx: EmbedableContent = [event.content];
|
||||
jsx = embedNostrLinks(jsx);
|
||||
jsx = embedNostrHashtags(jsx, event);
|
||||
jsx = embedEmoji(jsx, event);
|
||||
|
||||
jsx = embedUrls(jsx, [renderImageUrl, renderVideoUrl, renderAudioUrl, renderGenericUrl]);
|
||||
|
||||
return jsx;
|
||||
}, [event.content]);
|
||||
const content = useRenderedContent(event, components, { linkRenderers });
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -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";
|
@ -1,16 +0,0 @@
|
||||
import { lazy } from "react";
|
||||
import { EmbedableContent, embedJSX } from "../../../helpers/embeds";
|
||||
import { getMatchCashu } from "../../../helpers/regexp";
|
||||
|
||||
const InlineCachuCard = lazy(() => import("../../cashu/inline-cashu-card"));
|
||||
|
||||
export function embedCashuTokens(content: EmbedableContent) {
|
||||
return embedJSX(content, {
|
||||
regexp: getMatchCashu(),
|
||||
render: (match) => {
|
||||
// set zIndex and position so link over does not cover card
|
||||
return <InlineCachuCard token={match[0]} zIndex={1} position="relative" />;
|
||||
},
|
||||
name: "emoji",
|
||||
});
|
||||
}
|
@ -1,119 +0,0 @@
|
||||
import { Link, Tooltip } from "@chakra-ui/react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import { EmbedableContent, embedJSX } from "../../../helpers/embeds";
|
||||
import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event";
|
||||
import UserLink from "../../user/user-link";
|
||||
import { getMatchHashtag, getMatchNostrLink, stripInvisibleChar } from "../../../helpers/regexp";
|
||||
import { safeDecode } from "../../../helpers/nip19";
|
||||
import { EmbedEventPointer } from "../../embed-event";
|
||||
import { NIP_NAMES } from "../../../views/relays/components/supported-nips";
|
||||
|
||||
// nostr:nevent1qqsthg2qlxp9l7egtwa92t8lusm7pjknmjwa75ctrrpcjyulr9754fqpz3mhxue69uhhyetvv9ujuerpd46hxtnfduq36amnwvaz7tmwdaehgu3dwp6kytnhv4kxcmmjv3jhytnwv46q2qg5q9
|
||||
// nostr:nevent1qqsq3wc73lqxd70lg43m5rul57d4mhcanttjat56e30yx5zla48qzlspz9mhxue69uhkummnw3e82efwvdhk6qgdwaehxw309ahx7uewd3hkcq5hsum
|
||||
export function embedNostrLinks(content: EmbedableContent, inline = false) {
|
||||
return embedJSX(content, {
|
||||
name: "nostr-link",
|
||||
regexp: getMatchNostrLink(),
|
||||
render: (match) => {
|
||||
const decoded = safeDecode(match[2]);
|
||||
if (!decoded) return null;
|
||||
|
||||
switch (decoded.type) {
|
||||
case "npub":
|
||||
return <UserLink color="blue.500" pubkey={decoded.data} showAt />;
|
||||
case "nprofile":
|
||||
return <UserLink color="blue.500" pubkey={decoded.data.pubkey} showAt />;
|
||||
case "note":
|
||||
case "nevent":
|
||||
case "naddr":
|
||||
case "nrelay":
|
||||
return inline === false ? <EmbedEventPointer pointer={decoded} /> : null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
export function embedNostrMentions(content: EmbedableContent, event: NostrEvent | DraftNostrEvent) {
|
||||
return embedJSX(content, {
|
||||
name: "nostr-mention",
|
||||
regexp: /#\[(\d+)\]/g,
|
||||
render: (match) => {
|
||||
const index = parseInt(match[1]);
|
||||
const tag = event?.tags[index];
|
||||
|
||||
if (tag) {
|
||||
if (tag[0] === "p" && tag[1]) {
|
||||
return <UserLink color="blue.500" pubkey={tag[1]} showAt />;
|
||||
}
|
||||
if (tag[0] === "e" && tag[1]) {
|
||||
return (
|
||||
<EmbedEventPointer
|
||||
pointer={{ type: "nevent", data: { id: tag[1], relays: tag[2] ? [tag[2]] : undefined } }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function embedNostrHashtags(content: EmbedableContent, event: NostrEvent | DraftNostrEvent) {
|
||||
const hashtags = event.tags
|
||||
.filter((t) => t[0] === "t" && t[1])
|
||||
.map((t) => t[1]?.toLowerCase())
|
||||
.map(stripInvisibleChar);
|
||||
|
||||
return embedJSX(content, {
|
||||
name: "nostr-hashtag",
|
||||
regexp: getMatchHashtag(),
|
||||
getLocation: (match) => {
|
||||
if (match.index === undefined) throw new Error("match does not have index");
|
||||
|
||||
const start = match.index + match[1].length;
|
||||
const end = start + 1 + match[2].length;
|
||||
return { start, end };
|
||||
},
|
||||
render: (match) => {
|
||||
const hashtag = match[2].toLowerCase();
|
||||
|
||||
if (hashtags.includes(hashtag)) {
|
||||
return (
|
||||
<Link as={RouterLink} to={`/t/${hashtag}`} color="blue.500">
|
||||
#{match[2]}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function embedNipDefinitions(content: EmbedableContent) {
|
||||
return embedJSX(content, {
|
||||
name: "nip-definition",
|
||||
regexp: /nip-?(\d\d)/gi,
|
||||
render: (match) => {
|
||||
if (NIP_NAMES[match[1]]) {
|
||||
return (
|
||||
<Tooltip label={NIP_NAMES[match[1]]} aria-label="NIP Definition">
|
||||
<Link
|
||||
isExternal
|
||||
href={`https://github.com/nostr-protocol/nips/blob/master/${match[1]}.md`}
|
||||
textDecoration="underline"
|
||||
>
|
||||
{match[0]}
|
||||
</Link>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
});
|
||||
}
|
@ -2,6 +2,7 @@ import React, { Suspense, useMemo } from "react";
|
||||
import { Box, BoxProps, Spinner } from "@chakra-ui/react";
|
||||
import { EventTemplate, NostrEvent } from "nostr-tools";
|
||||
import { useRenderedContent } from "applesauce-react/hooks";
|
||||
import { defaultTransformers } from "applesauce-content/text";
|
||||
|
||||
import {
|
||||
renderWavlakeUrl,
|
||||
@ -23,14 +24,13 @@ import {
|
||||
renderCodePenURL,
|
||||
renderArchiveOrgURL,
|
||||
renderStreamUrl,
|
||||
} from "../../external-embeds";
|
||||
} from "../../content/links";
|
||||
import { LightboxProvider } from "../../lightbox-provider";
|
||||
import MediaOwnerProvider from "../../../providers/local/media-owner-provider";
|
||||
import buildLinkComponent from "../../content/links";
|
||||
import { components } from "../../content";
|
||||
import { FedimintTokensTransformer } from "../../../helpers/fedimint";
|
||||
import { fedimintTokens } from "../../../helpers/fedimint";
|
||||
|
||||
const transformers = [FedimintTokensTransformer];
|
||||
const transformers = [...defaultTransformers, fedimintTokens];
|
||||
|
||||
export type TextNoteContentsProps = {
|
||||
event: NostrEvent | EventTemplate;
|
||||
@ -38,47 +38,31 @@ export type TextNoteContentsProps = {
|
||||
maxLength?: number;
|
||||
};
|
||||
|
||||
const linkRenderers = [
|
||||
renderSimpleXLink,
|
||||
renderYoutubeURL,
|
||||
renderTwitterUrl,
|
||||
renderRedditUrl,
|
||||
renderWavlakeUrl,
|
||||
renderAppleMusicUrl,
|
||||
renderSpotifyUrl,
|
||||
renderTidalUrl,
|
||||
renderSongDotLinkUrl,
|
||||
renderStemstrUrl,
|
||||
renderSoundCloudUrl,
|
||||
renderImageUrl,
|
||||
renderVideoUrl,
|
||||
renderStreamUrl,
|
||||
renderAudioUrl,
|
||||
renderModelUrl,
|
||||
renderCodePenURL,
|
||||
renderArchiveOrgURL,
|
||||
renderOpenGraphUrl,
|
||||
];
|
||||
|
||||
export const TextNoteContents = React.memo(
|
||||
({ event, noOpenGraphLinks, maxLength, ...props }: TextNoteContentsProps & Omit<BoxProps, "children">) => {
|
||||
// let content = buildContents(event, noOpenGraphLinks);
|
||||
|
||||
// if (maxLength !== undefined) {
|
||||
// content = truncateEmbedableContent(content, maxLength);
|
||||
// }
|
||||
const LinkComponent = useMemo(
|
||||
() =>
|
||||
buildLinkComponent([
|
||||
renderSimpleXLink,
|
||||
renderYoutubeURL,
|
||||
renderTwitterUrl,
|
||||
renderRedditUrl,
|
||||
renderWavlakeUrl,
|
||||
renderAppleMusicUrl,
|
||||
renderSpotifyUrl,
|
||||
renderTidalUrl,
|
||||
renderSongDotLinkUrl,
|
||||
renderStemstrUrl,
|
||||
renderSoundCloudUrl,
|
||||
renderImageUrl,
|
||||
renderVideoUrl,
|
||||
renderStreamUrl,
|
||||
renderAudioUrl,
|
||||
renderModelUrl,
|
||||
renderCodePenURL,
|
||||
renderArchiveOrgURL,
|
||||
renderOpenGraphUrl,
|
||||
]),
|
||||
[],
|
||||
);
|
||||
const componentsMap = useMemo(
|
||||
() => ({
|
||||
...components,
|
||||
link: LinkComponent,
|
||||
}),
|
||||
[LinkComponent],
|
||||
);
|
||||
|
||||
const content = useRenderedContent(event, componentsMap, { transformers });
|
||||
const content = useRenderedContent(event, components, { linkRenderers, transformers, maxLength });
|
||||
|
||||
return (
|
||||
<MediaOwnerProvider owner={(event as NostrEvent).pubkey as string | undefined}>
|
||||
|
@ -5,12 +5,13 @@ import { Photo } from "react-photo-album";
|
||||
import { getMatchLink } from "../../../helpers/regexp";
|
||||
import { LightboxProvider } from "../../lightbox-provider";
|
||||
import { isImageURL } from "../../../helpers/url";
|
||||
import { EmbeddedImageProps, GalleryImage } from "../../external-embeds";
|
||||
import { EmbeddedImageProps } from "../../content/links";
|
||||
import { TrustProvider } from "../../../providers/local/trust-provider";
|
||||
import PhotoGallery, { PhotoWithoutSize } from "../../photo-gallery";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { useBreakpointValue } from "../../../providers/global/breakpoint-provider";
|
||||
import useEventIntersectionRef from "../../../hooks/use-event-intersection-ref";
|
||||
import { GalleryImage } from "../../content/gallery";
|
||||
|
||||
function CustomGalleryImage({ event, ...props }: EmbeddedImageProps & { event: NostrEvent }) {
|
||||
const ref = useEventIntersectionRef<HTMLImageElement>(event);
|
||||
|
@ -1,24 +1,21 @@
|
||||
import { useMemo } from "react";
|
||||
import { Box, BoxProps } from "@chakra-ui/react";
|
||||
import { useRenderedContent } from "applesauce-react";
|
||||
import { nostrMentions } from "applesauce-content/text";
|
||||
|
||||
import { EmbedableContent, embedUrls } from "../../helpers/embeds";
|
||||
import useUserProfile from "../../hooks/use-user-profile";
|
||||
import { embedNostrLinks, renderGenericUrl } from "../external-embeds";
|
||||
import { renderGenericUrl } from "../content/links";
|
||||
import { components } from "../content";
|
||||
|
||||
const transformers = [nostrMentions];
|
||||
const linkRenderers = [renderGenericUrl];
|
||||
|
||||
export default function UserAbout({ pubkey, ...props }: { pubkey: string } & Omit<BoxProps, "children">) {
|
||||
const metadata = useUserProfile(pubkey);
|
||||
|
||||
const aboutContent = useMemo(() => {
|
||||
if (!metadata?.about) return null;
|
||||
let content: EmbedableContent = [metadata.about.trim()];
|
||||
content = embedNostrLinks(content);
|
||||
content = embedUrls(content, [renderGenericUrl]);
|
||||
return content;
|
||||
}, [metadata?.about]);
|
||||
const profile = useUserProfile(pubkey);
|
||||
const content = useRenderedContent(profile?.about, components, { transformers, linkRenderers });
|
||||
|
||||
return (
|
||||
<Box whiteSpace="pre-line" {...props}>
|
||||
{aboutContent}
|
||||
{content}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
@ -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++) {
|
||||
|
@ -12,7 +12,7 @@ declare module "applesauce-content/nast" {
|
||||
}
|
||||
}
|
||||
|
||||
export function FedimintTokensTransformer(): Transformer<Root> {
|
||||
export function fedimintTokens(): Transformer<Root> {
|
||||
return (tree) => {
|
||||
findAndReplace(tree, [
|
||||
[
|
||||
|
@ -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);
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { CashuMint } from "@cashu/cashu-ts";
|
||||
import { CashuMint, CashuWallet } from "@cashu/cashu-ts";
|
||||
|
||||
const mints = new Map<string, CashuMint>();
|
||||
const wallets = new Map<string, CashuWallet>();
|
||||
|
||||
export async function getMint(url: string) {
|
||||
export async function getMintWallet(url: string) {
|
||||
const formatted = new URL(url).toString();
|
||||
if (!mints.has(formatted)) {
|
||||
if (!wallets.has(formatted)) {
|
||||
const mint = new CashuMint(formatted);
|
||||
mints.set(formatted, mint);
|
||||
const wallet = new CashuWallet(mint);
|
||||
wallets.set(formatted, wallet);
|
||||
}
|
||||
return mints.get(formatted)!;
|
||||
return wallets.get(formatted)!;
|
||||
}
|
||||
|
@ -4,4 +4,5 @@ body,
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { memo, useMemo } from "react";
|
||||
import { memo } from "react";
|
||||
import { Box, BoxProps } from "@chakra-ui/react";
|
||||
import { useRenderedContent } from "applesauce-react";
|
||||
|
||||
@ -20,86 +20,32 @@ import {
|
||||
renderVideoUrl,
|
||||
renderWavlakeUrl,
|
||||
renderYoutubeURL,
|
||||
} from "../../../components/external-embeds";
|
||||
} from "../../../components/content/links";
|
||||
import { LightboxProvider } from "../../../components/lightbox-provider";
|
||||
import { renderAudioUrl } from "../../../components/external-embeds/types/audio";
|
||||
import buildLinkComponent from "../../../components/content/links";
|
||||
import { renderAudioUrl } from "../../../components/content/links/audio";
|
||||
import { components } from "../../../components/content";
|
||||
|
||||
const linkRenderers = [
|
||||
renderSimpleXLink,
|
||||
renderYoutubeURL,
|
||||
renderTwitterUrl,
|
||||
renderRedditUrl,
|
||||
renderWavlakeUrl,
|
||||
renderAppleMusicUrl,
|
||||
renderSpotifyUrl,
|
||||
renderTidalUrl,
|
||||
renderSongDotLinkUrl,
|
||||
renderStemstrUrl,
|
||||
renderSoundCloudUrl,
|
||||
renderImageUrl,
|
||||
renderVideoUrl,
|
||||
renderStreamUrl,
|
||||
renderAudioUrl,
|
||||
renderGenericUrl,
|
||||
];
|
||||
|
||||
const ChannelMessageContent = memo(({ message, children, ...props }: BoxProps & { message: NostrEvent }) => {
|
||||
// const content = useMemo(() => {
|
||||
// let c: EmbedableContent = [message.content];
|
||||
|
||||
// // image gallery
|
||||
// c = embedImageGallery(c, message);
|
||||
|
||||
// // common
|
||||
// c = embedUrls(c, [
|
||||
// renderSimpleXLink,
|
||||
// renderYoutubeURL,
|
||||
// renderTwitterUrl,
|
||||
// renderRedditUrl,
|
||||
// renderWavlakeUrl,
|
||||
// renderAppleMusicUrl,
|
||||
// renderSpotifyUrl,
|
||||
// renderTidalUrl,
|
||||
// renderSongDotLinkUrl,
|
||||
// renderStemstrUrl,
|
||||
// renderSoundCloudUrl,
|
||||
// renderImageUrl,
|
||||
// renderVideoUrl,
|
||||
// renderStreamUrl,
|
||||
// renderAudioUrl,
|
||||
// renderGenericUrl,
|
||||
// ]);
|
||||
|
||||
// // bitcoin
|
||||
// c = embedLightningInvoice(c);
|
||||
|
||||
// // cashu
|
||||
// c = embedCashuTokens(c);
|
||||
|
||||
// // nostr
|
||||
// c = embedNostrLinks(c);
|
||||
// c = embedNostrMentions(c, message);
|
||||
// c = embedNostrHashtags(c, message);
|
||||
// c = embedNipDefinitions(c);
|
||||
// c = embedEmoji(c, message);
|
||||
|
||||
// return c;
|
||||
// }, [message.content]);
|
||||
|
||||
const LinkComponent = useMemo(
|
||||
() =>
|
||||
buildLinkComponent([
|
||||
renderSimpleXLink,
|
||||
renderYoutubeURL,
|
||||
renderTwitterUrl,
|
||||
renderRedditUrl,
|
||||
renderWavlakeUrl,
|
||||
renderAppleMusicUrl,
|
||||
renderSpotifyUrl,
|
||||
renderTidalUrl,
|
||||
renderSongDotLinkUrl,
|
||||
renderStemstrUrl,
|
||||
renderSoundCloudUrl,
|
||||
renderImageUrl,
|
||||
renderVideoUrl,
|
||||
renderStreamUrl,
|
||||
renderAudioUrl,
|
||||
renderGenericUrl,
|
||||
]),
|
||||
[],
|
||||
);
|
||||
const componentsMap = useMemo(
|
||||
() => ({
|
||||
...components,
|
||||
link: LinkComponent,
|
||||
}),
|
||||
[LinkComponent],
|
||||
);
|
||||
|
||||
const content = useRenderedContent(message, componentsMap);
|
||||
const content = useRenderedContent(message, components, { linkRenderers });
|
||||
|
||||
return (
|
||||
<TrustProvider event={message}>
|
||||
|
@ -1,10 +1,13 @@
|
||||
import { useState } from "react";
|
||||
import { Box, BoxProps, Button } from "@chakra-ui/react";
|
||||
import { useRenderedContent } from "applesauce-react";
|
||||
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { getCommunityDescription } from "../../../helpers/nostr/communities";
|
||||
import { EmbedableContent, embedUrls, truncateEmbedableContent } from "../../../helpers/embeds";
|
||||
import { renderGenericUrl } from "../../../components/external-embeds";
|
||||
import { components } from "../../../components/content";
|
||||
import { renderGenericUrl } from "../../../components/content/links";
|
||||
|
||||
const linkRenderers = [renderGenericUrl];
|
||||
|
||||
export default function CommunityDescription({
|
||||
community,
|
||||
@ -13,13 +16,11 @@ export default function CommunityDescription({
|
||||
...props
|
||||
}: Omit<BoxProps, "children"> & { community: NostrEvent; maxLength?: number; showExpand?: boolean }) {
|
||||
const description = getCommunityDescription(community);
|
||||
let content: EmbedableContent = description ? [description] : [];
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
|
||||
content = embedUrls(content, [renderGenericUrl]);
|
||||
if (maxLength !== undefined && !showAll) {
|
||||
content = truncateEmbedableContent(content, maxLength);
|
||||
}
|
||||
const content = useRenderedContent(description, components, {
|
||||
maxLength: showAll ? undefined : maxLength,
|
||||
linkRenderers,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useMemo } from "react";
|
||||
import { Box, BoxProps } from "@chakra-ui/react";
|
||||
import { useRenderedContent } from "applesauce-react";
|
||||
import { defaultTransformers } from "applesauce-content/text";
|
||||
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import {
|
||||
@ -19,16 +19,33 @@ import {
|
||||
renderVideoUrl,
|
||||
renderWavlakeUrl,
|
||||
renderYoutubeURL,
|
||||
} from "../../../components/external-embeds";
|
||||
} from "../../../components/content/links";
|
||||
import { TrustProvider } from "../../../providers/local/trust-provider";
|
||||
import { LightboxProvider } from "../../../components/lightbox-provider";
|
||||
import { renderAudioUrl } from "../../../components/external-embeds/types/audio";
|
||||
import buildLinkComponent from "../../../components/content/links";
|
||||
import { renderAudioUrl } from "../../../components/content/links/audio";
|
||||
import { components } from "../../../components/content";
|
||||
import { useKind4Decrypt } from "../../../hooks/use-kind4-decryption";
|
||||
import { FedimintTokensTransformer } from "../../../helpers/fedimint";
|
||||
import { fedimintTokens } from "../../../helpers/fedimint";
|
||||
|
||||
const transformers = [FedimintTokensTransformer];
|
||||
const transformers = [...defaultTransformers, fedimintTokens];
|
||||
const linkRenderers = [
|
||||
renderSimpleXLink,
|
||||
renderYoutubeURL,
|
||||
renderTwitterUrl,
|
||||
renderRedditUrl,
|
||||
renderWavlakeUrl,
|
||||
renderAppleMusicUrl,
|
||||
renderSpotifyUrl,
|
||||
renderTidalUrl,
|
||||
renderSongDotLinkUrl,
|
||||
renderStemstrUrl,
|
||||
renderSoundCloudUrl,
|
||||
renderImageUrl,
|
||||
renderVideoUrl,
|
||||
renderStreamUrl,
|
||||
renderAudioUrl,
|
||||
renderGenericUrl,
|
||||
];
|
||||
|
||||
export default function DirectMessageContent({
|
||||
event,
|
||||
@ -36,38 +53,8 @@ export default function DirectMessageContent({
|
||||
children,
|
||||
...props
|
||||
}: { event: NostrEvent; text: string } & BoxProps) {
|
||||
const LinkComponent = useMemo(
|
||||
() =>
|
||||
buildLinkComponent([
|
||||
renderSimpleXLink,
|
||||
renderYoutubeURL,
|
||||
renderTwitterUrl,
|
||||
renderRedditUrl,
|
||||
renderWavlakeUrl,
|
||||
renderAppleMusicUrl,
|
||||
renderSpotifyUrl,
|
||||
renderTidalUrl,
|
||||
renderSongDotLinkUrl,
|
||||
renderStemstrUrl,
|
||||
renderSoundCloudUrl,
|
||||
renderImageUrl,
|
||||
renderVideoUrl,
|
||||
renderStreamUrl,
|
||||
renderAudioUrl,
|
||||
renderGenericUrl,
|
||||
]),
|
||||
[],
|
||||
);
|
||||
const componentsMap = useMemo(
|
||||
() => ({
|
||||
...components,
|
||||
link: LinkComponent,
|
||||
}),
|
||||
[LinkComponent],
|
||||
);
|
||||
|
||||
const { plaintext } = useKind4Decrypt(event);
|
||||
const content = useRenderedContent(event, componentsMap, { overrideContent: plaintext, transformers });
|
||||
const content = useRenderedContent(plaintext, components, { transformers, linkRenderers });
|
||||
|
||||
return (
|
||||
<TrustProvider event={event}>
|
||||
|
@ -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 && (
|
||||
|
@ -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}</>;
|
||||
});
|
||||
|
@ -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}`
|
||||
|
@ -13,7 +13,6 @@ import { TrustProvider } from "../../providers/local/trust-provider";
|
||||
import UserAvatar from "../../components/user/user-avatar";
|
||||
import UserLink from "../../components/user/user-link";
|
||||
import { EmbedEventPointer } from "../../components/embed-event";
|
||||
import { embedEmoji } from "../../components/external-embeds";
|
||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||
import NoteMenu from "../../components/note/note-menu";
|
||||
import useEventIntersectionRef from "../../hooks/use-event-intersection-ref";
|
||||
@ -32,7 +31,7 @@ const Reaction = ({ reaction: reaction }: { reaction: NostrEvent }) => {
|
||||
<UserAvatar pubkey={reaction.pubkey} size="xs" />
|
||||
<Text>
|
||||
<UserLink pubkey={reaction.pubkey} /> {reaction.content === "+" ? "liked " : "reacted with "}
|
||||
{embedEmoji([reaction.content], reaction)}
|
||||
{reaction.content}
|
||||
</Text>
|
||||
<Spacer />
|
||||
<NoteMenu event={reaction} aria-label="Note menu" variant="ghost" size="xs" />
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { ReactNode, useCallback, useMemo, useState } from "react";
|
||||
import { Box, Flex, Select, Text } from "@chakra-ui/react";
|
||||
import dayjs from "dayjs";
|
||||
import { useOutletContext } from "react-router-dom";
|
||||
import { Box, Flex, Select, Text } from "@chakra-ui/react";
|
||||
import { useRenderedContent } from "applesauce-react";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { ErrorBoundary } from "../../components/error-boundary";
|
||||
import { LightningIcon } from "../../components/icons";
|
||||
@ -16,13 +17,15 @@ import { useReadRelays } from "../../hooks/use-client-relays";
|
||||
import TimelineActionAndStatus from "../../components/timeline/timeline-action-and-status";
|
||||
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import { EmbedableContent, embedUrls } from "../../helpers/embeds";
|
||||
import { embedNostrLinks, renderGenericUrl } from "../../components/external-embeds";
|
||||
import Timestamp from "../../components/timestamp";
|
||||
import { EmbedEventPointer } from "../../components/embed-event";
|
||||
import { parseCoordinate } from "../../helpers/nostr/event";
|
||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||
import useEventIntersectionRef from "../../hooks/use-event-intersection-ref";
|
||||
import { components } from "../../components/content";
|
||||
import { renderGenericUrl } from "../../components/content/links/common";
|
||||
|
||||
const linkRenderers = [renderGenericUrl];
|
||||
|
||||
const Zap = ({ zapEvent }: { zapEvent: NostrEvent }) => {
|
||||
const ref = useEventIntersectionRef(zapEvent);
|
||||
@ -51,9 +54,7 @@ const Zap = ({ zapEvent }: { zapEvent: NostrEvent }) => {
|
||||
eventJSX = <EmbedEventPointer pointer={{ type: "note", data: eventId }} />;
|
||||
}
|
||||
|
||||
let embedContent: EmbedableContent = [request.content];
|
||||
embedContent = embedNostrLinks(embedContent);
|
||||
embedContent = embedUrls(embedContent, [renderGenericUrl]);
|
||||
const content = useRenderedContent(request, components, { linkRenderers });
|
||||
|
||||
return (
|
||||
<Box ref={ref}>
|
||||
@ -69,7 +70,7 @@ const Zap = ({ zapEvent }: { zapEvent: NostrEvent }) => {
|
||||
)}
|
||||
<Timestamp ml="auto" timestamp={request.created_at} />
|
||||
</Flex>
|
||||
{embedContent && <Box>{embedContent}</Box>}
|
||||
{content && <Box whiteSpace="pre">{content}</Box>}
|
||||
{eventJSX}
|
||||
</Box>
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user