From 59ff5de651c77f95629f1d8d4511e3f2558370bd Mon Sep 17 00:00:00 2001 From: Kieran Date: Wed, 1 Feb 2023 19:48:35 +0000 Subject: [PATCH] useDb hook --- src/Feed/LoginFeed.ts | 236 +++++++++++++++++++-------------------- src/Nostr/System.ts | 90 ++++++++------- src/Notifications.ts | 6 +- src/Pages/Layout.tsx | 17 ++- src/State/Login.ts | 10 +- src/State/Users/Db.ts | 29 ++--- src/State/Users/Hooks.ts | 27 ++--- 7 files changed, 210 insertions(+), 205 deletions(-) diff --git a/src/Feed/LoginFeed.ts b/src/Feed/LoginFeed.ts index f3b67792..6d391485 100644 --- a/src/Feed/LoginFeed.ts +++ b/src/Feed/LoginFeed.ts @@ -8,151 +8,149 @@ import Event from "Nostr/Event"; import { Subscriptions } from "Nostr/Subscriptions"; import { addDirectMessage, setFollows, setRelays, setMuted, setBlocked, sendNotification } from "State/Login"; import { RootState } from "State/Store"; -import { mapEventToProfile, MetadataCache } from "State/Users"; -import { getDb } from "State/Users/Db"; +import { mapEventToProfile, MetadataCache } from "State/Users"; +import { useDb } from "State/Users/Db"; import useSubscription from "Feed/Subscription"; -import { getDisplayName } from "Element/ProfileImage"; import { barierNip07 } from "Feed/EventPublisher"; import { getMutedKeys, getNewest } from "Feed/MuteList"; -import { MentionRegex } from "Const"; import useModeration from "Hooks/useModeration"; /** * Managed loading data for the current logged in user */ export default function useLoginFeed() { - const dispatch = useDispatch(); - const { publicKey: pubKey, privateKey: privKey } = useSelector((s: RootState) => s.login); - const { isMuted } = useModeration(); + const dispatch = useDispatch(); + const { publicKey: pubKey, privateKey: privKey } = useSelector((s: RootState) => s.login); + const { isMuted } = useModeration(); + const db = useDb(); - const subMetadata = useMemo(() => { - if (!pubKey) return null; + const subMetadata = useMemo(() => { + if (!pubKey) return null; - let sub = new Subscriptions(); - sub.Id = `login:meta`; - sub.Authors = new Set([pubKey]); - sub.Kinds = new Set([EventKind.ContactList, EventKind.SetMetadata]); - sub.Limit = 2 + let sub = new Subscriptions(); + sub.Id = `login:meta`; + sub.Authors = new Set([pubKey]); + sub.Kinds = new Set([EventKind.ContactList, EventKind.SetMetadata]); + sub.Limit = 2 - return sub; - }, [pubKey]); + return sub; + }, [pubKey]); - const subNotification = useMemo(() => { - if (!pubKey) return null; + const subNotification = useMemo(() => { + if (!pubKey) return null; - let sub = new Subscriptions(); - sub.Id = "login:notifications"; - sub.Kinds = new Set([EventKind.TextNote]); - sub.PTags = new Set([pubKey]); - sub.Limit = 1; - return sub; - }, [pubKey]); + let sub = new Subscriptions(); + sub.Id = "login:notifications"; + sub.Kinds = new Set([EventKind.TextNote]); + sub.PTags = new Set([pubKey]); + sub.Limit = 1; + return sub; + }, [pubKey]); - const subMuted = useMemo(() => { - if (!pubKey) return null; + const subMuted = useMemo(() => { + if (!pubKey) return null; - let sub = new Subscriptions(); - sub.Id = "login:muted"; - sub.Kinds = new Set([EventKind.Lists]); - sub.Authors = new Set([pubKey]); - sub.DTags = new Set([Lists.Muted]); - sub.Limit = 1; + let sub = new Subscriptions(); + sub.Id = "login:muted"; + sub.Kinds = new Set([EventKind.Lists]); + sub.Authors = new Set([pubKey]); + sub.DTags = new Set([Lists.Muted]); + sub.Limit = 1; - return sub; - }, [pubKey]); + return sub; + }, [pubKey]); - const subDms = useMemo(() => { - if (!pubKey) return null; + const subDms = useMemo(() => { + if (!pubKey) return null; - let dms = new Subscriptions(); - dms.Id = "login:dms"; - dms.Kinds = new Set([EventKind.DirectMessage]); - dms.PTags = new Set([pubKey]); + let dms = new Subscriptions(); + dms.Id = "login:dms"; + dms.Kinds = new Set([EventKind.DirectMessage]); + dms.PTags = new Set([pubKey]); - let dmsFromME = new Subscriptions(); - dmsFromME.Authors = new Set([pubKey]); - dmsFromME.Kinds = new Set([EventKind.DirectMessage]); - dms.AddSubscription(dmsFromME); + let dmsFromME = new Subscriptions(); + dmsFromME.Authors = new Set([pubKey]); + dmsFromME.Kinds = new Set([EventKind.DirectMessage]); + dms.AddSubscription(dmsFromME); - return dms; - }, [pubKey]); + return dms; + }, [pubKey]); - const metadataFeed = useSubscription(subMetadata, { leaveOpen: true, cache: true }); - const notificationFeed = useSubscription(subNotification, { leaveOpen: true, cache: true }); - const dmsFeed = useSubscription(subDms, { leaveOpen: true, cache: true }); - const mutedFeed = useSubscription(subMuted, { leaveOpen: true, cache: true }); + const metadataFeed = useSubscription(subMetadata, { leaveOpen: true, cache: true }); + const notificationFeed = useSubscription(subNotification, { leaveOpen: true, cache: true }); + const dmsFeed = useSubscription(subDms, { leaveOpen: true, cache: true }); + const mutedFeed = useSubscription(subMuted, { leaveOpen: true, cache: true }); - useEffect(() => { - let contactList = metadataFeed.store.notes.filter(a => a.kind === EventKind.ContactList); - let metadata = metadataFeed.store.notes.filter(a => a.kind === EventKind.SetMetadata); - let profiles = metadata.map(a => mapEventToProfile(a)) - .filter(a => a !== undefined) - .map(a => a!); + useEffect(() => { + let contactList = metadataFeed.store.notes.filter(a => a.kind === EventKind.ContactList); + let metadata = metadataFeed.store.notes.filter(a => a.kind === EventKind.SetMetadata); + let profiles = metadata.map(a => mapEventToProfile(a)) + .filter(a => a !== undefined) + .map(a => a!); - for (let cl of contactList) { - if (cl.content !== "" && cl.content !== "{}") { - let relays = JSON.parse(cl.content); - dispatch(setRelays({ relays, createdAt: cl.created_at })); - } - let pTags = cl.tags.filter(a => a[0] === "p").map(a => a[1]); - dispatch(setFollows({ keys: pTags, createdAt: cl.created_at })); - } - - (async () => { - let maxProfile = profiles.reduce((acc, v) => { - if (v.created > acc.created) { - acc.profile = v; - acc.created = v.created; - } - return acc; - }, { created: 0, profile: null as MetadataCache | null }); - if (maxProfile.profile) { - const db = getDb() - let existing = await db.find(maxProfile.profile.pubkey); - if ((existing?.created ?? 0) < maxProfile.created) { - await db.put(maxProfile.profile); - } - } - })().catch(console.warn); - }, [dispatch, metadataFeed.store]); - - useEffect(() => { - const replies = notificationFeed.store.notes.filter(a => a.kind === EventKind.TextNote && !isMuted(a.pubkey)) - replies.forEach(nx => { - makeNotification(nx).then(notification => { - if (notification) { - // @ts-ignore - dispatch(sendNotification(notification)) - } - }) - }) - }, [dispatch, notificationFeed.store]); - - useEffect(() => { - const muted = getMutedKeys(mutedFeed.store.notes) - dispatch(setMuted(muted)) - - const newest = getNewest(mutedFeed.store.notes) - if (newest && newest.content.length > 0 && pubKey) { - decryptBlocked(newest, pubKey, privKey).then((plaintext) => { - try { - const blocked = JSON.parse(plaintext) - const keys = blocked.filter((p:any) => p && p.length === 2 && p[0] === "p").map((p: any) => p[1]) - dispatch(setBlocked({ - keys, - createdAt: newest.created_at, - })) - } catch(error) { - console.debug("Couldn't parse JSON") - } - }).catch((error) => console.warn(error)) + for (let cl of contactList) { + if (cl.content !== "" && cl.content !== "{}") { + let relays = JSON.parse(cl.content); + dispatch(setRelays({ relays, createdAt: cl.created_at })); } - }, [dispatch, mutedFeed.store]) + let pTags = cl.tags.filter(a => a[0] === "p").map(a => a[1]); + dispatch(setFollows({ keys: pTags, createdAt: cl.created_at })); + } - useEffect(() => { - let dms = dmsFeed.store.notes.filter(a => a.kind === EventKind.DirectMessage); - dispatch(addDirectMessage(dms)); - }, [dispatch, dmsFeed.store]); + (async () => { + let maxProfile = profiles.reduce((acc, v) => { + if (v.created > acc.created) { + acc.profile = v; + acc.created = v.created; + } + return acc; + }, { created: 0, profile: null as MetadataCache | null }); + if (maxProfile.profile) { + let existing = await db.find(maxProfile.profile.pubkey); + if ((existing?.created ?? 0) < maxProfile.created) { + await db.put(maxProfile.profile); + } + } + })().catch(console.warn); + }, [dispatch, metadataFeed.store, db]); + + useEffect(() => { + const replies = notificationFeed.store.notes.filter(a => a.kind === EventKind.TextNote && !isMuted(a.pubkey)) + replies.forEach(nx => { + makeNotification(db, nx).then(notification => { + if (notification) { + // @ts-ignore + dispatch(sendNotification(notification)) + } + }) + }) + }, [dispatch, notificationFeed.store, db]); + + useEffect(() => { + const muted = getMutedKeys(mutedFeed.store.notes) + dispatch(setMuted(muted)) + + const newest = getNewest(mutedFeed.store.notes) + if (newest && newest.content.length > 0 && pubKey) { + decryptBlocked(newest, pubKey, privKey).then((plaintext) => { + try { + const blocked = JSON.parse(plaintext) + const keys = blocked.filter((p: any) => p && p.length === 2 && p[0] === "p").map((p: any) => p[1]) + dispatch(setBlocked({ + keys, + createdAt: newest.created_at, + })) + } catch (error) { + console.debug("Couldn't parse JSON") + } + }).catch((error) => console.warn(error)) + } + }, [dispatch, mutedFeed.store]) + + useEffect(() => { + let dms = dmsFeed.store.notes.filter(a => a.kind === EventKind.DirectMessage); + dispatch(addDirectMessage(dms)); + }, [dispatch, dmsFeed.store]); } diff --git a/src/Nostr/System.ts b/src/Nostr/System.ts index 7c1fcb4c..9314206e 100644 --- a/src/Nostr/System.ts +++ b/src/Nostr/System.ts @@ -1,7 +1,6 @@ import { HexKey, TaggedRawEvent } from "Nostr"; -import { getDb } from "State/Users/Db"; import { ProfileCacheExpire } from "Const"; -import { mapEventToProfile, MetadataCache } from "State/Users"; +import { mapEventToProfile, MetadataCache, UsersDb } from "State/Users"; import Connection, { RelaySettings } from "Nostr/Connection"; import Event from "Nostr/Event"; import EventKind from "Nostr/EventKind"; @@ -31,6 +30,11 @@ export class NostrSystem { */ WantsMetadata: Set; + /** + * User db store + */ + UserDb?: UsersDb; + constructor() { this.Sockets = new Map(); this.Subscriptions = new Map(); @@ -166,54 +170,54 @@ export class NostrSystem { } async _FetchMetadata() { - let missing = new Set(); - const db = getDb() - let meta = await db.bulkGet(Array.from(this.WantsMetadata)); - let expire = new Date().getTime() - ProfileCacheExpire; - for (let pk of this.WantsMetadata) { - let m = meta.find(a => a?.pubkey === pk); - if (!m || m.loaded < expire) { - missing.add(pk); - // cap 100 missing profiles - if (missing.size >= 100) { - break; - } - } - } - - if (missing.size > 0) { - console.debug("Wants profiles: ", missing); - - let sub = new Subscriptions(); - sub.Id = `profiles:${sub.Id}`; - sub.Kinds = new Set([EventKind.SetMetadata]); - sub.Authors = missing; - sub.OnEvent = async (e) => { - let profile = mapEventToProfile(e); - if (profile) { - let existing = await db.find(profile.pubkey); - if ((existing?.created ?? 0) < profile.created) { - await db.put(profile); - } else if (existing) { - await db.update(profile.pubkey, { loaded: new Date().getTime() }); + if (this.UserDb) { + let missing = new Set(); + let meta = await this.UserDb.bulkGet(Array.from(this.WantsMetadata)); + let expire = new Date().getTime() - ProfileCacheExpire; + for (let pk of this.WantsMetadata) { + let m = meta.find(a => a?.pubkey === pk); + if (!m || m.loaded < expire) { + missing.add(pk); + // cap 100 missing profiles + if (missing.size >= 100) { + break; } } } - let results = await this.RequestSubscription(sub); - let couldNotFetch = Array.from(missing).filter(a => !results.some(b => b.pubkey === a)); - console.debug("No profiles: ", couldNotFetch); - await db.bulkPut(couldNotFetch.map(a => { - return { - pubkey: a, - loaded: new Date().getTime() - } as MetadataCache; - })); - } + if (missing.size > 0) { + console.debug("Wants profiles: ", missing); + + let sub = new Subscriptions(); + sub.Id = `profiles:${sub.Id}`; + sub.Kinds = new Set([EventKind.SetMetadata]); + sub.Authors = missing; + sub.OnEvent = async (e) => { + let profile = mapEventToProfile(e); + if (profile) { + let existing = await this.UserDb!.find(profile.pubkey); + if ((existing?.created ?? 0) < profile.created) { + await this.UserDb!.put(profile); + } else if (existing) { + await this.UserDb!.update(profile.pubkey, { loaded: new Date().getTime() }); + } + } + } + let results = await this.RequestSubscription(sub); + let couldNotFetch = Array.from(missing).filter(a => !results.some(b => b.pubkey === a)); + console.debug("No profiles: ", couldNotFetch); + await this.UserDb!.bulkPut(couldNotFetch.map(a => { + return { + pubkey: a, + loaded: new Date().getTime() + } as MetadataCache; + })); + } + } setTimeout(() => this._FetchMetadata(), 500); } - async nip42Auth(challenge: string, relay:string): Promise { + async nip42Auth(challenge: string, relay: string): Promise { return } } diff --git a/src/Notifications.ts b/src/Notifications.ts index 074c23c2..7ce9b58d 100644 --- a/src/Notifications.ts +++ b/src/Notifications.ts @@ -3,13 +3,11 @@ import Nostrich from "nostrich.webp"; import { TaggedRawEvent } from "Nostr"; import EventKind from "Nostr/EventKind"; import type { NotificationRequest } from "State/Login"; -import { MetadataCache } from "State/Users"; -import { getDb } from "State/Users/Db"; +import { MetadataCache, UsersDb } from "State/Users"; import { getDisplayName } from "Element/ProfileImage"; import { MentionRegex } from "Const"; -export async function makeNotification(ev: TaggedRawEvent): Promise { - const db = getDb() +export async function makeNotification(db: UsersDb, ev: TaggedRawEvent): Promise { switch (ev.kind) { case EventKind.TextNote: { const pubkeys = new Set([ev.pubkey, ...ev.tags.filter(a => a[0] === "p").map(a => a[1]!)]); diff --git a/src/Pages/Layout.tsx b/src/Pages/Layout.tsx index 0e847447..40a54302 100644 --- a/src/Pages/Layout.tsx +++ b/src/Pages/Layout.tsx @@ -17,6 +17,7 @@ import { totalUnread } from "Pages/MessagesPage"; import { SearchRelays } from 'Const'; import useEventPublisher from "Feed/EventPublisher"; import useModeration from "Hooks/useModeration"; +import { IndexedUDB, useDb } from "State/Users/Db"; export default function Layout() { @@ -31,12 +32,17 @@ export default function Layout() { const { isMuted } = useModeration(); const filteredDms = dms.filter(a => !isMuted(a.pubkey)) const prefs = useSelector(s => s.login.preferences); + const usingDb = useDb(); const pub = useEventPublisher(); useLoginFeed(); useEffect(() => { System.nip42Auth = pub.nip42Auth - },[pub]) + }, [pub]) + + useEffect(() => { + System.UserDb = usingDb; + }, [usingDb]) useEffect(() => { if (relays) { @@ -73,7 +79,14 @@ export default function Layout() { }, [prefs.theme]); useEffect(() => { - dispatch(init()); + // check DB support then init + IndexedUDB.isAvailable() + .then(a => { + const db = a ? "indexdDb" : "redux"; + console.debug(`Using db: ${db}`); + dispatch(init(db)); + }) + }, []); async function goToNotifications(e: any) { diff --git a/src/State/Login.ts b/src/State/Login.ts index ddaee2ac..7974192f 100644 --- a/src/State/Login.ts +++ b/src/State/Login.ts @@ -61,8 +61,14 @@ export interface UserPreferences { */ imgProxyConfig: ImgProxySettings | null } +export type DbType = "indexdDb" | "redux"; export interface LoginStore { + /** + * Which db we will use to cache data + */ + useDb: DbType, + /** * If there is no login */ @@ -146,6 +152,7 @@ const DefaultImgProxy = { }; export const InitState = { + useDb: "redux", loggedOut: undefined, publicKey: undefined, privateKey: undefined, @@ -186,7 +193,8 @@ const LoginSlice = createSlice({ name: "Login", initialState: InitState, reducers: { - init: (state) => { + init: (state, action: PayloadAction) => { + state.useDb = action.payload; state.privateKey = window.localStorage.getItem(PrivateKeyItem) ?? undefined; if (state.privateKey) { window.localStorage.removeItem(PublicKeyItem); // reset nip07 if using private key diff --git a/src/State/Users/Db.ts b/src/State/Users/Db.ts index 8983a414..22ed60bd 100644 --- a/src/State/Users/Db.ts +++ b/src/State/Users/Db.ts @@ -2,9 +2,10 @@ import { HexKey } from "Nostr"; import { db as idb } from "Db"; import { UsersDb, MetadataCache, setUsers } from "State/Users"; -import store from "State/Store"; +import store, { RootState } from "State/Store"; +import { useSelector } from "react-redux"; -class IndexedDb implements UsersDb { +class IndexedUsersDb implements UsersDb { ready: boolean = false; isAvailable() { @@ -132,21 +133,13 @@ class ReduxUsersDb implements UsersDb { } } +export const IndexedUDB = new IndexedUsersDb(); +export const ReduxUDB = new ReduxUsersDb(); -const indexedDb = new IndexedDb() -export const inMemoryDb = new ReduxUsersDb() - -let db: UsersDb = inMemoryDb -indexedDb.isAvailable().then((available) => { - if (available) { - console.debug('Using Indexed DB') - indexedDb.ready = true; - db = indexedDb; - } else { - console.debug('Using in-memory DB') +export function useDb(): UsersDb { + const db = useSelector((s: RootState) => s.login.useDb); + switch (db) { + case "indexdDb": return IndexedUDB + default: return ReduxUDB } -}) - -export function getDb() { - return db -} +} \ No newline at end of file diff --git a/src/State/Users/Hooks.ts b/src/State/Users/Hooks.ts index 345daff1..d6628328 100644 --- a/src/State/Users/Hooks.ts +++ b/src/State/Users/Hooks.ts @@ -1,27 +1,17 @@ import { useSelector } from "react-redux" import { useLiveQuery } from "dexie-react-hooks"; import { MetadataCache } from "State/Users"; -import { getDb, inMemoryDb } from "State/Users/Db"; import type { RootState } from "State/Store" import { HexKey } from "Nostr"; +import { useDb } from "./Db"; export function useQuery(query: string, limit: number = 5) { - const db = getDb() - - const allUsers = useLiveQuery( - () => db.query(query) - .catch((err) => { - console.error(err) - return inMemoryDb.query(query) - }), - [query], - ) - - return allUsers + const db = useDb() + return useLiveQuery(async () => db.query(query), [query],) } export function useKey(pubKey: HexKey) { - const db = getDb() + const db = useDb() const { users } = useSelector((state: RootState) => state.users) const defaultUser = users[pubKey] @@ -40,7 +30,9 @@ export function useKey(pubKey: HexKey) { } export function useKeys(pubKeys: HexKey[]): Map { - const db = getDb() + const db = useDb() + const { users } = useSelector((state: RootState) => state.users) + const dbUsers = useLiveQuery(async () => { if (pubKeys) { try { @@ -48,12 +40,11 @@ export function useKeys(pubKeys: HexKey[]): Map { return new Map(ret.map(a => [a.pubkey, a])) } catch (error) { console.error(error) - const ret = await inMemoryDb.bulkGet(pubKeys); - return new Map(ret.map(a => [a.pubkey, a])) + return new Map(pubKeys.map(a => [a, users[a]])) } } return new Map() - }, [pubKeys]); + }, [pubKeys, users]); return dbUsers! }