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==