wip: In-memory fallback for user state
This commit is contained in:
parent
eb2137b16f
commit
a8348597e2
@ -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);
|
||||
}
|
||||
}
|
@ -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<MetadataCache>;
|
||||
|
||||
|
@ -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 <Link to={profileLink(pubkey)} onClick={(e) => e.stopPropagation()}>@{name}</Link>
|
||||
}
|
||||
}
|
||||
|
@ -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<ReduxStore, string>(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<ServiceConfig>();
|
||||
@ -190,4 +190,4 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
||||
</div>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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" />
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
</>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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";
|
||||
@ -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)
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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: <MetadataCache | null>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]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<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,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<HexKey>();
|
||||
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();
|
||||
export const System = new NostrSystem();
|
||||
|
@ -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";
|
||||
@ -21,7 +21,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,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<typeof store.getState>
|
||||
export type AppDispatch = typeof store.dispatch
|
||||
|
||||
export default store;
|
||||
export default store;
|
||||
|
225
src/State/Users.ts
Normal file
225
src/State/Users.ts
Normal file
@ -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<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
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const { setUsers } = UsersSlice.actions
|
||||
|
||||
function groupByPubkey(acc: Record<HexKey, MetadataCache>, user: MetadataCache) {
|
||||
return { ...acc, [user.pubkey]: user }
|
||||
}
|
||||
|
||||
function groupByPubkeyMap(acc: Map<HexKey, MetadataCache>, 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<string, any>) => {
|
||||
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<HexKey, MetadataCache> {
|
||||
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;
|
Loading…
Reference in New Issue
Block a user