diff --git a/.gitignore b/.gitignore
index c87c9b3..7a16b18 100644
--- a/.gitignore
+++ b/.gitignore
@@ -34,3 +34,5 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
+
+temp
diff --git a/components/LandingLayout.tsx b/components/LandingLayout.tsx
index 38ed62a..f109bea 100644
--- a/components/LandingLayout.tsx
+++ b/components/LandingLayout.tsx
@@ -3,7 +3,7 @@ import Link from "next/link";
import React from "react";
import { Comfortaa, Source_Sans_Pro } from "next/font/google";
import { Bars3Icon } from "@heroicons/react/24/solid";
-import { useProfile } from "nostr-react";
+import useProfile from "@/hooks/useProfile";
import { usePubkey } from "@/context/pubkey";
import { nip19 } from "nostr-tools";
@@ -14,21 +14,19 @@ interface LandingLayoutProps {
}
const PubkeyNavMenu = ({ pubkey }: { pubkey: string }) => {
- const { data: userData, isLoading } = useProfile({
- pubkey,
- });
+ const [profile, isLoading] = useProfile(pubkey)
return (
<>
{!isLoading && (
- {userData?.name
- ? userData.name
+ {profile?.name
+ ? profile.name
: nip19.npubEncode(pubkey).slice(0, 12)}
)}
- {userData?.picture ? (
-
+ {profile?.picture ? (
+
) : (
diff --git a/hooks/useProfile.ts b/hooks/useProfile.ts
new file mode 100644
index 0000000..1710511
--- /dev/null
+++ b/hooks/useProfile.ts
@@ -0,0 +1,44 @@
+import { useEffect } from "react"
+import { nostrClient } from "@/lib/nostr"
+import dexieDb from "@/store/dexieDb"
+import { useLiveQuery } from 'dexie-react-hooks'
+
+export interface UserMetadata {
+ name?: string
+ pubkey?: string
+ npub?: string
+ display_name?: string
+ picture?: string
+ about?: string
+ website?: string
+ banner?: string
+ lud06?: string
+ lud16?: string
+ nip05?: string
+}
+
+export default function useProfile(pubkey: string | null): [UserMetadata | undefined, boolean] {
+ const [profile, isLoading] = useLiveQuery(async () => {
+ if (!pubkey) return [undefined, false]
+
+ const ret = await dexieDb.users.get(pubkey)
+ console.debug('live query res: ', ret)
+
+ return [ret, false]
+ },
+ [pubkey],
+ [undefined, true] // default result returned on initial render.
+ )
+
+ useEffect(() => {
+ if (!pubkey) return
+
+ nostrClient.addProfileToFetch(pubkey)
+
+ return () => {
+ nostrClient.removeProfileToFetch(pubkey)
+ }
+ }, [pubkey])
+
+ return [profile, isLoading]
+}
\ No newline at end of file
diff --git a/lib/nostr.ts b/lib/nostr.ts
new file mode 100644
index 0000000..c34b34e
--- /dev/null
+++ b/lib/nostr.ts
@@ -0,0 +1,90 @@
+import { SimplePool, Filter, nip19, Event } from "nostr-tools";
+import dexieDb from '@/store/dexieDb'
+
+export interface UserMetadata {
+ name?: string
+ display_name?: string
+ picture?: string
+ about?: string
+ website?: string
+ banner?: string
+ lud06?: string
+ lud16?: string
+ nip05?: string
+}
+
+export type UserMetadataStore = UserMetadata & {
+ pubkey: string
+ npub: string
+ created_at: number
+}
+
+const defaultProfileRelays = [
+ "wss://nostr.terminus.money",
+ "wss://brb.io",
+ "wss://nostr.wine",
+ "wss://relay.snort.social",
+ "wss://gratten.duckdns.org/nostrrelay/relay2",
+]
+
+class NostrClient {
+ pool = new SimplePool()
+ profileQueue: Set = new Set() // set of hex public keys to query
+ paused: boolean = false
+
+ addProfileToFetch(pubkey: string) {
+ this.profileQueue.add(pubkey)
+ this._fetchPubkeys()
+ }
+
+ removeProfileToFetch(pubkey: string) {
+ this.profileQueue.delete(pubkey)
+ }
+ _fetchPubkeys() {
+ if (this.paused || this.profileQueue.size === 0) return
+
+ const filters: Filter[] = [
+ {
+ kinds: [0],
+ authors: Array.from(this.profileQueue),
+ },
+ ]
+
+ console.debug('subscribing for pubkeys: ', Array.from(this.profileQueue))
+
+ const sub = this.pool.sub(defaultProfileRelays, filters)
+
+ sub.on('event', async (event: Event) => {
+ console.debug('got event', event)
+
+ const metadataToStore: UserMetadataStore = {
+ ...JSON.parse(event.content),
+ pubkey: event.pubkey,
+ npub: nip19.npubEncode(event.pubkey),
+ created_at: event.created_at
+ }
+
+ const existingProfile = await dexieDb.users.get(metadataToStore.pubkey)
+
+ if (!existingProfile || (metadataToStore.created_at > existingProfile.created_at)) {
+ console.debug('storing profile metadata: ', metadataToStore)
+ await dexieDb.users.put(metadataToStore)
+ }
+ })
+
+
+ this.profileQueue.forEach(pubkey => this.profileQueue.delete(pubkey))
+
+ // limit amount of subs to one per 500ms
+ this.paused = true
+ setTimeout(() => {
+ this.paused = false
+
+ // call it again in case new keys came in...
+ this._fetchPubkeys()
+ }, 500)
+ }
+
+}
+
+export const nostrClient = new NostrClient()
diff --git a/package.json b/package.json
index 001e1d7..0f26260 100644
--- a/package.json
+++ b/package.json
@@ -13,11 +13,12 @@
"@types/node": "18.15.3",
"@types/react": "18.0.28",
"@types/react-dom": "18.0.11",
+ "dexie": "^3.2.3",
+ "dexie-react-hooks": "^1.1.3",
"eslint": "8.36.0",
"eslint-config-next": "13.2.4",
"next": "13.2.4",
"nodemon": "^2.0.21",
- "nostr-react": "^0.6.4",
"passport-lnurl-auth": "^1.5.1",
"react": "18.2.0",
"react-dom": "18.2.0",
diff --git a/pages/_app.tsx b/pages/_app.tsx
index 755a984..65fe032 100644
--- a/pages/_app.tsx
+++ b/pages/_app.tsx
@@ -1,25 +1,11 @@
-import { NostrProvider } from "nostr-react";
import { PubkeyProvider } from "@/context/pubkey";
import "@/styles/globals.css";
import type { AppProps } from "next/app";
-// TODO: Save default relays in store, allow user to set and remove, retrieve user
-// relays in a smart way based off who they are interacting with
-// ...or just leave as fixed for hackathon (we have custom relay anyways)
-const relays = [
- "wss://nostr.terminus.money",
- "wss://brb.io",
- "wss://nostr.wine",
- "wss://relay.snort.social",
- "wss://gratten.duckdns.org/nostrrelay/relay2",
-];
-
export default function App({ Component, pageProps }: AppProps) {
return (
-
-
);
}
diff --git a/pages/testProfiles.tsx b/pages/testProfiles.tsx
new file mode 100644
index 0000000..360c036
--- /dev/null
+++ b/pages/testProfiles.tsx
@@ -0,0 +1,63 @@
+import useProfile from "@/hooks/useProfile"
+import { nip19 } from "nostr-tools"
+
+// profiles from nostr.directory
+const pubkeys = [
+ "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245",
+ "82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2",
+ "00000000827ffaa94bfea288c3dfce4422c794fbb96625b6b31e9049f729d700",
+ "04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9",
+ "6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93",
+ "1577e4599dd10c863498fe3c20bd82aafaf829a595ce83c5cf8ac3463531b09b",
+ "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
+ "3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24",
+ "7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194",
+ "84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240",
+ "f8e6c64342f1e052480630e27e1016dce35fc3a614e60434fef4aa2503328ca9",
+ "85080d3bad70ccdcd7f74c29a44f55bb85cbcd3dd0cbb957da1d215bdb931204",
+ "5b0183ab6c3e322bf4d41c6b3aef98562a144847b7499543727c5539a114563e",
+ "c48e29f04b482cc01ca1f9ef8c86ef8318c059e0e9353235162f080f26e14c11",
+ "bf2376e17ba4ec269d10fcc996a4746b451152be9031fa48e74553dde5526bce",
+ "c43bbb58e2e6bc2f9455758257f6ba5329107bd4e8274068c2936c69d9980b7d",
+ "d307643547703537dfdef811c3dea96f1f9e84c8249e200353425924a9908cf8",
+ "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c",
+ "803a613997a26e8714116f99aa1f98e8589cb6116e1aaa1fc9c389984fcd9bb8",
+ "c49d52a573366792b9a6e4851587c28042fb24fa5625c6d67b8c95c8751aca15",
+ "92de68b21302fa2137b1cbba7259b8ba967b535a05c6d2b0847d9f35ff3cf56a",
+ "eab0e756d32b80bcd464f3d844b8040303075a13eabc3599a762c9ac7ab91f4f",
+ "c4eabae1be3cf657bc1855ee05e69de9f059cb7a059227168b80b89761cbc4e0",
+ "f728d9e6e7048358e70930f5ca64b097770d989ccd86854fe618eda9c8a38106",
+]
+
+const Profile = ({pubkey}:{pubkey: string}) => {
+ const [profile, isLoading] = useProfile(pubkey)
+ return (
+
+ {/* Image */}
+ {profile?.picture ? (
+

+ ) : (
+
+ )}
+
+ {/* Name */}
+ {!isLoading && (
+
+ {profile?.name
+ ? profile.name
+ : nip19.npubEncode(pubkey).slice(0, 12)}
+
+ )}
+
+ )
+}
+
+export default function Test() {
+ return (
+
+ {pubkeys.map(pk => {
+ return
+ })}
+
+ )
+}
\ No newline at end of file
diff --git a/store/dexieDb.ts b/store/dexieDb.ts
new file mode 100644
index 0000000..9368575
--- /dev/null
+++ b/store/dexieDb.ts
@@ -0,0 +1,37 @@
+import Dexie, { Table } from 'dexie'
+
+interface UserMetadata {
+ name?: string
+ display_name?: string
+ picture?: string
+ about?: string
+ website?: string
+ banner?: string
+ lud06?: string
+ lud16?: string
+ nip05?: string
+}
+
+type UserMetadataStore = UserMetadata & {
+ pubkey?: string
+ npub?: string
+ created_at: number
+}
+
+class DexieDB extends Dexie {
+ users!: Table
+
+ constructor() {
+ super('DexieDB')
+ //Writing this because there have been some issues on github where people index images or movies
+ // without really understanding the purpose of indexing fields.
+ // A rule of thumb: Are you going to put your property in a where(‘…’) clause?
+ // If yes, index it, if not, dont. Large indexes will affect database performance and in
+ // extreme cases make it unstable.
+ this.version(1).stores({
+ users: '++pubkey, name, npub, nip05', // Primary key and indexed props
+ })
+ }
+}
+
+export default new DexieDB()
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index 9544624..467481e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -913,6 +913,16 @@ detective@^5.2.1:
defined "^1.0.0"
minimist "^1.2.6"
+dexie-react-hooks@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/dexie-react-hooks/-/dexie-react-hooks-1.1.3.tgz#dfcd723d533172605f06335823b205adf7442cc6"
+ integrity sha512-bXXE1gfYtfuVYTNiOlyam+YVaO8KaqacgRuxFuP37YtpS6o/jxT6KOl5h+hhqY36s0UavlHWbL+HWJFMcQumIg==
+
+dexie@^3.2.3:
+ version "3.2.3"
+ resolved "https://registry.yarnpkg.com/dexie/-/dexie-3.2.3.tgz#f35c91ca797599df8e771b998e9ae9669c877f8c"
+ integrity sha512-iHayBd4UYryDCVUNa3PMsJMEnd8yjyh5p7a+RFeC8i8n476BC9wMhVvqiImq5zJZJf5Tuer+s4SSj+AA3x+ZbQ==
+
didyoumean@^1.2.2:
version "1.2.2"
resolved "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz"
@@ -1967,11 +1977,6 @@ isexe@^2.0.0:
resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz"
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
-jotai@^1.12.1:
- version "1.13.1"
- resolved "https://registry.yarnpkg.com/jotai/-/jotai-1.13.1.tgz#20cc46454cbb39096b12fddfa635b873b3668236"
- integrity sha512-RUmH1S4vLsG3V6fbGlKzGJnLrDcC/HNb5gH2AeA9DzuJknoVxSGvvg8OBB7lke+gDc4oXmdVsaKn/xDUhWZ0vw==
-
js-sdsl@^4.1.4:
version "4.3.0"
resolved "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz"
@@ -2293,15 +2298,7 @@ normalize-range@^0.1.2:
resolved "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz"
integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==
-nostr-react@^0.6.4:
- version "0.6.4"
- resolved "https://registry.yarnpkg.com/nostr-react/-/nostr-react-0.6.4.tgz#03c15f6ac4807efdb3ad3c181457353bedf30d28"
- integrity sha512-esRgmhTP5kPQ8ufs8cFAQxxJtMmzuba/k2QfXevG/ejHP3IMa41pb82qi8V0aPzY3KJ0Nr54x0OSa39d2InKzA==
- dependencies:
- jotai "^1.12.1"
- nostr-tools "^1.1.0"
-
-nostr-tools@^1.1.0, nostr-tools@^1.7.5:
+nostr-tools@^1.7.5:
version "1.7.5"
resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-1.7.5.tgz#349f469ff2877deb99d71c63d4883af93ec9f9a5"
integrity sha512-FFaYOAn9lFyISClbBzPe2eQ2ZiKx8xFviwHmdgTAmuue+eLrtPEI3tCqPtP624HghX/X4VnaixoiMvDB8g2+tQ==