From afcb62e7a1f73e65609049b2007481af6e19b8f4 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 17 Apr 2025 11:23:46 -0500 Subject: [PATCH] Add custom Nostr hooks, improve goosehints --- .gitignore | 2 -- .goosehints | 61 ++++++++++++++++++++++++++++-------- .vscode/settings.json | 3 ++ src/hooks/useAuthor.ts | 32 +++++++++++++++++++ src/hooks/useCurrentUser.ts | 48 ++++++++++++++++++++++++++++ src/hooks/useLoginActions.ts | 22 +++++++++++++ src/hooks/useNostrPublish.ts | 38 ++++++++++++++++++++++ tsconfig.app.json | 1 + tsconfig.json | 2 +- 9 files changed, 193 insertions(+), 16 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/hooks/useAuthor.ts create mode 100644 src/hooks/useCurrentUser.ts create mode 100644 src/hooks/useLoginActions.ts create mode 100644 src/hooks/useNostrPublish.ts diff --git a/.gitignore b/.gitignore index a547bf3..8b7e502 100644 --- a/.gitignore +++ b/.gitignore @@ -13,8 +13,6 @@ dist-ssr *.local # Editor directories and files -.vscode/* -!.vscode/extensions.json .idea .DS_Store *.suo diff --git a/.goosehints b/.goosehints index 1f4176f..a6af7fd 100644 --- a/.goosehints +++ b/.goosehints @@ -23,26 +23,25 @@ This project is a Nostr client application built with React 19.x, TailwindCSS 3. ## Nostr Protocol Integration -The best Nostr library is Nostrify. Use this for all Nostr event and relay functions: https://nostrify.dev/ +This project comes with custom hooks for querying and publishing events on the Nostr network. -Nostrify is a flexible library for building Nostr apps in TypeScript. It provides Relays, Signers, Storages, and more to help you build your app. +### The `useNostr` Hook -Please read this LLM context for Nostrify: https://nostrify.dev/llms.txt - -### Nostrify Import Examples +The `useNostr` hook returns an object containing a `nostr` property, with `.query()` and `.event()` methods for querying and publishing Nostr events respectively. ```typescript -import { - NPool, - NRelay1, - NSchema as n, -} from '@nostrify/nostrify'; +import { useNostr } from '@nostrify/react'; -import { useNostr, NostrContext } from '@nostrify/react'; -import { useNostrLogin, NUser, NLogin, NostrLoginProvider } from '@nostrify/react/login'; +function useCustomHook() { + const { nostr } = useNostr(); + + // ... +} ``` -### Nostrify Example Usage in a Hook +### Query Nostr Data with `useNostr` and Tanstack Query + +When querying Nostr, the best practice is to create custom hooks that combine `useNostr` and `useQuery` to get the required data. ```typescript import { useNostr } from '@nostrify/react'; @@ -61,6 +60,42 @@ function usePosts() { } ``` +The data may be transformed into a more appropriate format if needed, and multiple calls to `nostr.query()` may be made in a single queryFn. + +### The `useNostrPublish` Hook + +To publish events, use the `useNostrPublish` hook in this project. + +```tsx +import { useState } from 'react'; + +import { useCurrentUser } from "@/hooks/useCurrentUser"; +import { useNostrPublish } from '@/hooks/useNostrPublish'; + +export function MyComponent() { + const [ data, setData] = useState>({}); + + const { user } = useCurrentUser(); + const { mutate: createEvent } = useNostrPublish(); + + const handleSubmit = () => { + createEvent({ kind: 1, content: data.content }); + }; + + if (!user) { + return You must be logged in to use this form.; + } + + return ( +
+ {/* ...some input fields */} +
+ ); +} +``` + +The `useCurrentUser` hook should be used to ensure that the user is logged in before they are able to publish Nostr events. + ## Development Practices - Uses React Query for data fetching and caching diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0a77011 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.tabSize": 2 +} \ No newline at end of file diff --git a/src/hooks/useAuthor.ts b/src/hooks/useAuthor.ts new file mode 100644 index 0000000..1d85e7a --- /dev/null +++ b/src/hooks/useAuthor.ts @@ -0,0 +1,32 @@ +import { type NostrEvent, type NostrMetadata, NSchema as n } from '@nostrify/nostrify'; +import { useNostr } from '@nostrify/react'; +import { useQuery } from '@tanstack/react-query'; + +export function useAuthor(pubkey: string | undefined) { + const { nostr } = useNostr(); + + return useQuery<{ event?: NostrEvent; metadata?: NostrMetadata }>({ + queryKey: ['author', pubkey ?? ''], + queryFn: async ({ signal }) => { + if (!pubkey) { + return {}; + } + + const [event] = await nostr.query( + [{ kinds: [0], authors: [pubkey!], limit: 1 }], + { signal: AbortSignal.any([signal, AbortSignal.timeout(500)]) }, + ); + + if (!event) { + return {}; + } + + try { + const metadata = n.json().pipe(n.metadata()).parse(event.content); + return { metadata, event }; + } catch { + return { event }; + } + }, + }); +} diff --git a/src/hooks/useCurrentUser.ts b/src/hooks/useCurrentUser.ts new file mode 100644 index 0000000..d98513d --- /dev/null +++ b/src/hooks/useCurrentUser.ts @@ -0,0 +1,48 @@ +import { type NLoginType, NUser, useNostrLogin } from '@nostrify/react/login'; +import { useNostr } from '@nostrify/react'; +import { useCallback, useMemo } from 'react'; + +import { useAuthor } from './useAuthor.ts'; + +export function useCurrentUser() { + const { nostr } = useNostr(); + const { logins } = useNostrLogin(); + + const loginToUser = useCallback((login: NLoginType): NUser => { + switch (login.type) { + case 'nsec': + return NUser.fromNsecLogin(login); + case 'bunker': + return NUser.fromBunkerLogin(login, nostr); + case 'extension': + return NUser.fromExtensionLogin(login); + default: + // Learn how to define other login types: https://nostrify.dev/react/logins#custom-login-types + throw new Error(`Unsupported login type: ${login.type}`); + } + }, [nostr]); + + const users = useMemo(() => { + const users: NUser[] = []; + + for (const login of logins) { + try { + const user = loginToUser(login); + users.push(user); + } catch (error) { + console.warn('Skipped invalid login', login.id, error); + } + } + + return users; + }, [logins, loginToUser]); + + const user = users[0] as NUser | undefined; + const data = useAuthor(user?.pubkey); + + return { + user, + data, + users, + }; +} diff --git a/src/hooks/useLoginActions.ts b/src/hooks/useLoginActions.ts new file mode 100644 index 0000000..fcb4a9b --- /dev/null +++ b/src/hooks/useLoginActions.ts @@ -0,0 +1,22 @@ +import { useNostr } from '@nostrify/react'; +import { NLogin, useNostrLogin } from '@nostrify/react/login'; + +export function useLoginActions() { + const { nostr } = useNostr(); + const { addLogin } = useNostrLogin(); + + return { + nsec(nsec: string): void { + const login = NLogin.fromNsec(nsec); + addLogin(login); + }, + async bunker(uri: string): Promise { + const login = await NLogin.fromBunker(uri, nostr); + addLogin(login); + }, + async extension(): Promise { + const login = await NLogin.fromExtension(); + addLogin(login); + }, + }; +} diff --git a/src/hooks/useNostrPublish.ts b/src/hooks/useNostrPublish.ts new file mode 100644 index 0000000..d36ada4 --- /dev/null +++ b/src/hooks/useNostrPublish.ts @@ -0,0 +1,38 @@ +import { useNostr } from "@nostrify/react"; +import { useMutation } from "@tanstack/react-query"; + +import { useCurrentUser } from "./useCurrentUser"; + +interface EventTemplate { + kind: number; + content?: string; + tags?: string[][]; + created_at?: number; +} + +export function useNostrPublish() { + const { nostr } = useNostr(); + const { user } = useCurrentUser(); + + return useMutation({ + mutationFn: async (t: EventTemplate) => { + if (user) { + const event = await user.signer.signEvent({ + kind: t.kind, + content: t.content ?? "", + tags: t.tags ?? [], + created_at: t.created_at ?? Math.floor(Date.now() / 1000), + }); + nostr.event(event); + } else { + throw new Error("User is not logged in"); + } + }, + onError: (error) => { + console.error("Failed to publish event:", error); + }, + onSuccess: (data) => { + console.log("Event published successfully:", data); + }, + }); +} \ No newline at end of file diff --git a/tsconfig.app.json b/tsconfig.app.json index 0b0e43e..26ef794 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -16,6 +16,7 @@ /* Linting */ "strict": false, + "strictNullChecks": true, "noUnusedLocals": false, "noUnusedParameters": false, "noImplicitAny": false, diff --git a/tsconfig.json b/tsconfig.json index 129b1a3..2ea8a61 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,6 +14,6 @@ "skipLibCheck": true, "allowJs": true, "noUnusedLocals": false, - "strictNullChecks": false + "strictNullChecks": true } }