From e9efa336ebf93d281b329f63b2fed1a74eeb6526 Mon Sep 17 00:00:00 2001 From: hzrd149 Date: Wed, 16 Oct 2024 22:08:06 +0100 Subject: [PATCH] show fedimint tokens in content --- .changeset/curvy-mice-return.md | 5 +++ .prettierignore | 1 + package.json | 1 + pnpm-lock.yaml | 3 ++ src/components/cashu/inline-cashu-card.tsx | 14 +++--- src/components/content/index.tsx | 3 ++ .../fedimint/inline-fedimint-card.tsx | 43 +++++++++++++++++++ .../note/timeline-note/text-note-contents.tsx | 26 ++--------- src/helpers/fedimint.ts | 33 ++++++++++++++ src/hooks/use-trusted-mints.ts | 15 +++++++ src/queries/trusted-mints.ts | 43 +++++++++++++++++++ .../dms/components/direct-message-content.tsx | 5 ++- 12 files changed, 161 insertions(+), 31 deletions(-) create mode 100644 .changeset/curvy-mice-return.md create mode 100644 src/components/fedimint/inline-fedimint-card.tsx create mode 100644 src/helpers/fedimint.ts create mode 100644 src/hooks/use-trusted-mints.ts create mode 100644 src/queries/trusted-mints.ts diff --git a/.changeset/curvy-mice-return.md b/.changeset/curvy-mice-return.md new file mode 100644 index 000000000..a714aed1d --- /dev/null +++ b/.changeset/curvy-mice-return.md @@ -0,0 +1,5 @@ +--- +"nostrudel": minor +--- + +Show fedimint tokens in content diff --git a/.prettierignore b/.prettierignore index ae3de1f23..a247dada0 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,3 +2,4 @@ node_modules dist public/lib stats.html +pnpm-lock.yaml diff --git a/package.json b/package.json index 21429125d..3f62134a1 100644 --- a/package.json +++ b/package.json @@ -105,6 +105,7 @@ "three-spritetext": "^1.8.2", "three-stdlib": "^2.29.11", "tiny-lru": "^11.2.11", + "unified": "^11.0.5", "webln": "^0.3.2", "workbox-core": "7.0.0", "workbox-precaching": "7.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a8276ff61..b4d7cb5b0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -279,6 +279,9 @@ importers: tiny-lru: specifier: ^11.2.11 version: 11.2.11 + unified: + specifier: ^11.0.5 + version: 11.0.5 webln: specifier: ^0.3.2 version: 0.3.2 diff --git a/src/components/cashu/inline-cashu-card.tsx b/src/components/cashu/inline-cashu-card.tsx index 4e118639d..1652b89ba 100644 --- a/src/components/cashu/inline-cashu-card.tsx +++ b/src/components/cashu/inline-cashu-card.tsx @@ -1,5 +1,5 @@ import { useAsync } from "react-use"; -import { Box, Button, ButtonGroup, Card, CardProps, Heading, IconButton, Link, Text } from "@chakra-ui/react"; +import { Box, Button, ButtonGroup, Card, CardProps, Heading, IconButton, Link, Spinner, Text } from "@chakra-ui/react"; import { Token, getEncodedToken } from "@cashu/cashu-ts"; import { CopyIconButton } from "../copy-icon-button"; @@ -9,7 +9,6 @@ import { ECashIcon, WalletIcon } from "../icons"; import { getMint } from "../../services/cashu-mints"; import CurrencyDollar from "../icons/currency-dollar"; import CurrencyEthereum from "../icons/currency-ethereum"; -import CurrencyRupeeCircle from "../icons/currency-rupee-circle"; import CurrencyEuro from "../icons/currency-euro"; import CurrencyYen from "../icons/currency-yen"; import CurrencyPound from "../icons/currency-pound"; @@ -38,14 +37,14 @@ export default function InlineCachuCard({ const account = useCurrentAccount(); encoded = encoded || getEncodedToken(token); - const { value: spendable } = useAsync(async () => { + 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 === "SPENT")) return false; + if (spent.states.some((v) => v.state === "UNSPENT")) return true; } - return true; + return false; }, [token]); const amount = token?.token[0].proofs.reduce((acc, v) => acc + v.amount, 0); @@ -98,17 +97,18 @@ export default function InlineCachuCard({ icon={} title="Open Wallet" aria-label="Open Wallet" - href={`cashu://` + token} + href={`cashu://` + encoded} /> {account && } - {denomination} {spendable === false ? " (Spent)" : ""} + {denomination} {spendable === false ? " (Spent)" : loading ? : undefined} {token && Mint: {new URL(token.token[0].mint).hostname}} {token.unit && Unit: {token.unit}} {token.memo && {token.memo}} + {loading && } ); } diff --git a/src/components/content/index.tsx b/src/components/content/index.tsx index 72805367e..4ba9295aa 100644 --- a/src/components/content/index.tsx +++ b/src/components/content/index.tsx @@ -1,11 +1,14 @@ +import { lazy } from "react"; import { Text } from "@chakra-ui/react"; import { ComponentMap } from "applesauce-react"; import Mention from "./mention"; import Cashu from "./cashu"; +const InlineFedimintCard = lazy(() => import("../fedimint/inline-fedimint-card")); export const components: ComponentMap = { text: ({ node }) => {node.value}, mention: Mention, cashu: Cashu, + fedimint: ({ node }) => , }; diff --git a/src/components/fedimint/inline-fedimint-card.tsx b/src/components/fedimint/inline-fedimint-card.tsx new file mode 100644 index 000000000..455cec699 --- /dev/null +++ b/src/components/fedimint/inline-fedimint-card.tsx @@ -0,0 +1,43 @@ +import { Button, ButtonGroup, Card, CardProps, Heading, Link } from "@chakra-ui/react"; + +import { CopyIconButton } from "../copy-icon-button"; +import useCurrentAccount from "../../hooks/use-current-account"; +import { ECashIcon, WalletIcon } from "../icons"; + +export default function InlineFedimintCard({ token, ...props }: Omit & { token: string }) { + const account = useCurrentAccount(); + + // const { value: amount } = useAsync(async () => { + // const { FedimintWallet } = await import("@fedimint/core-web"); + // const wallet = new FedimintWallet(); + // const opened = await wallet.open("noStrudel"); + // if (opened) { + // return await wallet.mint.parseNotes(token); + // } + // }, []); + + let UnitIcon = ECashIcon; + let unitColor = "green.500"; + + return ( + + + + ecash + + + + + + + ); +} diff --git a/src/components/note/timeline-note/text-note-contents.tsx b/src/components/note/timeline-note/text-note-contents.tsx index 4be898888..29e59ddfa 100644 --- a/src/components/note/timeline-note/text-note-contents.tsx +++ b/src/components/note/timeline-note/text-note-contents.tsx @@ -28,29 +28,9 @@ 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"; -// function buildContents(event: NostrEvent | EventTemplate, simpleLinks = false) { -// let content: EmbedableContent = [event.content.trim()]; - -// // image gallery -// content = embedImageGallery(content, event as NostrEvent); - -// // bitcoin -// content = embedLightningInvoice(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); - -// return content; -// } +const transformers = [FedimintTokensTransformer]; export type TextNoteContentsProps = { event: NostrEvent | EventTemplate; @@ -98,7 +78,7 @@ export const TextNoteContents = React.memo( [LinkComponent], ); - const content = useRenderedContent(event, componentsMap); + const content = useRenderedContent(event, componentsMap, { transformers }); return ( diff --git a/src/helpers/fedimint.ts b/src/helpers/fedimint.ts new file mode 100644 index 000000000..3bbd6ddde --- /dev/null +++ b/src/helpers/fedimint.ts @@ -0,0 +1,33 @@ +import { Transformer } from "unified"; +import { Root, findAndReplace, Node } from "applesauce-content/nast"; + +declare module "applesauce-content/nast" { + export interface FedimintToken extends Node { + type: "fedimint"; + token: string; + } + + export interface ContentMap { + fedimint: FedimintToken; + } +} + +export function FedimintTokensTransformer(): Transformer { + return (tree) => { + findAndReplace(tree, [ + [ + /([A-Za-z0-9_+/-]{100,10000}={0,3})/gi, + (_: string, $1: string) => { + try { + return { + type: "fedimint", + token: $1, + }; + } catch (error) {} + + return false; + }, + ], + ]); + }; +} diff --git a/src/hooks/use-trusted-mints.ts b/src/hooks/use-trusted-mints.ts new file mode 100644 index 000000000..ebb0c0884 --- /dev/null +++ b/src/hooks/use-trusted-mints.ts @@ -0,0 +1,15 @@ +import { useEffect } from "react"; + +import replaceableEventsService from "../services/replaceable-events"; +import { useReadRelays } from "./use-client-relays"; +import { useStoreQuery } from "applesauce-react"; +import TrustedMintsQuery from "../queries/trusted-mints"; + +export default function useTrustedMints(pubkey?: string) { + const relays = useReadRelays(); + useEffect(() => { + if (pubkey) replaceableEventsService.requestEvent(relays, 10019, pubkey); + }, [pubkey, relays]); + + return useStoreQuery(TrustedMintsQuery, pubkey ? [pubkey] : undefined); +} diff --git a/src/queries/trusted-mints.ts b/src/queries/trusted-mints.ts new file mode 100644 index 000000000..1ea082ef1 --- /dev/null +++ b/src/queries/trusted-mints.ts @@ -0,0 +1,43 @@ +import { Query } from "applesauce-core"; +import { safeRelayUrl } from "applesauce-core/helpers"; + +type TrustedMints = { + mints: string[]; + relays: Set; + pubkey?: string; +}; + +export default function TrustedMintsQuery(pubkey: string): Query { + return { + key: pubkey, + run: (events) => + events.replaceable(10019, pubkey).map((event) => { + if (!event) return undefined; + + const relays = new Set(); + const mints: string[] = []; + let pubkey: string | undefined = undefined; + + for (const tag of event.tags) { + switch (tag[0]) { + case "mint": + if (tag[1]) mints.push(tag[1]); + break; + + case "relay": + if (tag[1]) { + let safe = safeRelayUrl(tag[1]); + if (safe) mints.push(safe); + } + break; + + case "pubkey": + if (tag[1]) pubkey = tag[1]; + break; + } + } + + return { relays, mints, pubkey }; + }), + }; +} diff --git a/src/views/dms/components/direct-message-content.tsx b/src/views/dms/components/direct-message-content.tsx index 6ddf0e614..ec86abdbf 100644 --- a/src/views/dms/components/direct-message-content.tsx +++ b/src/views/dms/components/direct-message-content.tsx @@ -26,6 +26,9 @@ import { renderAudioUrl } from "../../../components/external-embeds/types/audio" import buildLinkComponent from "../../../components/content/links"; import { components } from "../../../components/content"; import { useKind4Decrypt } from "../../../hooks/use-kind4-decryption"; +import { FedimintTokensTransformer } from "../../../helpers/fedimint"; + +const transformers = [FedimintTokensTransformer]; export default function DirectMessageContent({ event, @@ -64,7 +67,7 @@ export default function DirectMessageContent({ ); const { plaintext } = useKind4Decrypt(event); - const content = useRenderedContent(event, componentsMap, plaintext); + const content = useRenderedContent(event, componentsMap, { overrideContent: plaintext, transformers }); return (