diff --git a/src/db.ts b/src/db.ts index 203982d2..630ddb73 100644 --- a/src/db.ts +++ b/src/db.ts @@ -1,9 +1,8 @@ import Dexie, { Table } from 'dexie'; - -import type { User } from './nostr/types'; +import { MetadataCache } from './state/Users'; export class SnortDB extends Dexie { - users!: Table; + users!: Table; constructor() { super('snortDB'); diff --git a/src/element/Textarea.tsx b/src/element/Textarea.tsx index 1916efd8..f4da809b 100644 --- a/src/element/Textarea.tsx +++ b/src/element/Textarea.tsx @@ -11,12 +11,12 @@ import "./Textarea.css"; // @ts-expect-error import Nostrich from "../nostrich.jpg"; import { hexToBech32 } from "../Util"; -import type { User } from "../nostr/types"; import { db } from "../db"; +import { MetadataCache } from "../state/Users"; -function searchUsers(query: string, users: User[]) { +function searchUsers(query: string, users: MetadataCache[]) { const q = query.toLowerCase() - return users.filter(({ name, display_name, about, nip05 }: User) => { + return users.filter(({ name, display_name, about, nip05 }: MetadataCache) => { return name?.toLowerCase().includes(q) || display_name?.toLowerCase().includes(q) || about?.toLowerCase().includes(q) @@ -24,7 +24,7 @@ function searchUsers(query: string, users: User[]) { }).slice(0, 3) } -const UserItem = ({ pubkey, display_name, picture, nip05, ...rest }: User) => { +const UserItem = ({ pubkey, display_name, picture, nip05, ...rest }: MetadataCache) => { return (
@@ -38,22 +38,22 @@ const UserItem = ({ pubkey, display_name, picture, nip05, ...rest }: User) => { ) } -function normalizeUser({ pubkey, picture, nip05, name, display_name }: User) { +function normalizeUser({ pubkey, picture, nip05, name, display_name }: MetadataCache) { return { pubkey, nip05, name, picture, display_name } } const Textarea = ({ users, onChange, ...rest }: any) => { - const normalizedUsers = Object.keys(users).reduce((acc, pk) => { - return {...acc, [pk]: normalizeUser(users[pk]) } - }, {}) - const dbUsers = useLiveQuery( - () => db.users.toArray().then(usrs => { - return usrs.reduce((acc, usr) => { - return { ...acc, [usr.pubkey]: normalizeUser(usr)} - }, {}) - }) - ) - const allUsers: User[] = Object.values({...normalizedUsers, ...dbUsers}) + const normalizedUsers = Object.keys(users).reduce((acc, pk) => { + return { ...acc, [pk]: normalizeUser(users[pk]) } + }, {}) + const dbUsers = useLiveQuery( + () => db.users.toArray().then(usrs => { + return usrs.reduce((acc, usr) => { + return { ...acc, [usr.pubkey]: normalizeUser(usr) } + }, {}) + }) + ) + const allUsers: MetadataCache[] = Object.values({ ...normalizedUsers, ...dbUsers }) return ( (s => s.users.users[pubKey]); + const user = useLiveQuery(async () => { + return await db.users.get(pubKey); + }, [pubKey]); useEffect(() => { - if (pubKey) { - dispatch(addPubKey(pubKey)); - } + System.GetMetadata(pubKey); }, [pubKey]); return user; diff --git a/src/feed/UsersFeed.ts b/src/feed/UsersFeed.ts index 1458af6a..1c6a1f2b 100644 --- a/src/feed/UsersFeed.ts +++ b/src/feed/UsersFeed.ts @@ -42,16 +42,13 @@ export default function useUsersCache() { .filter(a => a !== undefined) .map(a => a!); dispatch(setUserData(profiles)); - const dbProfiles = results.notes.map(ev => { - return { ...JSON.parse(ev.content), pubkey: ev.pubkey } - }); - db.users.bulkPut(dbProfiles); + db.users.bulkPut(profiles); }, [results]); return results; } -export function mapEventToProfile(ev: TaggedRawEvent): MetadataCache | undefined { +export function mapEventToProfile(ev: TaggedRawEvent) { try { let data: UserMetadata = JSON.parse(ev.content); return { @@ -59,7 +56,7 @@ export function mapEventToProfile(ev: TaggedRawEvent): MetadataCache | undefined created: ev.created_at, loaded: new Date().getTime(), ...data - }; + } as MetadataCache; } catch (e) { console.error("Failed to parse JSON", ev, e); } diff --git a/src/nostr/System.ts b/src/nostr/System.ts index 297cc623..5076ddd7 100644 --- a/src/nostr/System.ts +++ b/src/nostr/System.ts @@ -1,6 +1,10 @@ -import { TaggedRawEvent } from "."; +import { HexKey, TaggedRawEvent } from "."; +import { ProfileCacheExpire } from "../Const"; +import { db } from "../db"; +import { mapEventToProfile } from "../feed/UsersFeed"; import Connection, { RelaySettings } from "./Connection"; import Event from "./Event"; +import EventKind from "./EventKind"; import { Subscriptions } from "./Subscriptions"; /** @@ -22,10 +26,17 @@ export class NostrSystem { */ PendingSubscriptions: Subscriptions[]; + /** + * List of pubkeys to fetch metadata for + */ + WantsMetadata: Set; + constructor() { this.Sockets = new Map(); this.Subscriptions = new Map(); this.PendingSubscriptions = []; + this.WantsMetadata = new Set(); + this._FetchMetadata() } /** @@ -79,11 +90,17 @@ export class NostrSystem { } } + GetMetadata(pk: HexKey) { + if (pk.length > 0) { + this.WantsMetadata.add(pk); + } + } + /** * Request/Response pattern */ RequestSubscription(sub: Subscriptions) { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { let events: TaggedRawEvent[] = []; // force timeout returning current results @@ -119,6 +136,37 @@ export class NostrSystem { this.AddSubscription(sub); }); } + + async _FetchMetadata() { + let missing = new Set(); + for (let pk of this.WantsMetadata) { + let meta = await db.users.get(pk); + let now = new Date().getTime(); + if (!meta || meta.loaded < now - ProfileCacheExpire) { + missing.add(pk); + } else { + this.WantsMetadata.delete(pk); + } + } + + if (missing.size > 0) { + console.debug("Wants: ", missing); + + let sub = new Subscriptions(); + sub.Id = `profiles:${sub.Id}`; + sub.Kinds = new Set([EventKind.SetMetadata]); + sub.Authors = missing; + sub.OnEvent = (e) => { + let profile = mapEventToProfile(e); + if (profile) { + db.users.put(profile); + } + } + await this.RequestSubscription(sub); + } + + setTimeout(() => this._FetchMetadata(), 500); + } } export const System = new NostrSystem(); \ No newline at end of file diff --git a/src/nostr/types.tsx b/src/nostr/types.tsx deleted file mode 100644 index 8c02463b..00000000 --- a/src/nostr/types.tsx +++ /dev/null @@ -1,9 +0,0 @@ -export interface User { - name?: string - about?: string - display_name?: string - nip05?: string - pubkey: string - picture?: string -} - diff --git a/src/state/Users.ts b/src/state/Users.ts index 382acdb0..7fa9629c 100644 --- a/src/state/Users.ts +++ b/src/state/Users.ts @@ -1,6 +1,4 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' -import { ProfileCacheExpire } from '../Const'; -import { db } from '../db'; import { HexKey, UserMetadata } from '../nostr'; export interface MetadataCache extends UserMetadata { @@ -27,10 +25,10 @@ export interface UsersStore { const UsersSlice = createSlice({ name: "Users", - initialState: { + initialState: { pubKeys: [], users: {}, - }, + } as UsersStore, reducers: { addPubKey: (state, action: PayloadAction>) => { let keys = action.payload; @@ -38,31 +36,15 @@ const UsersSlice = createSlice({ keys = [keys]; } let changes = false; - let fromCache = false; let temp = new Set(state.pubKeys); for (let k of keys) { if (!temp.has(k)) { changes = true; temp.add(k); - - // load from cache - let cache = window.localStorage.getItem(`user:${k}`); - if (cache) { - let ud: MetadataCache = JSON.parse(cache); - if (ud.loaded > new Date().getTime() - ProfileCacheExpire) { - state.users[ud.pubkey] = ud; - fromCache = true; - } - } } } if (changes) { state.pubKeys = Array.from(temp); - if (fromCache) { - state.users = { - ...state.users - }; - } } }, setUserData: (state, action: PayloadAction>) => { @@ -84,8 +66,6 @@ const UsersSlice = createSlice({ }; } state.users[x.pubkey] = x; - db.users.put(x) - state.users = { ...state.users };