From 8434c358ff7ebc61d2f3acd6adb23401e8fee971 Mon Sep 17 00:00:00 2001 From: zmeyer44 Date: Tue, 17 Oct 2023 14:25:26 -0400 Subject: [PATCH] create lists with subscriptions --- app/_providers/index.tsx | 14 +- app/_providers/signer/index.tsx | 111 +++++++++++++ components/Modals/CreateList.tsx | 126 +++++++++++++++ components/Modals/FormModal.tsx | 227 +++++++++++++++++++------- components/Modals/NewEvent.tsx | 8 +- components/Modals/Template.tsx | 2 +- components/ui/avatar.tsx | 28 ++-- lib/actions/create.ts | 267 +++++++++++++++++++++++++++++++ lib/actions/ephemeral.ts | 161 +++++++++++++++++++ lib/actions/zap.ts | 174 ++++++++++++++++++++ lib/nostr/index.ts | 150 +++++++++++++++++ lib/utils/index.ts | 3 + 12 files changed, 1188 insertions(+), 83 deletions(-) create mode 100644 app/_providers/signer/index.tsx create mode 100644 components/Modals/CreateList.tsx create mode 100644 lib/actions/create.ts create mode 100644 lib/actions/ephemeral.ts create mode 100644 lib/actions/zap.ts create mode 100644 lib/nostr/index.ts diff --git a/app/_providers/index.tsx b/app/_providers/index.tsx index 4d09648..6eec9f9 100644 --- a/app/_providers/index.tsx +++ b/app/_providers/index.tsx @@ -4,6 +4,7 @@ import { Toaster } from "sonner"; import { ModalProvider } from "./modal/provider"; import useRouteChange from "@/lib/hooks/useRouteChange"; import { NDKProvider } from "./ndk"; +import SignerProvider from "./signer"; import { RELAYS } from "@/constants"; export function Providers({ children }: { children: React.ReactNode }) { @@ -16,13 +17,12 @@ export function Providers({ children }: { children: React.ReactNode }) { useRouteChange(handleRouteChange); return ( <> - - - - - {children} + + + + + {children} + ); diff --git a/app/_providers/signer/index.tsx b/app/_providers/signer/index.tsx new file mode 100644 index 0000000..b45b511 --- /dev/null +++ b/app/_providers/signer/index.tsx @@ -0,0 +1,111 @@ +"use client"; +import { + createContext, + useContext, + useState, + useMemo, + type ReactNode, +} from "react"; +import useCurrentUser from "@/lib/hooks/useCurrentUser"; +import { findEphemeralSigner } from "@/lib/actions/ephemeral"; +import { + NDKPrivateKeySigner, + NDKUser, + type NDKSigner, + type NDKList, +} from "@nostr-dev-kit/ndk"; +import { useNDK } from "../ndk"; + +export type SignerStoreItem = { + signer: NDKPrivateKeySigner; + user: NDKUser; + saved: boolean; + title?: string; + id: string; +}; +type SignerItems = Map; + +type SignerContextProps = { + npubSigners: Map; + signers: SignerItems; + getSigner: (list: NDKList) => Promise; +}; +const SignerContext = createContext(undefined); + +export default function SignerProvider({ + children, +}: { + children?: ReactNode | undefined; +}) { + const { currentUser } = useCurrentUser(); + const { ndk } = useNDK(); + const [signers, setSigners] = useState(new Map()); + const npubSigners = useMemo(() => { + const npubs = new Map(); + for (const entry of signers) { + const { user, signer } = entry[1]; + npubs.set(user.npub, signer); + } + return npubs; + }, [signers]); + + async function getDelegatedSignerName(list: NDKList) { + let name = ""; + console.log("getDelegatedSignerName", list); + if (!currentUser?.profile) { + console.log("fetching user profile"); + await currentUser?.fetchProfile(); + } + name = `${ + currentUser?.profile?.displayName ?? + currentUser?.profile?.name ?? + currentUser?.profile?.nip05 ?? + `${currentUser?.npub.slice(0, 9)}...` + }'s `; + + return name + list.title ?? "List"; + } + + async function getSigner(list: NDKList): Promise { + const id = list.encode(); + let item = signers.get(id); + if (item) return item; + let signer = await findEphemeralSigner(ndk!, ndk!.signer!, { + associatedEventNip19: list.encode(), + }); + + if (signer) { + console.log(`found a signer for list ${list.title}`); + item = { + signer: signer!, + user: await signer.user(), + saved: true, + id, + }; + } else { + console.log(`no signer found for list ${list.title}`); + signer = NDKPrivateKeySigner.generate(); + console.log(`Signer generated ${JSON.stringify(signer)}`); + item = { + signer, + user: await signer.user(), + saved: false, + title: await getDelegatedSignerName(list), + id, + }; + } + item.user.ndk = ndk; + setSigners((prev) => new Map(prev.set(id, item))); + return item; + } + + return ( + + {children} + + ); +} + +export function useSigner() { + return useContext(SignerContext); +} diff --git a/components/Modals/CreateList.tsx b/components/Modals/CreateList.tsx new file mode 100644 index 0000000..1d75df8 --- /dev/null +++ b/components/Modals/CreateList.tsx @@ -0,0 +1,126 @@ +import { useState } from "react"; +import FormModal from "./FormModal"; +import { z } from "zod"; +import { useModal } from "@/app/_providers/modal/provider"; +import { toast } from "sonner"; +import { generateRandomString } from "@/lib/nostr"; +import { satsToBtc } from "@/lib/utils"; +import useCurrentUser from "@/lib/hooks/useCurrentUser"; +import { useNDK } from "@/app/_providers/ndk"; +import { useSigner, type SignerStoreItem } from "@/app/_providers/signer"; +import { createEvent } from "@/lib/actions/create"; +import { getTagValues } from "@/lib/nostr/utils"; +import { NDKList } from "@nostr-dev-kit/ndk"; +import { saveEphemeralSigner } from "@/lib/actions/ephemeral"; + +const CreateListSchema = z.object({ + title: z.string(), + image: z.string().optional(), + description: z.string().optional(), + subscriptions: z.boolean(), + price: z.number().optional(), +}); + +type CreateListType = z.infer; + +export default function CreateList() { + const [isLoading, setIsLoading] = useState(false); + const modal = useModal(); + + const { currentUser, updateUser } = useCurrentUser(); + const { ndk } = useNDK(); + const { getSigner } = useSigner()!; + async function handleSubmit(data: CreateListType) { + setIsLoading(true); + const random = generateRandomString(); + const tags = [ + ["title", data.title], + ["name", data.title], + ["d", random], + ]; + if (data.description) { + tags.push(["description", data.description]); + tags.push(["summary", data.description]); + } + if (data.image) { + tags.push(["image", data.image]); + tags.push(["picture", data.image]); + } + if (data.subscriptions) { + tags.push(["subscriptions", "true"]); + } + if (data.price) { + tags.push(["price", satsToBtc(data.price).toString(), "btc", "year"]); + } + const event = await createEvent(ndk!, { + content: "", + kind: 30001, + tags: tags, + }); + if (event && getTagValues("subscriptions", event.tags)) { + await getSigner(new NDKList(ndk, event.rawEvent())) + .then((delegateSigner) => + saveEphemeralSigner(ndk!, delegateSigner.signer, { + associatedEvent: event, + keyProfile: { + name: delegateSigner.title, + picture: currentUser?.profile?.image, + lud06: currentUser?.profile?.lud06, + lud16: currentUser?.profile?.lud16, + }, + }), + ) + .then( + (savedSigner) => savedSigner, + // updateList(ndk!, event.rawEvent(), [ + // ["delegate", savedSigner.hexpubkey], + // ]), + ) + .catch((err) => console.log("Error creating delegate")); + } + // getLists(currentUser!.hexpubkey); + setIsLoading(false); + toast.success("List Created!"); + modal?.hide(); + } + return ( + + ); +} diff --git a/components/Modals/FormModal.tsx b/components/Modals/FormModal.tsx index 4d4ad8c..d282a2e 100644 --- a/components/Modals/FormModal.tsx +++ b/components/Modals/FormModal.tsx @@ -9,6 +9,7 @@ import { FieldValues, DefaultValues, Path, + PathValue, } from "react-hook-form"; import { cn } from "@/lib/utils"; import Template from "./Template"; @@ -37,6 +38,7 @@ type FieldOptions = | "toggle" | "horizontal-tabs" | "input" + | "number" | "text-area" | "custom"; @@ -44,11 +46,13 @@ type DefaultFieldType = { label: string; slug: keyof z.infer> & string; placeholder?: string; + subtitle?: string; description?: string; lines?: number; styles?: string; value?: string | number | boolean; custom?: ReactNode; + condition?: keyof z.infer> & string; options?: { label: string; value: string; icon?: ReactNode }[]; }; type FieldType = DefaultFieldType & @@ -91,10 +95,11 @@ export default function FormModal({ defaultValues: defaultValues as DefaultValues, mode: "onChange", }); + const { watch, setValue } = form; return (