diff --git a/src/Db/User.ts b/src/Db/User.ts index c5f4947a..e69de29b 100644 --- a/src/Db/User.ts +++ b/src/Db/User.ts @@ -1,34 +0,0 @@ -import { HexKey, TaggedRawEvent, UserMetadata } from "Nostr"; -import { hexToBech32 } from "../Util"; - -export interface MetadataCache extends UserMetadata { - /** - * When the object was saved in cache - */ - loaded: number, - - /** - * When the source metadata event was created - */ - created: number, - - /** - * The pubkey of the owner of this metadata - */ - pubkey: HexKey -}; - -export function mapEventToProfile(ev: TaggedRawEvent) { - try { - let data: UserMetadata = JSON.parse(ev.content); - return { - pubkey: ev.pubkey, - npub: hexToBech32("npub", ev.pubkey), - created: ev.created_at, - loaded: new Date().getTime(), - ...data - } as MetadataCache; - } catch (e) { - console.error("Failed to parse JSON", ev, e); - } -} \ No newline at end of file diff --git a/src/Db/index.ts b/src/Db/index.ts index b30881a7..c7d965a9 100644 --- a/src/Db/index.ts +++ b/src/Db/index.ts @@ -1,8 +1,7 @@ import Dexie, { Table } from "dexie"; -import { MetadataCache } from "Db/User"; +import { MetadataCache } from "State/Users"; import { hexToBech32 } from "Util"; - export class SnortDB extends Dexie { users!: Table; diff --git a/src/Element/Mention.tsx b/src/Element/Mention.tsx index 20898cc0..6e53ba2f 100644 --- a/src/Element/Mention.tsx +++ b/src/Element/Mention.tsx @@ -1,11 +1,11 @@ import { useMemo } from "react"; import { Link } from "react-router-dom"; -import useProfile from "Feed/ProfileFeed"; +import { useUserProfile } from "Feed/ProfileFeed"; import { HexKey } from "Nostr"; import { hexToBech32, profileLink } from "Util"; export default function Mention({ pubkey }: { pubkey: HexKey }) { - const user = useProfile(pubkey)?.get(pubkey); + const user = useUserProfile(pubkey) const name = useMemo(() => { let name = hexToBech32("npub", pubkey).substring(0, 12); @@ -18,4 +18,4 @@ export default function Mention({ pubkey }: { pubkey: HexKey }) { }, [user, pubkey]); return e.stopPropagation()}>@{name} -} \ No newline at end of file +} diff --git a/src/Element/Nip5Service.tsx b/src/Element/Nip5Service.tsx index bf0185f2..099a6a45 100644 --- a/src/Element/Nip5Service.tsx +++ b/src/Element/Nip5Service.tsx @@ -13,7 +13,7 @@ import { import AsyncButton from "Element/AsyncButton"; import LNURLTip from "Element/LNURLTip"; import Copy from "Element/Copy"; -import useProfile from "Feed/ProfileFeed"; +import { useUserProfile }from "Feed/ProfileFeed"; import useEventPublisher from "Feed/EventPublisher"; import { hexToBech32 } from "Util"; import { UserMetadata } from "Nostr"; @@ -31,7 +31,7 @@ type ReduxStore = any; export default function Nip5Service(props: Nip05ServiceProps) { const navigate = useNavigate(); const pubkey = useSelector(s => s.login.publicKey); - const user = useProfile(pubkey); + const user = useUserProfile(pubkey); const publisher = useEventPublisher(); const svc = new ServiceProvider(props.service); const [serviceConfig, setServiceConfig] = useState(); @@ -190,4 +190,4 @@ export default function Nip5Service(props: Nip05ServiceProps) { } ) -} \ No newline at end of file +} diff --git a/src/Element/Note.tsx b/src/Element/Note.tsx index d68d1383..38d5193f 100644 --- a/src/Element/Note.tsx +++ b/src/Element/Note.tsx @@ -9,7 +9,7 @@ import { eventLink, getReactions, hexToBech32 } from "Util"; import NoteFooter from "Element/NoteFooter"; import NoteTime from "Element/NoteTime"; import EventKind from "Nostr/EventKind"; -import useProfile from "Feed/ProfileFeed"; +import { useUserProfiles } from "Feed/ProfileFeed"; import { TaggedRawEvent, u256 } from "Nostr"; import { useInView } from "react-intersection-observer"; @@ -31,7 +31,7 @@ export default function Note(props: NoteProps) { const { data, isThread, related, highlight, options: opt, ["data-ev"]: parsedEvent } = props const ev = useMemo(() => parsedEvent ?? new NEvent(data), [data]); const pubKeys = useMemo(() => ev.Thread?.PubKeys || [], [ev]); - const users = useProfile(pubKeys); + const users = useUserProfiles(pubKeys); const deletions = useMemo(() => getReactions(related, ev.Id, EventKind.Deletion), [related]); const { ref, inView } = useInView({ triggerOnce: true }); diff --git a/src/Element/NoteFooter.tsx b/src/Element/NoteFooter.tsx index aaa453ff..0ba109d2 100644 --- a/src/Element/NoteFooter.tsx +++ b/src/Element/NoteFooter.tsx @@ -9,7 +9,7 @@ import useEventPublisher from "Feed/EventPublisher"; import { getReactions, hexToBech32, normalizeReaction, Reaction } from "Util"; import { NoteCreator } from "Element/NoteCreator"; import LNURLTip from "Element/LNURLTip"; -import useProfile from "Feed/ProfileFeed"; +import { useUserProfile } from "Feed/ProfileFeed"; import { default as NEvent } from "Nostr/Event"; import { RootState } from "State/Store"; import { HexKey, TaggedRawEvent } from "Nostr"; @@ -26,7 +26,7 @@ export default function NoteFooter(props: NoteFooterProps) { const login = useSelector(s => s.login.publicKey); const prefs = useSelector(s => s.login.preferences); - const author = useProfile(ev.RootPubKey)?.get(ev.RootPubKey); + const author = useUserProfile(ev.RootPubKey); const publisher = useEventPublisher(); const [reply, setReply] = useState(false); const [tip, setTip] = useState(false); diff --git a/src/Element/NoteToSelf.tsx b/src/Element/NoteToSelf.tsx index 74b893ca..9ad1feff 100644 --- a/src/Element/NoteToSelf.tsx +++ b/src/Element/NoteToSelf.tsx @@ -3,7 +3,7 @@ import "./NoteToSelf.css"; import { Link, useNavigate } from "react-router-dom"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faBook, faCertificate } from "@fortawesome/free-solid-svg-icons" -import useProfile from "Feed/ProfileFeed"; +import { useUserProfile } from "Feed/ProfileFeed"; import Nip05 from "Element/Nip05"; import { profileLink } from "Util"; @@ -15,7 +15,7 @@ export interface NoteToSelfProps { }; function NoteLabel({pubkey, link}:NoteToSelfProps) { - const user = useProfile(pubkey)?.get(pubkey); + const user = useUserProfile(pubkey); return (
Note to Self diff --git a/src/Element/ProfileImage.tsx b/src/Element/ProfileImage.tsx index 661433f3..04dba831 100644 --- a/src/Element/ProfileImage.tsx +++ b/src/Element/ProfileImage.tsx @@ -2,12 +2,12 @@ import "./ProfileImage.css"; import { useMemo } from "react"; import { Link, useNavigate } from "react-router-dom"; -import useProfile from "Feed/ProfileFeed"; +import { useUserProfile } from "Feed/ProfileFeed"; import { hexToBech32, profileLink } from "Util"; import Avatar from "Element/Avatar" import Nip05 from "Element/Nip05"; import { HexKey } from "Nostr"; -import { MetadataCache } from "Db/User"; +import { MetadataCache } from "State/Users"; export interface ProfileImageProps { pubkey: HexKey, @@ -19,7 +19,7 @@ export interface ProfileImageProps { export default function ProfileImage({ pubkey, subHeader, showUsername = true, className, link }: ProfileImageProps) { const navigate = useNavigate(); - const user = useProfile(pubkey)?.get(pubkey); + const user = useUserProfile(pubkey); const name = useMemo(() => { return getDisplayName(user, pubkey); diff --git a/src/Element/ProfilePreview.tsx b/src/Element/ProfilePreview.tsx index d00aacd5..a27f2ab0 100644 --- a/src/Element/ProfilePreview.tsx +++ b/src/Element/ProfilePreview.tsx @@ -3,7 +3,7 @@ import { ReactNode } from "react"; import ProfileImage from "Element/ProfileImage"; import FollowButton from "Element/FollowButton"; -import useProfile from "Feed/ProfileFeed"; +import { useUserProfile } from "Feed/ProfileFeed"; import { HexKey } from "Nostr"; import { useInView } from "react-intersection-observer"; @@ -16,7 +16,7 @@ export interface ProfilePreviewProps { } export default function ProfilePreview(props: ProfilePreviewProps) { const pubkey = props.pubkey; - const user = useProfile(pubkey)?.get(pubkey); + const user = useUserProfile(pubkey); const { ref, inView } = useInView({ triggerOnce: true }); const options = { about: true, @@ -34,4 +34,4 @@ export default function ProfilePreview(props: ProfilePreviewProps) { }
) -} \ No newline at end of file +} diff --git a/src/Element/Text.tsx b/src/Element/Text.tsx index e74a5cef..8166fef3 100644 --- a/src/Element/Text.tsx +++ b/src/Element/Text.tsx @@ -10,7 +10,7 @@ import Invoice from "Element/Invoice"; import Hashtag from "Element/Hashtag"; import Tag from "Nostr/Tag"; -import { MetadataCache } from "Db/User"; +import { MetadataCache } from "State/Users"; import Mention from "Element/Mention"; import TidalEmbed from "Element/TidalEmbed"; import { useSelector } from 'react-redux'; diff --git a/src/Element/Textarea.tsx b/src/Element/Textarea.tsx index 79397df2..116d272d 100644 --- a/src/Element/Textarea.tsx +++ b/src/Element/Textarea.tsx @@ -2,7 +2,6 @@ import "@webscopeio/react-textarea-autocomplete/style.css"; import "./Textarea.css"; import { useState } from "react"; -import { useLiveQuery } from "dexie-react-hooks"; import ReactTextareaAutocomplete from "@webscopeio/react-textarea-autocomplete"; import emoji from "@jukben/emoji-search"; import TextareaAutosize from "react-textarea-autosize"; @@ -11,7 +10,7 @@ import Avatar from "Element/Avatar"; import Nip05 from "Element/Nip05"; import { hexToBech32 } from "Util"; import { db } from "Db"; -import { MetadataCache } from "Db/User"; +import { useQuery, MetadataCache } from "State/Users"; interface EmojiItemProps { name: string @@ -45,16 +44,7 @@ const UserItem = (metadata: MetadataCache) => { const Textarea = ({ users, onChange, ...rest }: any) => { const [query, setQuery] = useState('') - const allUsers = useLiveQuery( - () => db.users - .where("npub").startsWithIgnoreCase(query) - .or("name").startsWithIgnoreCase(query) - .or("display_name").startsWithIgnoreCase(query) - .or("nip05").startsWithIgnoreCase(query) - .limit(5) - .toArray(), - [query], - ); + const allUsers = useQuery(query) const userDataProvider = (token: string) => { setQuery(token) diff --git a/src/Element/ZapButton.tsx b/src/Element/ZapButton.tsx index 82416d00..e187b62b 100644 --- a/src/Element/ZapButton.tsx +++ b/src/Element/ZapButton.tsx @@ -2,12 +2,12 @@ import "./ZapButton.css"; import { faBolt } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useState } from "react"; -import useProfile from "Feed/ProfileFeed"; +import { useUserProfile } from "Feed/ProfileFeed"; import { HexKey } from "Nostr"; import LNURLTip from "Element/LNURLTip"; const ZapButton = ({ pubkey }: { pubkey: HexKey }) => { - const profile = useProfile(pubkey)?.get(pubkey); + const profile = useUserProfile(pubkey); const [zap, setZap] = useState(false); const svc = profile?.lud16 || profile?.lud06; diff --git a/src/Feed/LoginFeed.ts b/src/Feed/LoginFeed.ts index ce643426..8af0d236 100644 --- a/src/Feed/LoginFeed.ts +++ b/src/Feed/LoginFeed.ts @@ -7,9 +7,8 @@ import EventKind from "Nostr/EventKind"; import { Subscriptions } from "Nostr/Subscriptions"; import { addDirectMessage, addNotifications, setFollows, setRelays } from "State/Login"; import { RootState } from "State/Store"; -import { db } from "Db"; +import { mapEventToProfile, MetadataCache, find, put, bulkGet } from "State/Users"; import useSubscription from "Feed/Subscription"; -import { mapEventToProfile, MetadataCache } from "Db/User"; import { getDisplayName } from "Element/ProfileImage"; import { MentionRegex } from "Const"; @@ -81,9 +80,9 @@ export default function useLoginFeed() { return acc; }, { created: 0, profile: null }); if (maxProfile.profile) { - let existing = await db.users.get(maxProfile.profile.pubkey); + let existing = await find(maxProfile.profile.pubkey); if ((existing?.created ?? 0) < maxProfile.created) { - await db.users.put(maxProfile.profile); + await put(maxProfile.profile); } } })().catch(console.warn); @@ -94,7 +93,8 @@ async function makeNotification(ev: TaggedRawEvent) { switch (ev.kind) { case EventKind.TextNote: { const pubkeys = new Set([ev.pubkey, ...ev.tags.filter(a => a[0] === "p").map(a => a[1]!)]); - const users = (await db.users.bulkGet(Array.from(pubkeys))).filter(a => a !== undefined).map(a => a!); + const users = await bulkGet(Array.from(pubkeys)) + // @ts-ignore const fromUser = users.find(a => a?.pubkey === ev.pubkey); const name = getDisplayName(fromUser, ev.pubkey); const avatarUrl = (fromUser?.picture?.length ?? 0) === 0 ? Nostrich : fromUser?.picture; @@ -135,4 +135,4 @@ async function sendNotification(ev: TaggedRawEvent) { vibrate: [500] }); } -} \ No newline at end of file +} diff --git a/src/Feed/ProfileFeed.ts b/src/Feed/ProfileFeed.ts index 435a5928..d633a611 100644 --- a/src/Feed/ProfileFeed.ts +++ b/src/Feed/ProfileFeed.ts @@ -1,27 +1,13 @@ import { useLiveQuery } from "dexie-react-hooks"; +import { useSelector } from "react-redux"; import { useEffect, useMemo } from "react"; -import { db } from "Db"; -import { MetadataCache } from "Db/User"; +import { RootState } from "State/Store"; +import { MetadataCache, find, bulkGet, useQuery, useKey, useKeys } from "State/Users"; import { HexKey } from "Nostr"; import { System } from "Nostr/System"; -export default function useProfile(pubKey: HexKey | Array | undefined): Map | undefined { - const user = useLiveQuery(async () => { - let userList = new Map(); - if (pubKey) { - if (Array.isArray(pubKey)) { - let ret = await db.users.bulkGet(pubKey); - let filtered = ret.filter(a => a !== undefined).map(a => a!); - return new Map(filtered.map(a => [a.pubkey, a])) - } else { - let ret = await db.users.get(pubKey); - if (ret) { - userList.set(ret.pubkey, ret); - } - } - } - return userList; - }, [pubKey]); +export function useUserProfile(pubKey: HexKey): MetadataCache | undefined { + const users = useKey(pubKey); useEffect(() => { if (pubKey) { @@ -30,5 +16,19 @@ export default function useProfile(pubKey: HexKey | Array | undefined): } }, [pubKey]); - return user; -} \ No newline at end of file + return users; +} + + +export function useUserProfiles(pubKeys: Array): Map | undefined { + const users = useKeys(pubKeys); + + useEffect(() => { + if (pubKeys) { + System.TrackMetadata(pubKeys); + return () => System.UntrackMetadata(pubKeys); + } + }, [pubKeys]); + + return users; +} diff --git a/src/Nostr/System.ts b/src/Nostr/System.ts index bdc81e73..a21a1a31 100644 --- a/src/Nostr/System.ts +++ b/src/Nostr/System.ts @@ -1,7 +1,6 @@ import { HexKey, TaggedRawEvent } from "Nostr"; import { ProfileCacheExpire } from "Const"; -import { db } from "Db"; -import { mapEventToProfile, MetadataCache } from "Db/User"; +import { mapEventToProfile, MetadataCache, add, bulkAdd, bulkGet, find, put, update, bulkPut } from "State/Users"; import Connection, { RelaySettings } from "Nostr/Connection"; import Event from "Nostr/Event"; import EventKind from "Nostr/EventKind"; @@ -167,7 +166,7 @@ export class NostrSystem { async _FetchMetadata() { let missing = new Set(); - let meta = await db.users.bulkGet(Array.from(this.WantsMetadata)); + let meta = await 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); @@ -190,18 +189,18 @@ export class NostrSystem { sub.OnEvent = async (e) => { let profile = mapEventToProfile(e); if (profile) { - let existing = await db.users.get(profile.pubkey); - if ((existing?.created ?? 0) < profile.created) { - await db.users.put(profile); - } else if (existing) { - await db.users.update(profile.pubkey, { loaded: new Date().getTime() }); + let existing = await find(profile.pubkey); + if((existing?.created ?? 0) < profile.created) { + await put(profile); + } else if(existing) { + await 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 db.users.bulkPut(couldNotFetch.map(a => { + await bulkPut(couldNotFetch.map(a => { return { pubkey: a, loaded: new Date().getTime() @@ -213,4 +212,4 @@ export class NostrSystem { } } -export const System = new NostrSystem(); \ No newline at end of file +export const System = new NostrSystem(); diff --git a/src/Pages/ProfilePage.tsx b/src/Pages/ProfilePage.tsx index 8ad0e744..6871f065 100644 --- a/src/Pages/ProfilePage.tsx +++ b/src/Pages/ProfilePage.tsx @@ -6,7 +6,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faGear, faEnvelope } from "@fortawesome/free-solid-svg-icons"; import { useNavigate, useParams } from "react-router-dom"; -import useProfile from "Feed/ProfileFeed"; +import { useUserProfile } from "Feed/ProfileFeed"; import FollowButton from "Element/FollowButton"; import { extractLnAddress, parseId, hexToBech32 } from "Util"; import Avatar from "Element/Avatar"; @@ -33,7 +33,7 @@ export default function ProfilePage() { const params = useParams(); const navigate = useNavigate(); const id = useMemo(() => parseId(params.id!), [params]); - const user = useProfile(id)?.get(id); + const user = useUserProfile(id); const loginPubKey = useSelector(s => s.login.publicKey); const follows = useSelector(s => s.login.follows); const isMe = loginPubKey === id; diff --git a/src/Pages/settings/Profile.tsx b/src/Pages/settings/Profile.tsx index cd5b3cd7..57fbfb60 100644 --- a/src/Pages/settings/Profile.tsx +++ b/src/Pages/settings/Profile.tsx @@ -8,7 +8,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faShop } from "@fortawesome/free-solid-svg-icons"; import useEventPublisher from "Feed/EventPublisher"; -import useProfile from "Feed/ProfileFeed"; +import { useUserProfile } from "Feed/ProfileFeed"; import VoidUpload from "Feed/VoidUpload"; import { logout } from "State/Login"; import { hexToBech32, openFile } from "Util"; @@ -21,7 +21,7 @@ export default function ProfileSettings() { const id = useSelector(s => s.login.publicKey); const privKey = useSelector(s => s.login.privateKey); const dispatch = useDispatch(); - const user = useProfile(id)?.get(id || ""); + const user = useUserProfile(id!); const publisher = useEventPublisher(); const [name, setName] = useState(); diff --git a/src/State/Store.ts b/src/State/Store.ts index 1c997610..74a82d6b 100644 --- a/src/State/Store.ts +++ b/src/State/Store.ts @@ -1,13 +1,15 @@ import { configureStore } from "@reduxjs/toolkit"; import { reducer as LoginReducer } from "State/Login"; +import { reducer as UsersReducer } from "State/Users"; const store = configureStore({ reducer: { - login: LoginReducer + login: LoginReducer, + users: UsersReducer, } }); export type RootState = ReturnType export type AppDispatch = typeof store.dispatch -export default store; \ No newline at end of file +export default store; diff --git a/src/State/Users.ts b/src/State/Users.ts new file mode 100644 index 00000000..5932fbe6 --- /dev/null +++ b/src/State/Users.ts @@ -0,0 +1,225 @@ +import { useMemo } from "react"; +import { createSlice, PayloadAction } from '@reduxjs/toolkit' +import { useLiveQuery } from "dexie-react-hooks"; +import { HexKey, TaggedRawEvent, UserMetadata } from "Nostr"; +import { hexToBech32 } from "../Util"; +import { db } from "Db"; +import store from "State/Store"; + +export interface MetadataCache extends UserMetadata { + /** + * When the object was saved in cache + */ + loaded: number, + + /** + * When the source metadata event was created + */ + created: number, + + /** + * The pubkey of the owner of this metadata + */ + pubkey: HexKey + + /** + * The bech32 encoded pubkey + */ + npub: string +}; + +export function mapEventToProfile(ev: TaggedRawEvent) { + try { + let data: UserMetadata = JSON.parse(ev.content); + return { + pubkey: ev.pubkey, + npub: hexToBech32("npub", ev.pubkey), + created: ev.created_at, + loaded: new Date().getTime(), + ...data + } as MetadataCache; + } catch (e) { + console.error("Failed to parse JSON", ev, e); + } +} + +export interface UsersStore { + /** + * A list of seen users + */ + users: Record, +}; + +const InitState = { users: {} } as UsersStore; + +const UsersSlice = createSlice({ + name: "Users", + initialState: InitState, + reducers: { + setUsers(state, action: PayloadAction>) { + state.users = action.payload + } + } +}); + +const { setUsers } = UsersSlice.actions + +function groupByPubkey(acc: Record, user: MetadataCache) { + return { ...acc, [user.pubkey]: user } +} + +function groupByPubkeyMap(acc: Map, user: MetadataCache) { + acc.set(user.pubkey, user) + return acc +} + +export const add = async (user: MetadataCache) => { + try { + return await db.users.add(user) + } catch (error) { + const state = store.getState() + const { users } = state.users + store.dispatch(setUsers({...users, [user.pubkey]: user })) + } +} + +export const bulkAdd = async (newUserProfiles: MetadataCache[]) => { + try { + return await db.users.bulkAdd(newUserProfiles) + } catch (error) { + const state = store.getState() + const { users } = state.users + const newUsers = newUserProfiles.reduce(groupByPubkey, {}) + store.dispatch(setUsers({...users, ...newUsers })) + } +} + +export const bulkGet = async (pubKeys: HexKey[]) => { + try { + const ret = await db.users.bulkGet(pubKeys); + return ret.filter(a => a !== undefined).map(a => a!); + } catch (error) { + const state = store.getState() + const { users } = state.users + const ids = new Set([...pubKeys]) + return Object.values(users).filter(user => { + return ids.has(user.pubkey) + }) + } +} + +export const find = async (pubKey: HexKey) => { + try { + const user = await db.users.get(pubKey); + return user + } catch (error) { + const { users } = store.getState() + return users.users[pubKey] + } +} + +export const put = async (user: MetadataCache) => { + try { + await db.users.put(user) + } catch (error) { + const state = store.getState() + const { users } = state.users + store.dispatch(setUsers({...users, [user.pubkey]: user })) + } +} + +export const update = async (pubKey: HexKey, fields: Record) => { + try { + await db.users.update(pubKey, fields) + } catch (error) { + const state = store.getState() + const { users } = state.users + const current = users[pubKey] + store.dispatch(setUsers({...users, [pubKey]: {...current, ...fields }})) + } +} + +export const bulkPut = async (newUsers: MetadataCache[]) => { + try { + await db.users.bulkPut(newUsers) + } catch (error) { + const state = store.getState() + const { users } = state.users + const newProfiles = newUsers.reduce(groupByPubkey, {}) + store.dispatch(setUsers({ ...users, ...newProfiles })) + } +} + +export function useQuery(query: string, limit: number = 5) { + const state = store.getState() + const { users } = state.users + + const inMemoryUsers = useMemo(() => { + return Object.values(users).filter((user) => { + return user.name?.includes(query) + || user.npub?.includes(query) + || user.display_name?.includes(query) + || user.nip05?.includes(query) + }) + }, [users, query]) + + const allUsers = useLiveQuery( + () => db.users + .where("npub").startsWithIgnoreCase(query) + .or("name").startsWithIgnoreCase(query) + .or("display_name").startsWithIgnoreCase(query) + .or("nip05").startsWithIgnoreCase(query) + .limit(5) + .toArray() + .catch((err) => { + return inMemoryUsers + }), + [query], + ) + + return allUsers +} + +export function useKey(pubKey: HexKey) { + const state = store.getState() + const { users } = state.users + + const inMemoryUser = useMemo(() => { + return users[pubKey] + }, [users, pubKey]) + + const user = useLiveQuery(async () => { + if (pubKey) { + return await find(pubKey); + } + }, [pubKey]); + + return user ?? inMemoryUser +} + +export function useKeys(pubKeys: HexKey[]): Map { + const state = store.getState() + const { users } = state.users + + const inMemoryUsers = useMemo(() => { + const res = new Map() + Object.values(users).forEach(u => { + if (pubKeys.includes(u.pubkey)) { + res.set(u.pubkey, u) + } + }) + return res + }, [users, pubKeys]) + + const dbUsers = useLiveQuery(async () => { + if (pubKeys) { + const ret = await bulkGet(pubKeys); + return new Map(ret.map(a => [a.pubkey, a])) + } + return new Map() + }, [pubKeys]); + + return dbUsers || inMemoryUsers +} + +export const reducer = UsersSlice.reducer;