mirror of
https://gitlab.com/soapbox-pub/mkstack.git
synced 2025-08-26 12:39:22 +00:00
Add custom Nostr hooks, improve goosehints
This commit is contained in:
parent
e2a0d55170
commit
afcb62e7a1
2
.gitignore
vendored
2
.gitignore
vendored
@ -13,8 +13,6 @@ dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
|
61
.goosehints
61
.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<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
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"editor.tabSize": 2
|
||||
}
|
32
src/hooks/useAuthor.ts
Normal file
32
src/hooks/useAuthor.ts
Normal 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 };
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
48
src/hooks/useCurrentUser.ts
Normal file
48
src/hooks/useCurrentUser.ts
Normal 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,
|
||||
};
|
||||
}
|
22
src/hooks/useLoginActions.ts
Normal file
22
src/hooks/useLoginActions.ts
Normal 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);
|
||||
},
|
||||
};
|
||||
}
|
38
src/hooks/useNostrPublish.ts
Normal file
38
src/hooks/useNostrPublish.ts
Normal 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);
|
||||
},
|
||||
});
|
||||
}
|
@ -16,6 +16,7 @@
|
||||
|
||||
/* Linting */
|
||||
"strict": false,
|
||||
"strictNullChecks": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noImplicitAny": false,
|
||||
|
@ -14,6 +14,6 @@
|
||||
"skipLibCheck": true,
|
||||
"allowJs": true,
|
||||
"noUnusedLocals": false,
|
||||
"strictNullChecks": false
|
||||
"strictNullChecks": true
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user