mirror of
https://gitlab.com/soapbox-pub/mkstack.git
synced 2025-08-26 20:49: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
|
*.local
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea
|
.idea
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.suo
|
*.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
|
## 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
|
The `useNostr` hook returns an object containing a `nostr` property, with `.query()` and `.event()` methods for querying and publishing Nostr events respectively.
|
||||||
|
|
||||||
### Nostrify Import Examples
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import {
|
import { useNostr } from '@nostrify/react';
|
||||||
NPool,
|
|
||||||
NRelay1,
|
|
||||||
NSchema as n,
|
|
||||||
} from '@nostrify/nostrify';
|
|
||||||
|
|
||||||
import { useNostr, NostrContext } from '@nostrify/react';
|
function useCustomHook() {
|
||||||
import { useNostrLogin, NUser, NLogin, NostrLoginProvider } from '@nostrify/react/login';
|
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
|
```typescript
|
||||||
import { useNostr } from '@nostrify/react';
|
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
|
## Development Practices
|
||||||
|
|
||||||
- Uses React Query for data fetching and caching
|
- 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 */
|
/* Linting */
|
||||||
"strict": false,
|
"strict": false,
|
||||||
|
"strictNullChecks": true,
|
||||||
"noUnusedLocals": false,
|
"noUnusedLocals": false,
|
||||||
"noUnusedParameters": false,
|
"noUnusedParameters": false,
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
|
@ -14,6 +14,6 @@
|
|||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"noUnusedLocals": false,
|
"noUnusedLocals": false,
|
||||||
"strictNullChecks": false
|
"strictNullChecks": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user