feat: in-memory fallback for storing user profiles (#110)
This commit is contained in:
parent
2e3fe92b1f
commit
d070185322
@ -1,39 +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,
|
||||
|
||||
/**
|
||||
* bech32 encoded pub key
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
@ -1,16 +1,20 @@
|
||||
import Dexie, { Table } from "dexie";
|
||||
import { MetadataCache } from "Db/User";
|
||||
import { MetadataCache } from "State/Users";
|
||||
import { hexToBech32 } from "Util";
|
||||
|
||||
export const NAME = 'snortDB'
|
||||
export const VERSION = 2
|
||||
|
||||
const STORES = {
|
||||
users: '++pubkey, name, display_name, picture, nip05, npub'
|
||||
}
|
||||
|
||||
export class SnortDB extends Dexie {
|
||||
users!: Table<MetadataCache>;
|
||||
|
||||
constructor() {
|
||||
super('snortDB');
|
||||
this.version(2).stores({
|
||||
users: '++pubkey, name, display_name, picture, nip05, npub'
|
||||
}).upgrade(tx => {
|
||||
super(NAME);
|
||||
this.version(VERSION).stores(STORES).upgrade(tx => {
|
||||
return tx.table("users").toCollection().modify(user => {
|
||||
user.npub = hexToBech32("npub", user.pubkey)
|
||||
})
|
||||
|
@ -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);
|
||||
|
@ -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 { debounce, 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<ReduxStore, string>(s => s.login.publicKey);
|
||||
const user = useProfile(pubkey);
|
||||
const user = useUserProfile(pubkey);
|
||||
const publisher = useEventPublisher();
|
||||
const svc = useMemo(() => new ServiceProvider(props.service), [props.service]);
|
||||
const [serviceConfig, setServiceConfig] = useState<ServiceConfig>();
|
||||
|
@ -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 });
|
||||
|
||||
|
@ -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<RootState, HexKey | undefined>(s => s.login.publicKey);
|
||||
const prefs = useSelector<RootState, UserPreferences>(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);
|
||||
|
@ -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 (
|
||||
<div>
|
||||
Note to Self <FontAwesomeIcon icon={faCertificate} size="xs" />
|
||||
|
@ -1,13 +1,13 @@
|
||||
import "./ProfileImage.css";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import useProfile from "Feed/ProfileFeed";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
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);
|
||||
|
@ -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,
|
||||
|
@ -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';
|
||||
|
@ -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";
|
||||
@ -10,8 +9,8 @@ import TextareaAutosize from "react-textarea-autosize";
|
||||
import Avatar from "Element/Avatar";
|
||||
import Nip05 from "Element/Nip05";
|
||||
import { hexToBech32 } from "Util";
|
||||
import { db } from "Db";
|
||||
import { MetadataCache } from "Db/User";
|
||||
import { MetadataCache } from "State/Users";
|
||||
import { useQuery } from "State/Users/Hooks";
|
||||
|
||||
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)
|
||||
|
@ -2,12 +2,13 @@ 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, svc }: { pubkey?: HexKey, svc?: string }) => {
|
||||
const profile = useProfile(pubkey)?.get(pubkey ?? "");
|
||||
const profile = useUserProfile(pubkey!)
|
||||
const [zap, setZap] = useState(false);
|
||||
const service = svc ?? (profile?.lud16 || profile?.lud06);
|
||||
|
||||
|
@ -7,9 +7,9 @@ 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 } from "State/Users";
|
||||
import { getDb } from "State/Users/Db";
|
||||
import useSubscription from "Feed/Subscription";
|
||||
import { mapEventToProfile, MetadataCache } from "Db/User";
|
||||
import { getDisplayName } from "Element/ProfileImage";
|
||||
import { MentionRegex } from "Const";
|
||||
|
||||
@ -87,9 +87,10 @@ export default function useLoginFeed() {
|
||||
return acc;
|
||||
}, { created: 0, profile: <MetadataCache | null>null });
|
||||
if (maxProfile.profile) {
|
||||
let existing = await db.users.get(maxProfile.profile.pubkey);
|
||||
const db = getDb()
|
||||
let existing = await db.find(maxProfile.profile.pubkey);
|
||||
if ((existing?.created ?? 0) < maxProfile.created) {
|
||||
await db.users.put(maxProfile.profile);
|
||||
await db.put(maxProfile.profile);
|
||||
}
|
||||
}
|
||||
})().catch(console.warn);
|
||||
@ -115,10 +116,12 @@ export default function useLoginFeed() {
|
||||
}
|
||||
|
||||
async function makeNotification(ev: TaggedRawEvent) {
|
||||
const db = getDb()
|
||||
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 db.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;
|
||||
|
@ -1,27 +1,13 @@
|
||||
import { useLiveQuery } from "dexie-react-hooks";
|
||||
import { useEffect } from "react";
|
||||
import { db } from "Db";
|
||||
import { MetadataCache } from "Db/User";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { RootState } from "State/Store";
|
||||
import { MetadataCache } from "State/Users";
|
||||
import { useKey, useKeys } from "State/Users/Hooks";
|
||||
import { HexKey } from "Nostr";
|
||||
import { System } from "Nostr/System";
|
||||
|
||||
export default function useProfile(pubKey?: HexKey | Array<HexKey> | undefined): Map<HexKey, MetadataCache> | undefined {
|
||||
const user = useLiveQuery(async () => {
|
||||
let userList = new Map<HexKey, MetadataCache>();
|
||||
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<HexKey> | undefined):
|
||||
}
|
||||
}, [pubKey]);
|
||||
|
||||
return user;
|
||||
return users;
|
||||
}
|
||||
|
||||
|
||||
export function useUserProfiles(pubKeys: Array<HexKey>): Map<HexKey, MetadataCache> | undefined {
|
||||
const users = useKeys(pubKeys);
|
||||
|
||||
useEffect(() => {
|
||||
if (pubKeys) {
|
||||
System.TrackMetadata(pubKeys);
|
||||
return () => System.UntrackMetadata(pubKeys);
|
||||
}
|
||||
}, [pubKeys]);
|
||||
|
||||
return users;
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { HexKey, TaggedRawEvent } from "Nostr";
|
||||
import { ProfileCacheExpire } from "Const";
|
||||
import { db } from "Db";
|
||||
import { mapEventToProfile, MetadataCache } from "Db/User";
|
||||
import { mapEventToProfile, MetadataCache, } from "State/Users";
|
||||
import { getDb } from "State/Users/Db";
|
||||
import Connection, { RelaySettings } from "Nostr/Connection";
|
||||
import Event from "Nostr/Event";
|
||||
import EventKind from "Nostr/EventKind";
|
||||
@ -167,7 +167,8 @@ export class NostrSystem {
|
||||
|
||||
async _FetchMetadata() {
|
||||
let missing = new Set<HexKey>();
|
||||
let meta = await db.users.bulkGet(Array.from(this.WantsMetadata));
|
||||
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);
|
||||
@ -190,18 +191,18 @@ export class NostrSystem {
|
||||
sub.OnEvent = async (e) => {
|
||||
let profile = mapEventToProfile(e);
|
||||
if (profile) {
|
||||
let existing = await db.users.get(profile.pubkey);
|
||||
let existing = await db.find(profile.pubkey);
|
||||
if((existing?.created ?? 0) < profile.created) {
|
||||
await db.users.put(profile);
|
||||
await db.put(profile);
|
||||
} else if(existing) {
|
||||
await db.users.update(profile.pubkey, { loaded: new Date().getTime() });
|
||||
await db.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 db.bulkPut(couldNotFetch.map(a => {
|
||||
return {
|
||||
pubkey: a,
|
||||
loaded: new Date().getTime()
|
||||
|
@ -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<RootState, HexKey | undefined>(s => s.login.publicKey);
|
||||
const follows = useSelector<RootState, HexKey[]>(s => s.login.follows);
|
||||
const isMe = loginPubKey === id;
|
||||
|
@ -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";
|
||||
@ -22,7 +22,7 @@ export default function ProfileSettings() {
|
||||
const id = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
|
||||
const privKey = useSelector<RootState, HexKey | undefined>(s => s.login.privateKey);
|
||||
const dispatch = useDispatch();
|
||||
const user = useProfile(id)?.get(id || "");
|
||||
const user = useUserProfile(id!);
|
||||
const publisher = useEventPublisher();
|
||||
|
||||
const [name, setName] = useState<string>();
|
||||
|
@ -1,9 +1,11 @@
|
||||
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,
|
||||
}
|
||||
});
|
||||
|
||||
|
75
src/State/Users.ts
Normal file
75
src/State/Users.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
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
|
||||
|
||||
/**
|
||||
* 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 UsersDb {
|
||||
isAvailable(): Promise<boolean>
|
||||
query(str: string): Promise<MetadataCache[]>
|
||||
find(key: HexKey): Promise<MetadataCache | undefined>
|
||||
add(user: MetadataCache): Promise<any>
|
||||
put(user: MetadataCache): Promise<any>
|
||||
bulkAdd(users: MetadataCache[]): Promise<any>
|
||||
bulkGet(keys: HexKey[]): Promise<MetadataCache[]>
|
||||
bulkPut(users: MetadataCache[]): Promise<any>
|
||||
update(key: HexKey, fields: Record<string, any>): Promise<any>
|
||||
}
|
||||
|
||||
export interface UsersStore {
|
||||
/**
|
||||
* A list of seen users
|
||||
*/
|
||||
users: Record<HexKey, MetadataCache>,
|
||||
};
|
||||
|
||||
const InitState = { users: {} } as UsersStore;
|
||||
|
||||
const UsersSlice = createSlice({
|
||||
name: "Users",
|
||||
initialState: InitState,
|
||||
reducers: {
|
||||
setUsers(state, action: PayloadAction<Record<HexKey, MetadataCache>>) {
|
||||
state.users = action.payload
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const { setUsers } = UsersSlice.actions
|
||||
|
||||
export const reducer = UsersSlice.reducer;
|
149
src/State/Users/Db.ts
Normal file
149
src/State/Users/Db.ts
Normal file
@ -0,0 +1,149 @@
|
||||
import { HexKey } from "Nostr";
|
||||
import { db as idb } from "Db";
|
||||
|
||||
import { UsersDb, MetadataCache, setUsers } from "State/Users";
|
||||
import store from "State/Store";
|
||||
|
||||
class IndexedDb implements UsersDb {
|
||||
isAvailable() {
|
||||
if ("indexedDB" in window) {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
const req = window.indexedDB.open("dummy", 1)
|
||||
req.onsuccess = (ev) => {
|
||||
resolve(true)
|
||||
}
|
||||
req.onerror = (ev) => {
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
return Promise.resolve(false)
|
||||
}
|
||||
|
||||
find(key: HexKey) {
|
||||
return idb.users.get(key);
|
||||
}
|
||||
|
||||
query(q: string) {
|
||||
return idb.users
|
||||
.where("npub").startsWithIgnoreCase(q)
|
||||
.or("name").startsWithIgnoreCase(q)
|
||||
.or("display_name").startsWithIgnoreCase(q)
|
||||
.or("nip05").startsWithIgnoreCase(q)
|
||||
.limit(5)
|
||||
.toArray()
|
||||
}
|
||||
|
||||
bulkGet(keys: HexKey[]) {
|
||||
return idb.users.bulkGet(keys).then(ret => ret.filter(a => a !== undefined).map(a => a!));
|
||||
}
|
||||
|
||||
add(user: MetadataCache) {
|
||||
return idb.users.add(user)
|
||||
}
|
||||
|
||||
put(user: MetadataCache) {
|
||||
return idb.users.put(user)
|
||||
}
|
||||
|
||||
bulkAdd(users: MetadataCache[]) {
|
||||
return idb.users.bulkAdd(users)
|
||||
}
|
||||
|
||||
bulkPut(users: MetadataCache[]) {
|
||||
return idb.users.bulkPut(users)
|
||||
}
|
||||
|
||||
update(key: HexKey, fields: Record<string, any>) {
|
||||
return idb.users.update(key, fields)
|
||||
}
|
||||
}
|
||||
|
||||
function groupByPubkey(acc: Record<HexKey, MetadataCache>, user: MetadataCache) {
|
||||
return { ...acc, [user.pubkey]: user }
|
||||
}
|
||||
|
||||
class ReduxUsersDb implements UsersDb {
|
||||
async isAvailable() { return true }
|
||||
|
||||
async query(q: string) {
|
||||
const state = store.getState()
|
||||
const { users } = state.users
|
||||
return Object.values(users).filter(user => {
|
||||
const profile = user as MetadataCache
|
||||
return profile.name?.includes(q)
|
||||
|| profile.npub?.includes(q)
|
||||
|| profile.display_name?.includes(q)
|
||||
|| profile.nip05?.includes(q)
|
||||
})
|
||||
}
|
||||
|
||||
async find(key: HexKey) {
|
||||
const state = store.getState()
|
||||
const { users } = state.users
|
||||
return users[key]
|
||||
}
|
||||
|
||||
|
||||
async add(user: MetadataCache) {
|
||||
const state = store.getState()
|
||||
const { users } = state.users
|
||||
store.dispatch(setUsers({...users, [user.pubkey]: user }))
|
||||
}
|
||||
|
||||
|
||||
async put(user: MetadataCache) {
|
||||
const state = store.getState()
|
||||
const { users } = state.users
|
||||
store.dispatch(setUsers({...users, [user.pubkey]: user }))
|
||||
}
|
||||
|
||||
async bulkAdd(newUserProfiles: MetadataCache[]) {
|
||||
const state = store.getState()
|
||||
const { users } = state.users
|
||||
const newUsers = newUserProfiles.reduce(groupByPubkey, {})
|
||||
store.dispatch(setUsers({...users, ...newUsers }))
|
||||
}
|
||||
|
||||
async bulkGet(keys: HexKey[]) {
|
||||
const state = store.getState()
|
||||
const { users } = state.users
|
||||
const ids = new Set([...keys])
|
||||
return Object.values(users).filter(user => {
|
||||
return ids.has(user.pubkey)
|
||||
})
|
||||
}
|
||||
|
||||
async update(key: HexKey, fields: Record<string, any>) {
|
||||
const state = store.getState()
|
||||
const { users } = state.users
|
||||
const current = users[key]
|
||||
const updated = {...current, ...fields }
|
||||
store.dispatch(setUsers({...users, [key]: updated }))
|
||||
}
|
||||
|
||||
async bulkPut(newUsers: MetadataCache[]) {
|
||||
const state = store.getState()
|
||||
const { users } = state.users
|
||||
const newProfiles = newUsers.reduce(groupByPubkey, {})
|
||||
store.dispatch(setUsers({ ...users, ...newProfiles }))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const indexedDb = new IndexedDb()
|
||||
export const inMemoryDb = new ReduxUsersDb()
|
||||
|
||||
let db: UsersDb = inMemoryDb
|
||||
indexedDb.isAvailable().then((available) => {
|
||||
if (available) {
|
||||
console.debug('Using Indexed DB')
|
||||
db = indexedDb;
|
||||
} else {
|
||||
console.debug('Using in-memory DB')
|
||||
}
|
||||
})
|
||||
|
||||
export function getDb() {
|
||||
return db
|
||||
}
|
60
src/State/Users/Hooks.ts
Normal file
60
src/State/Users/Hooks.ts
Normal file
@ -0,0 +1,60 @@
|
||||
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";
|
||||
|
||||
export function useQuery(query: string, limit: number = 5) {
|
||||
const db = getDb()
|
||||
|
||||
const allUsers = useLiveQuery(
|
||||
() => db.query(query)
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
}).then(() => {
|
||||
return inMemoryDb.query(query)
|
||||
}),
|
||||
[query],
|
||||
)
|
||||
|
||||
return allUsers
|
||||
}
|
||||
|
||||
export function useKey(pubKey: HexKey) {
|
||||
const db = getDb()
|
||||
const { users } = useSelector((state: RootState) => state.users)
|
||||
const defaultUser = users[pubKey]
|
||||
|
||||
const user = useLiveQuery(async () => {
|
||||
if (pubKey) {
|
||||
try {
|
||||
return await db.find(pubKey);
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return defaultUser
|
||||
}
|
||||
}
|
||||
}, [pubKey, defaultUser]);
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
export function useKeys(pubKeys: HexKey[]): Map<HexKey, MetadataCache> {
|
||||
const db = getDb()
|
||||
const dbUsers = useLiveQuery(async () => {
|
||||
if (pubKeys) {
|
||||
try {
|
||||
const ret = await db.bulkGet(pubKeys);
|
||||
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]);
|
||||
|
||||
return dbUsers!
|
||||
}
|
Loading…
Reference in New Issue
Block a user