Add custom Nostr hooks, improve goosehints

This commit is contained in:
Alex Gleason 2025-04-17 11:23:46 -05:00
parent e2a0d55170
commit afcb62e7a1
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7
9 changed files with 193 additions and 16 deletions

2
.gitignore vendored
View File

@ -13,8 +13,6 @@ dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo

View File

@ -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<Record<string, string>>({});
const { user } = useCurrentUser();
const { mutate: createEvent } = useNostrPublish();
const handleSubmit = () => {
createEvent({ kind: 1, content: data.content });
};
if (!user) {
return <span>You must be logged in to use this form.</span>;
}
return (
<form onSubmit={handleSubmit} disabled={!user}>
{/* ...some input fields */}
</form>
);
}
```
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

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"editor.tabSize": 2
}

32
src/hooks/useAuthor.ts Normal file
View File

@ -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 };
}
},
});
}

View File

@ -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,
};
}

View File

@ -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<void> {
const login = await NLogin.fromBunker(uri, nostr);
addLogin(login);
},
async extension(): Promise<void> {
const login = await NLogin.fromExtension();
addLogin(login);
},
};
}

View File

@ -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);
},
});
}

View File

@ -16,6 +16,7 @@
/* Linting */
"strict": false,
"strictNullChecks": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noImplicitAny": false,

View File

@ -14,6 +14,6 @@
"skipLibCheck": true,
"allowJs": true,
"noUnusedLocals": false,
"strictNullChecks": false
"strictNullChecks": true
}
}