add nostr-react

This commit is contained in:
2025-05-28 22:57:27 +02:00
parent 354f22f463
commit 15de65af9d
6 changed files with 379 additions and 107 deletions

159
.github/prompts/nostr-react.md vendored Normal file
View File

@@ -0,0 +1,159 @@
<p align="center">
<b>nostr-react</b>
</p>
<p align="center">
React Hooks for Nostr ✨
</p>
## Installation
```
npm install nostr-react
```
## Example usage:
Wrap your app in the NostrProvider:
```tsx
import { NostrProvider } from "nostr-react";
const relayUrls = [
"wss://nostr-pub.wellorder.net",
"wss://relay.nostr.ch",
];
function MyApp() {
return (
<NostrProvider relayUrls={relayUrls} debug={true}>
<App />
</NostrProvider>
);
};
```
You can now use the `useNostr` and `useNostrEvents` hooks in your components!
**Fetching all `text_note` events starting now:**
```tsx
import { useRef } from "react";
import { useNostrEvents, dateToUnix } from "nostr-react";
const GlobalFeed = () => {
const now = useRef(new Date()); // Make sure current time isn't re-rendered
const { events } = useNostrEvents({
filter: {
since: dateToUnix(now.current), // all new events from now
kinds: [1],
},
});
return (
<>
{events.map((event) => (
<p key={event.id}>{event.pubkey} posted: {event.content}</p>
))}
</>
);
};
```
**Fetching all `text_note` events from a specific user, since the beginning of time:**
```tsx
import { useNostrEvents } from "nostr-react";
const ProfileFeed = () => {
const { events } = useNostrEvents({
filter: {
authors: [
"9c2a6495b4e3de93f3e1cc254abe4078e17c64e5771abc676a5e205b62b1286c",
],
since: 0,
kinds: [1],
},
});
return (
<>
{events.map((event) => (
<p key={event.id}>{event.pubkey} posted: {event.content}</p>
))}
</>
);
};
```
**Fetching user profiles**
Use the `useProfile` hook to render user profiles. You can use this in multiple components at once (for example, rendering a name and avatar for each message in a chat), the hook will automatically use *batching* to prevent errors where a client sends too many requests at once. 🎉
```tsx
import { useProfile } from "nostr-react";
const Profile = () => {
const { data: userData } = useProfile({
pubkey,
});
return (
<>
<p>Name: {userData?.name}</p>
<p>Public key: {userData?.npub}</p>
<p>Picture URL: {userData?.picture}</p>
</>
)
}
```
**Post a message:**
```tsx
import { useNostr, dateToUnix } from "nostr-react";
import {
type Event as NostrEvent,
getEventHash,
getPublicKey,
signEvent,
} from "nostr-tools";
export default function PostButton() {
const { publish } = useNostr();
const onPost = async () => {
const privKey = prompt("Paste your private key:");
if (!privKey) {
alert("no private key provided");
return;
}
const message = prompt("Enter the message you want to send:");
if (!message) {
alert("no message provided");
return;
}
const event: NostrEvent = {
content: message,
kind: 1,
tags: [],
created_at: dateToUnix(),
pubkey: getPublicKey(privKey),
};
event.id = getEventHash(event);
event.sig = signEvent(event, privKey);
publish(event);
};
return (
<Button onClick={onPost}>Post a message!</Button>
);
}
```

View File

@@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider";
import NostrClientWrapper from "@/components/nostr-client-wrapper";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -28,14 +29,16 @@ export default function RootLayout({
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
<NostrClientWrapper>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</NostrClientWrapper>
</body>
</html>
);

View File

@@ -1,103 +1,23 @@
import Image from "next/image";
'use client';
import { dateToUnix, useNostrEvents } from "nostr-react";
import { useRef } from "react";
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2 tracking-[-.01em]">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
app/page.tsx
</code>
.
</li>
<li className="tracking-[-.01em]">
Save and see your changes instantly.
</li>
</ol>
const now = useRef(new Date()); // Make sure current time isn't re-rendered
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
</div>
const { events } = useNostrEvents({
filter: {
since: dateToUnix(now.current), // all new events from now
kinds: [1],
},
});
return (
<>
{events.map((event) => (
<p key={event.id}>{event.pubkey} posted: {event.content}</p>
))}
</>
);
}

