diff --git a/.env b/.env index 7d22136..81ec38e 100644 --- a/.env +++ b/.env @@ -1,7 +1,7 @@ # S3 data storage S3_BUCKET_NAME="flockstr" -MY_AWS_ACCESS_KEY="AKIAT2UDOJMC6K25RFFN" -MY_AWS_SECRET_KEY="secret" +MY_AWS_ACCESS_KEY="AKIAT2UDOJMC4RSXQO5Y" +MY_AWS_SECRET_KEY="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" REGION="us-east-1" S3_BUCKET_URL="https://flockstr.s3.amazonaws.com" NEXT_PUBLIC_S3_BUCKET_URL="https://flockstr.s3.amazonaws.com" \ No newline at end of file diff --git a/.gitignore b/.gitignore index a802636..45c1abc 100644 --- a/.gitignore +++ b/.gitignore @@ -26,7 +26,7 @@ yarn-error.log* # local env files .env*.local -# .env +.env # vercel .vercel diff --git a/app/(app)/app/_sections/LiveStreaming.tsx b/app/(app)/app/_sections/LiveStreaming.tsx index adf9b5f..21731e3 100644 --- a/app/(app)/app/_sections/LiveStreaming.tsx +++ b/app/(app)/app/_sections/LiveStreaming.tsx @@ -47,7 +47,6 @@ export default function LiveStreamingSection() { if (bParticipants) return 1; return -1; }); - console.log(processedEvents); return (
diff --git a/components/Modals/FormModal.tsx b/components/Modals/FormModal.tsx index 0fcbaab..f08ac52 100644 --- a/components/Modals/FormModal.tsx +++ b/components/Modals/FormModal.tsx @@ -228,9 +228,15 @@ export default function FormModal({ {fieldProps.options?.map((o) => ( - console.log("Captured", o.value) - } + onSelect={() => { + setValue( + field.name, + o.value as PathValue< + TSchema, + Path + >, + ); + }} className="teamaspace-y-1 flex flex-col items-start px-4 py-2" >

{o.label}

diff --git a/components/Modals/ShortTextNote.tsx b/components/Modals/ShortTextNote.tsx index 2039535..04e75ad 100644 --- a/components/Modals/ShortTextNote.tsx +++ b/components/Modals/ShortTextNote.tsx @@ -2,17 +2,19 @@ import { useEffect, useState } from "react"; import FormModal from "./FormModal"; import { z } from "zod"; import useEvents from "@/lib/hooks/useEvents"; -import { updateList } from "@/lib/actions/create"; +import { createEventHandler } from "@/lib/actions/create"; import { unixTimeNowInSeconds } from "@/lib/nostr/dates"; import { useModal } from "@/app/_providers/modal/provider"; import { toast } from "sonner"; import { useNDK } from "@/app/_providers/ndk"; -import { NostrEvent } from "@nostr-dev-kit/ndk"; +import { useSigner, type SignerStoreItem } from "@/app/_providers/signer"; import { getTagValues } from "@/lib/nostr/utils"; +import useCurrentUser from "@/lib/hooks/useCurrentUser"; +import { saveEphemeralSigner } from "@/lib/actions/ephemeral"; +import useLists from "@/lib/hooks/useLists"; const ShortTextNoteSchema = z.object({ content: z.string(), - image: z.string().optional(), list: z.string().optional(), isPrivate: z.boolean().optional(), }); @@ -21,18 +23,19 @@ type ShortTextNoteType = z.infer; export default function ShortTextNoteModal() { const modal = useModal(); + const { lists, init } = useLists(); const [isLoading, setIsLoading] = useState(false); + const { currentUser } = useCurrentUser(); const [sent, setSent] = useState(false); const { ndk } = useNDK(); - // const { events } = useEvents({ - // filter: { - // kinds: [listEvent.kind as number], - // authors: [listEvent.pubkey], - // since: unixTimeNowInSeconds() - 10, - // limit: 1, - // }, - // enabled: sent, - // }); + const { getSigner } = useSigner()!; + + useEffect(() => { + if (currentUser) { + void init(currentUser.pubkey); + } + }, [currentUser]); + // useEffect(() => { // if (events.length) { // console.log("Done!"); @@ -42,11 +45,69 @@ export default function ShortTextNoteModal() { // } // }, [events]); - async function handleSubmit(listData: ShortTextNoteType) { + async function handleSubmit(data: ShortTextNoteType) { setIsLoading(true); - const newTags = Object.entries(listData); - setSent(true); - // const result = await updateList(ndk!, listEvent, newTags); + if (!ndk) { + toast.error("Error connecting"); + return; + } + if (data.list) { + const list = lists.find((l) => getTagValues("d", l.tags) === data.list); + if (!list) { + toast.error("No list found"); + return; + } + let listSigner: SignerStoreItem | undefined = undefined; + if (data.isPrivate) { + listSigner = await getSigner(list); + if (!listSigner?.signer) { + toast.error("Error creating signer"); + return; + } + if (!listSigner?.saved) { + console.log("Saving delegate..."); + await saveEphemeralSigner(ndk!, listSigner.signer, { + associatedEvent: list, + keyProfile: { + name: listSigner.title, + picture: currentUser?.profile?.image, + lud06: currentUser?.profile?.lud06, + lud16: currentUser?.profile?.lud16, + }, + }); + } + } + + const result = await createEventHandler( + ndk, + { + content: data.content, + kind: 1, + tags: [], + }, + data.isPrivate, + list, + listSigner?.signer, + ); + if (result) { + toast.success("Note added!"); + modal?.hide(); + } + } else { + const result = await createEventHandler( + ndk, + { + content: data.content, + kind: 1, + tags: [], + }, + data.isPrivate, + ); + if (result) { + toast.success("Note added!"); + modal?.hide(); + } + } } return ( @@ -63,20 +124,23 @@ export default function ShortTextNoteModal() { type: "select-search", placeholder: "Search your lists", slug: "list", - options: [ - { - label: "Spicy Takes 🌶️", - value: "325grg ", - }, - { - label: "Public reading list", - value: "grherh ", - }, - { - label: "Radnosm other", - value: "grhfaggferh ", - }, - ], + options: lists + .map((l) => { + console.log("MApping", l); + const title = + getTagValues("title", l.tags) ?? + getTagValues("name", l.tags) ?? + "Untitled"; + const description = getTagValues("description", l.tags); + const value = getTagValues("d", l.tags); + if (!value) return; + return { + label: title, + description, + value: value, + }; + }) + .filter(Boolean), }, { label: "Private", diff --git a/lib/actions/create.ts b/lib/actions/create.ts index c1866db..4e0bcfd 100644 --- a/lib/actions/create.ts +++ b/lib/actions/create.ts @@ -41,6 +41,90 @@ export async function createEvent( return false; } } +export async function createEventHandler( + ndk: NDK, + event: { + content: string; + kind: number; + tags: string[][]; + }, + isPrivate?: boolean, + list?: NDKList, + delegateSigner?: NDKPrivateKeySigner, +) { + const pubkey = await window.nostr?.getPublicKey(); + if (!pubkey || !window.nostr) { + throw new Error("No public key provided!"); + } + const eventToPublish = new NDKEvent(ndk, { + ...event, + tags: [...event.tags, ["client", "ordstr"]], + pubkey, + created_at: unixTimeNowInSeconds(), + } as NostrEvent); + + await eventToPublish.sign(); + + let publishedEvent: NDKEvent | null = null; + // Check if is private event + if (isPrivate) { + const rawEventString = JSON.stringify(eventToPublish.rawEvent()); + const passphrase = generateRandomString(); + const encryptedRawEventString = await encryptMessage( + rawEventString, + passphrase, + ); + const signer = delegateSigner ?? ndk.signer!; + const user = await signer.user(); + const newEvent = new NDKEvent(ndk, { + content: encryptedRawEventString, + kind: 3745, + tags: [ + ["kind", event.kind.toString()], + ["client", "ordstr"], + ], + pubkey: user.pubkey, + } as NostrEvent); + await newEvent.sign(signer); + await newEvent.publish(); + + if (list) { + // Send DMs to subscribers + const subscribers = getTagsValues("p", list.tags); + for (const subscriber of subscribers) { + const messageEvent = new NDKEvent(ndk, { + content: passphrase, + kind: 4, + tags: [ + ["p", subscriber], + ["e", newEvent.id], + ["client", "ordstr"], + ], + pubkey: user.pubkey, + } as NostrEvent); + await messageEvent.encrypt( + new NDKUser({ hexpubkey: subscriber }), + signer, + ); + await messageEvent.sign(signer); + await messageEvent.publish(); + } + } + publishedEvent = newEvent; + } else { + await eventToPublish.publish(); + publishedEvent = eventToPublish; + } + if (list) { + const tag = publishedEvent.tagReference(); + if (!tag) return; + // Add event to list + await list.addItem(tag, undefined, false); + await list.sign(); + await list.publish(); + } + return true; +} export async function createEncryptedEventOnPrivateList( ndk: NDK, event: { @@ -64,7 +148,6 @@ export async function createEncryptedEventOnPrivateList( await eventToPublish.sign(); const rawEventString = JSON.stringify(eventToPublish.rawEvent()); const passphrase = generateRandomString(); - // const passphrase = "test"; const encryptedRawEventString = await encryptMessage( rawEventString, passphrase, @@ -79,7 +162,7 @@ export async function createEncryptedEventOnPrivateList( ["kind", event.kind.toString()], ["client", "ordstr"], ], - pubkey: user.hexpubkey, + pubkey: user.pubkey, } as NostrEvent); await newEvent.sign(signer); diff --git a/lib/hooks/useCurrentUser.ts b/lib/hooks/useCurrentUser.ts index c923ffe..95f9fef 100644 --- a/lib/hooks/useCurrentUser.ts +++ b/lib/hooks/useCurrentUser.ts @@ -5,7 +5,7 @@ import currentUserStore from "@/lib/stores/currentUser"; import { UserSchema } from "@/types"; import { useNDK } from "@/app/_providers/ndk"; import { nip19 } from "nostr-tools"; - +import useLists from "./useLists"; export default function useCurrentUser() { const { currentUser, @@ -15,30 +15,7 @@ export default function useCurrentUser() { follows, } = currentUserStore(); const { loginWithNip07, getProfile, ndk } = useNDK(); - - // const { - // events: contactList, - // isLoading, - // onEvent, - // } = useEvents({ - // filter: { - // kinds: [3], - // authors: [currentUser?.pubkey ?? ""], - // limit: 1, - // }, - // enabled: !!currentUser, - // }); - // onEvent((event) => { - // console.log("EVENT", event); - // const foundFollows = event.tags - // .filter(([key]) => key === "p") - // .map(([key, pubkey]) => pubkey); - // console.log("Found follows", foundFollows); - // if (follows.length !== foundFollows.length) { - // setFollows(follows); - // } - // }); - + const { init } = useLists(); async function attemptLogin() { try { const shouldReconnect = localStorage.getItem("shouldReconnect"); @@ -86,6 +63,7 @@ export default function useCurrentUser() { console.log("user", user); await user.fetchProfile(); setCurrentUser(user); + void init(user.pubkey); } return { diff --git a/lib/hooks/useLists.ts b/lib/hooks/useLists.ts new file mode 100644 index 0000000..2c6ce60 --- /dev/null +++ b/lib/hooks/useLists.ts @@ -0,0 +1,33 @@ +"use client"; + +import listsStore from "@/lib/stores/lists"; +import { useNDK } from "@/app/_providers/ndk"; +import { useState } from "react"; +import { NDKList } from "@nostr-dev-kit/ndk"; + +export default function useLists() { + const [isLoading, setIsLoading] = useState(false); + const { lists, setLists, follows } = listsStore(); + const { fetchEvents, ndk } = useNDK(); + async function init(pubkey: string) { + setIsLoading(true); + try { + const listEvents = await fetchEvents({ + kinds: [30001], + authors: [pubkey], + }); + setLists(listEvents.map((l) => new NDKList(ndk, l.rawEvent()))); + } catch (err) { + console.log("error in init", err); + } finally { + setIsLoading(false); + } + } + + return { + lists, + isLoading, + init, + follows, + }; +} diff --git a/lib/stores/lists.ts b/lib/stores/lists.ts new file mode 100644 index 0000000..25b6c1d --- /dev/null +++ b/lib/stores/lists.ts @@ -0,0 +1,18 @@ +import { create } from "zustand"; +import { type NDKList } from "@nostr-dev-kit/ndk"; + +interface CurrentUserState { + lists: NDKList[]; + follows: string[]; + setLists: (lists: NDKList[]) => void; + setFollows: (follows: string[]) => void; +} + +const listsStore = create()((set) => ({ + lists: [], + follows: [], + setLists: (lists) => set((state) => ({ ...state, lists: lists })), + setFollows: (follows) => set((state) => ({ ...state, follows: follows })), +})); + +export default listsStore;