View File

@@ -0,0 +1,20 @@
'use client';
import { NostrProvider } from "nostr-react";
import { ReactNode } from "react";
const relayUrls = [
"wss://relay.nostr.band"
];
export default function NostrClientWrapper({
children
}: {
children: ReactNode
}) {
return (
<NostrProvider relayUrls={relayUrls} debug={true}>
{children}
</NostrProvider>
);
}

171
package-lock.json generated
View File

@@ -14,6 +14,7 @@
"lucide-react": "^0.511.0",
"next": "15.3.2",
"next-themes": "^0.4.6",
"nostr-react": "^0.7.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwind-merge": "^3.3.0"
@@ -917,6 +918,39 @@
"node": ">= 10"
}
},
"node_modules/@noble/ciphers": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.2.0.tgz",
"integrity": "sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw==",
"license": "MIT",
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/curves": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz",
"integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.3.1"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/hashes": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -1012,6 +1046,45 @@
"dev": true,
"license": "MIT"
},
"node_modules/@scure/base": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
"integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==",
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
],
"license": "MIT"
},
"node_modules/@scure/bip32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz",
"integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==",
"license": "MIT",
"dependencies": {
"@noble/curves": "~1.1.0",
"@noble/hashes": "~1.3.1",
"@scure/base": "~1.1.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@scure/bip39": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz",
"integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "~1.3.0",
"@scure/base": "~1.1.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
@@ -4123,6 +4196,64 @@
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/jotai": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/jotai/-/jotai-1.13.1.tgz",
"integrity": "sha512-RUmH1S4vLsG3V6fbGlKzGJnLrDcC/HNb5gH2AeA9DzuJknoVxSGvvg8OBB7lke+gDc4oXmdVsaKn/xDUhWZ0vw==",
"license": "MIT",
"engines": {
"node": ">=12.20.0"
},
"peerDependencies": {
"@babel/core": "*",
"@babel/template": "*",
"jotai-devtools": "*",
"jotai-immer": "*",
"jotai-optics": "*",
"jotai-redux": "*",
"jotai-tanstack-query": "*",
"jotai-urql": "*",
"jotai-valtio": "*",
"jotai-xstate": "*",
"jotai-zustand": "*",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@babel/core": {
"optional": true
},
"@babel/template": {
"optional": true
},
"jotai-devtools": {
"optional": true
},
"jotai-immer": {
"optional": true
},
"jotai-optics": {
"optional": true
},
"jotai-redux": {
"optional": true
},
"jotai-tanstack-query": {
"optional": true
},
"jotai-urql": {
"optional": true
},
"jotai-valtio": {
"optional": true
},
"jotai-xstate": {
"optional": true
},
"jotai-zustand": {
"optional": true
}
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -4767,6 +4898,44 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/nostr-react": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/nostr-react/-/nostr-react-0.7.0.tgz",
"integrity": "sha512-m8fX+eaor+Xq7/DcxsHYS5IsNj4cyTYiIgm6lNqZeNR5mV8Yb4JlzOdw53WCJztgwDUrBx8/gPK3F88uIY1B6A==",
"license": "MIT",
"dependencies": {
"jotai": "^1.12.1",
"nostr-tools": "^1.1.0"
},
"engines": {
"node": ">=12"
},
"peerDependencies": {
"react": ">=16"
}
},
"node_modules/nostr-tools": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-1.17.0.tgz",
"integrity": "sha512-LZmR8GEWKZeElbFV5Xte75dOeE9EFUW/QLI1Ncn3JKn0kFddDKEfBbFN8Mu4TMs+L4HR/WTPha2l+PPuRnJcMw==",
"license": "Unlicense",
"dependencies": {
"@noble/ciphers": "0.2.0",
"@noble/curves": "1.1.0",
"@noble/hashes": "1.3.1",
"@scure/base": "1.1.1",
"@scure/bip32": "1.3.1",
"@scure/bip39": "1.2.1"
},
"peerDependencies": {
"typescript": ">=5.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -5980,7 +6149,7 @@
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",

View File

@@ -15,6 +15,7 @@
"lucide-react": "^0.511.0",
"next": "15.3.2",
"next-themes": "^0.4.6",
"nostr-react": "^0.7.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwind-merge": "^3.3.0"