feat: improve profile cache (again)
This commit is contained in:
parent
27edf5f592
commit
32549522d4
@ -17,12 +17,28 @@ const STORES = {
|
||||
};
|
||||
|
||||
export class SnortDB extends Dexie {
|
||||
ready = false;
|
||||
users!: Table<MetadataCache>;
|
||||
|
||||
constructor() {
|
||||
super(NAME);
|
||||
this.version(VERSION).stores(STORES);
|
||||
}
|
||||
|
||||
isAvailable() {
|
||||
if ("indexedDB" in window) {
|
||||
return new Promise<boolean>(resolve => {
|
||||
const req = window.indexedDB.open("dummy", 1);
|
||||
req.onsuccess = () => {
|
||||
resolve(true);
|
||||
};
|
||||
req.onerror = () => {
|
||||
resolve(false);
|
||||
};
|
||||
});
|
||||
}
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
}
|
||||
|
||||
export const db = new SnortDB();
|
||||
|
@ -1,7 +1,9 @@
|
||||
import "./Avatar.css";
|
||||
import Nostrich from "nostrich.webp";
|
||||
|
||||
import { CSSProperties, useEffect, useState } from "react";
|
||||
import type { UserMetadata } from "@snort/nostr";
|
||||
|
||||
import useImgProxy from "Hooks/useImgProxy";
|
||||
|
||||
const Avatar = ({ user, ...rest }: { user?: UserMetadata; onClick?: () => void }) => {
|
||||
|
@ -5,8 +5,8 @@ import { FormattedMessage } from "react-intl";
|
||||
import { dedupeByPubkey } from "Util";
|
||||
import Note from "Element/Note";
|
||||
import { HexKey, TaggedRawEvent } from "@snort/nostr";
|
||||
import { useUserProfiles } from "Feed/ProfileFeed";
|
||||
import { RootState } from "State/Store";
|
||||
import { UserCache } from "State/Users/UserCache";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
@ -22,10 +22,9 @@ const Bookmarks = ({ pubkey, bookmarks, related }: BookmarksProps) => {
|
||||
const ps = useMemo(() => {
|
||||
return dedupeByPubkey(bookmarks).map(ev => ev.pubkey);
|
||||
}, [bookmarks]);
|
||||
const profiles = useUserProfiles(ps);
|
||||
|
||||
function renderOption(p: HexKey) {
|
||||
const profile = profiles?.get(p);
|
||||
const profile = UserCache.get(p);
|
||||
return profile ? <option value={p}>{profile?.display_name || profile?.name}</option> : null;
|
||||
}
|
||||
|
||||
|
@ -54,7 +54,7 @@ export default function DM(props: DMProps) {
|
||||
<NoteTime from={props.data.created_at * 1000} fallback={formatMessage(messages.JustNow)} />
|
||||
</div>
|
||||
<div className="w-max">
|
||||
<Text content={content} tags={[]} users={new Map()} creator={otherPubkey} />
|
||||
<Text content={content} tags={[]} creator={otherPubkey} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useMemo } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useUserProfile } from "Feed/ProfileFeed";
|
||||
import { useUserProfile } from "Hooks/useUserProfile";
|
||||
import { HexKey } from "@snort/nostr";
|
||||
import { hexToBech32, profileLink } from "Util";
|
||||
|
||||
|
@ -17,7 +17,7 @@ import {
|
||||
import AsyncButton from "Element/AsyncButton";
|
||||
import SendSats from "Element/SendSats";
|
||||
import Copy from "Element/Copy";
|
||||
import { useUserProfile } from "Feed/ProfileFeed";
|
||||
import { useUserProfile } from "Hooks/useUserProfile";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import { debounce } from "Util";
|
||||
import { UserMetadata } from "@snort/nostr";
|
||||
|
@ -18,14 +18,15 @@ import {
|
||||
hexToBech32,
|
||||
normalizeReaction,
|
||||
Reaction,
|
||||
profileLink,
|
||||
} from "Util";
|
||||
import NoteFooter, { Translation } from "Element/NoteFooter";
|
||||
import NoteTime from "Element/NoteTime";
|
||||
import { useUserProfiles } from "Feed/ProfileFeed";
|
||||
import { TaggedRawEvent, u256, HexKey, Event as NEvent, EventKind } from "@snort/nostr";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
import { setPinned, setBookmarked } from "State/Login";
|
||||
import type { RootState } from "State/Store";
|
||||
import { UserCache } from "State/Users/UserCache";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
@ -72,8 +73,6 @@ export default function Note(props: NoteProps) {
|
||||
const { data, related, highlight, options: opt, ["data-ev"]: parsedEvent, ignoreModeration = false } = props;
|
||||
const [showReactions, setShowReactions] = useState(false);
|
||||
const ev = useMemo(() => parsedEvent ?? new NEvent(data), [data]);
|
||||
const pubKeys = useMemo(() => ev.Thread?.PubKeys || [], [ev]);
|
||||
const users = useUserProfiles(pubKeys);
|
||||
const deletions = useMemo(() => getReactions(related, ev.Id, EventKind.Deletion), [related]);
|
||||
const { isMuted } = useModeration();
|
||||
const isOpMuted = isMuted(ev.PubKey);
|
||||
@ -162,7 +161,7 @@ export default function Note(props: NoteProps) {
|
||||
</b>
|
||||
);
|
||||
}
|
||||
return <Text content={body} tags={ev.Tags} users={users || new Map()} creator={ev.PubKey} />;
|
||||
return <Text content={body} tags={ev.Tags} creator={ev.PubKey} />;
|
||||
}, [ev]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
@ -188,22 +187,14 @@ export default function Note(props: NoteProps) {
|
||||
const replyId = ev.Thread?.ReplyTo?.Event ?? ev.Thread?.Root?.Event;
|
||||
const mentions: { pk: string; name: string; link: ReactNode }[] = [];
|
||||
for (const pk of ev.Thread?.PubKeys ?? []) {
|
||||
const u = users?.get(pk);
|
||||
const u = UserCache.get(pk);
|
||||
const npub = hexToBech32("npub", pk);
|
||||
const shortNpub = npub.substring(0, 12);
|
||||
if (u) {
|
||||
mentions.push({
|
||||
pk,
|
||||
name: u.name ?? shortNpub,
|
||||
link: <Link to={`/p/${npub}`}>{u.name ? `@${u.name}` : shortNpub}</Link>,
|
||||
});
|
||||
} else {
|
||||
mentions.push({
|
||||
pk,
|
||||
name: shortNpub,
|
||||
link: <Link to={`/p/${npub}`}>{shortNpub}</Link>,
|
||||
});
|
||||
}
|
||||
mentions.push({
|
||||
pk,
|
||||
name: u?.name ?? shortNpub,
|
||||
link: <Link to={profileLink(pk)}>{u?.name ? `@${u.name}` : shortNpub}</Link>,
|
||||
});
|
||||
}
|
||||
mentions.sort(a => (a.name.startsWith("npub") ? 1 : -1));
|
||||
const othersLength = mentions.length - maxMentions;
|
||||
|
@ -15,7 +15,7 @@ import { NoteCreator } from "Element/NoteCreator";
|
||||
import Reactions from "Element/Reactions";
|
||||
import SendSats from "Element/SendSats";
|
||||
import { ParsedZap, ZapsSummary } from "Element/Zap";
|
||||
import { useUserProfile } from "Feed/ProfileFeed";
|
||||
import { useUserProfile } from "Hooks/useUserProfile";
|
||||
import { RootState } from "State/Store";
|
||||
import { UserPreferences, setPinned, setBookmarked } from "State/Login";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
|
@ -2,7 +2,7 @@ import "./ProfileImage.css";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { useUserProfile } from "Feed/ProfileFeed";
|
||||
import { useUserProfile } from "Hooks/useUserProfile";
|
||||
import { hexToBech32, profileLink } from "Util";
|
||||
import Avatar from "Element/Avatar";
|
||||
import Nip05 from "Element/Nip05";
|
||||
|
@ -3,7 +3,7 @@ import { ReactNode } from "react";
|
||||
|
||||
import ProfileImage from "Element/ProfileImage";
|
||||
import FollowButton from "Element/FollowButton";
|
||||
import { useUserProfile } from "Feed/ProfileFeed";
|
||||
import { useUserProfile } from "Hooks/useUserProfile";
|
||||
import { HexKey } from "@snort/nostr";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
|
||||
|
@ -10,7 +10,6 @@ import Invoice from "Element/Invoice";
|
||||
import Hashtag from "Element/Hashtag";
|
||||
|
||||
import { Tag } from "@snort/nostr";
|
||||
import { MetadataCache } from "State/Users";
|
||||
import Mention from "Element/Mention";
|
||||
import HyperText from "Element/HyperText";
|
||||
import { HexKey } from "@snort/nostr";
|
||||
@ -21,17 +20,15 @@ export type Fragment = string | React.ReactNode;
|
||||
export interface TextFragment {
|
||||
body: React.ReactNode[];
|
||||
tags: Tag[];
|
||||
users: Map<string, MetadataCache>;
|
||||
}
|
||||
|
||||
export interface TextProps {
|
||||
content: string;
|
||||
creator: HexKey;
|
||||
tags: Tag[];
|
||||
users: Map<string, MetadataCache>;
|
||||
}
|
||||
|
||||
export default function Text({ content, tags, creator, users }: TextProps) {
|
||||
export default function Text({ content, tags, creator }: TextProps) {
|
||||
function extractLinks(fragments: Fragment[]) {
|
||||
return fragments
|
||||
.map(f => {
|
||||
@ -143,9 +140,9 @@ export default function Text({ content, tags, creator, users }: TextProps) {
|
||||
|
||||
const components = useMemo(() => {
|
||||
return {
|
||||
p: (x: { children?: React.ReactNode[] }) => transformParagraph({ body: x.children ?? [], tags, users }),
|
||||
p: (x: { children?: React.ReactNode[] }) => transformParagraph({ body: x.children ?? [], tags }),
|
||||
a: (x: { href?: string }) => <HyperText link={x.href ?? ""} creator={creator} />,
|
||||
li: (x: { children?: Fragment[] }) => transformLi({ body: x.children ?? [], tags, users }),
|
||||
li: (x: { children?: Fragment[] }) => transformLi({ body: x.children ?? [], tags }),
|
||||
};
|
||||
}, [content]);
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
import "@webscopeio/react-textarea-autocomplete/style.css";
|
||||
import "./Textarea.css";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import ReactTextareaAutocomplete from "@webscopeio/react-textarea-autocomplete";
|
||||
import emoji from "@jukben/emoji-search";
|
||||
@ -11,7 +10,7 @@ import Avatar from "Element/Avatar";
|
||||
import Nip05 from "Element/Nip05";
|
||||
import { hexToBech32 } from "Util";
|
||||
import { MetadataCache } from "State/Users";
|
||||
import { useQuery } from "State/Users/Hooks";
|
||||
import { UserCache } from "State/Users/UserCache";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
@ -53,14 +52,10 @@ interface TextareaProps {
|
||||
}
|
||||
|
||||
const Textarea = (props: TextareaProps) => {
|
||||
const [query, setQuery] = useState("");
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const allUsers = useQuery(query);
|
||||
|
||||
const userDataProvider = (token: string) => {
|
||||
setQuery(token);
|
||||
return allUsers ?? [];
|
||||
const userDataProvider = async (token: string) => {
|
||||
return await UserCache.search(token);
|
||||
};
|
||||
|
||||
const emojiDataProvider = (token: string) => {
|
||||
|
@ -109,7 +109,7 @@ const Zap = ({ zap, showZapped = true }: { zap: ParsedZap; showZapped?: boolean
|
||||
</div>
|
||||
{content.length > 0 && zapper && (
|
||||
<div className="body">
|
||||
<Text creator={zapper} content={content} tags={[]} users={new Map()} />
|
||||
<Text creator={zapper} content={content} tags={[]} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useState } from "react";
|
||||
import { HexKey } from "@snort/nostr";
|
||||
|
||||
import { useUserProfile } from "Feed/ProfileFeed";
|
||||
import { useUserProfile } from "Hooks/useUserProfile";
|
||||
import SendSats from "Element/SendSats";
|
||||
|
||||
const ZapButton = ({ pubkey, lnurl }: { pubkey: HexKey; lnurl?: string }) => {
|
||||
|
@ -18,14 +18,11 @@ import {
|
||||
setLatestNotifications,
|
||||
} from "State/Login";
|
||||
import { RootState } from "State/Store";
|
||||
import { mapEventToProfile, MetadataCache } from "State/Users";
|
||||
import useSubscription from "Feed/Subscription";
|
||||
import { barrierNip07 } from "Feed/EventPublisher";
|
||||
import { getMutedKeys } from "Feed/MuteList";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
import { unwrap } from "Util";
|
||||
import { AnyAction, ThunkDispatch } from "@reduxjs/toolkit";
|
||||
import { ReduxUDB } from "State/Users/Db";
|
||||
|
||||
/**
|
||||
* Managed loading data for the current logged in user
|
||||
@ -46,7 +43,7 @@ export default function useLoginFeed() {
|
||||
const sub = new Subscriptions();
|
||||
sub.Id = `login:meta`;
|
||||
sub.Authors = new Set([pubKey]);
|
||||
sub.Kinds = new Set([EventKind.ContactList, EventKind.SetMetadata]);
|
||||
sub.Kinds = new Set([EventKind.ContactList]);
|
||||
sub.Limit = 2;
|
||||
|
||||
return sub;
|
||||
@ -148,12 +145,6 @@ export default function useLoginFeed() {
|
||||
|
||||
useEffect(() => {
|
||||
const contactList = metadataFeed.store.notes.filter(a => a.kind === EventKind.ContactList);
|
||||
const metadata = metadataFeed.store.notes.filter(a => a.kind === EventKind.SetMetadata);
|
||||
const profiles = metadata
|
||||
.map(a => mapEventToProfile(a))
|
||||
.filter(a => a !== undefined)
|
||||
.map(a => unwrap(a));
|
||||
|
||||
for (const cl of contactList) {
|
||||
if (cl.content !== "" && cl.content !== "{}") {
|
||||
const relays = JSON.parse(cl.content);
|
||||
@ -162,26 +153,7 @@ export default function useLoginFeed() {
|
||||
const pTags = cl.tags.filter(a => a[0] === "p").map(a => a[1]);
|
||||
dispatch(setFollows({ keys: pTags, createdAt: cl.created_at }));
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const 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 existing = await ReduxUDB.find(maxProfile.profile.pubkey);
|
||||
if ((existing?.created ?? 0) < maxProfile.created) {
|
||||
await ReduxUDB.put(maxProfile.profile);
|
||||
}
|
||||
}
|
||||
})().catch(console.warn);
|
||||
}, [dispatch, metadataFeed.store, ReduxUDB]);
|
||||
}, [dispatch, metadataFeed.store]);
|
||||
|
||||
useEffect(() => {
|
||||
const replies = notificationFeed.store.notes.filter(
|
||||
@ -189,13 +161,13 @@ export default function useLoginFeed() {
|
||||
);
|
||||
replies.forEach(nx => {
|
||||
dispatch(setLatestNotifications(nx.created_at));
|
||||
makeNotification(ReduxUDB, nx).then(notification => {
|
||||
makeNotification(nx).then(notification => {
|
||||
if (notification) {
|
||||
(dispatch as ThunkDispatch<RootState, undefined, AnyAction>)(sendNotification(notification));
|
||||
}
|
||||
});
|
||||
});
|
||||
}, [dispatch, notificationFeed.store, ReduxUDB, readNotifications]);
|
||||
}, [dispatch, notificationFeed.store, readNotifications]);
|
||||
|
||||
useEffect(() => {
|
||||
const muted = getMutedKeys(mutedFeed.store.notes);
|
||||
|
@ -1,31 +0,0 @@
|
||||
import { useEffect } from "react";
|
||||
import { MetadataCache } from "State/Users";
|
||||
import { useKey, useKeys } from "State/Users/Hooks";
|
||||
import { HexKey } from "@snort/nostr";
|
||||
import { System } from "System";
|
||||
|
||||
export function useUserProfile(pubKey?: HexKey): MetadataCache | undefined {
|
||||
const users = useKey(pubKey);
|
||||
|
||||
useEffect(() => {
|
||||
if (pubKey) {
|
||||
System.TrackMetadata(pubKey);
|
||||
return () => System.UntrackMetadata(pubKey);
|
||||
}
|
||||
}, [pubKey]);
|
||||
|
||||
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;
|
||||
}
|
21
packages/app/src/Hooks/useUserProfile.ts
Normal file
21
packages/app/src/Hooks/useUserProfile.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { useEffect, useSyncExternalStore } from "react";
|
||||
import { MetadataCache } from "State/Users";
|
||||
import { HexKey } from "@snort/nostr";
|
||||
import { System } from "System";
|
||||
import { UserCache } from "State/Users/UserCache";
|
||||
|
||||
export function useUserProfile(pubKey?: HexKey): MetadataCache | undefined {
|
||||
const user = useSyncExternalStore<MetadataCache | undefined>(
|
||||
h => UserCache.hook(h, pubKey),
|
||||
() => UserCache.get(pubKey)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (pubKey) {
|
||||
System.TrackMetadata(pubKey);
|
||||
return () => System.UntrackMetadata(pubKey);
|
||||
}
|
||||
}, [pubKey]);
|
||||
|
||||
return user;
|
||||
}
|
@ -3,25 +3,30 @@ import Nostrich from "nostrich.webp";
|
||||
import { TaggedRawEvent } from "@snort/nostr";
|
||||
import { EventKind } from "@snort/nostr";
|
||||
import type { NotificationRequest } from "State/Login";
|
||||
import { MetadataCache, UsersDb } from "State/Users";
|
||||
import { MetadataCache } from "State/Users";
|
||||
import { getDisplayName } from "Element/ProfileImage";
|
||||
import { MentionRegex } from "Const";
|
||||
import { tagFilterOfTextRepost } from "Util";
|
||||
import { tagFilterOfTextRepost, unwrap } from "Util";
|
||||
import { UserCache } from "State/Users/UserCache";
|
||||
|
||||
export async function makeNotification(db: UsersDb, ev: TaggedRawEvent): Promise<NotificationRequest | null> {
|
||||
export async function makeNotification(ev: TaggedRawEvent): Promise<NotificationRequest | null> {
|
||||
switch (ev.kind) {
|
||||
case EventKind.TextNote: {
|
||||
if (ev.tags.some(tagFilterOfTextRepost(ev))) {
|
||||
return null;
|
||||
}
|
||||
const pubkeys = new Set([ev.pubkey, ...ev.tags.filter(a => a[0] === "p").map(a => a[1])]);
|
||||
const users = await db.bulkGet(Array.from(pubkeys));
|
||||
const fromUser = users.find(a => a?.pubkey === ev.pubkey);
|
||||
await UserCache.buffer([...pubkeys]);
|
||||
const allUsers = [...pubkeys]
|
||||
.map(a => UserCache.get(a))
|
||||
.filter(a => a)
|
||||
.map(a => unwrap(a));
|
||||
const fromUser = UserCache.get(ev.pubkey);
|
||||
const name = getDisplayName(fromUser, ev.pubkey);
|
||||
const avatarUrl = fromUser?.picture || Nostrich;
|
||||
return {
|
||||
title: `Reply from ${name}`,
|
||||
body: replaceTagsWithUser(ev, users).substring(0, 50),
|
||||
body: replaceTagsWithUser(ev, allUsers).substring(0, 50),
|
||||
icon: avatarUrl,
|
||||
timestamp: ev.created_at * 1000,
|
||||
};
|
||||
|
@ -14,12 +14,13 @@ import { totalUnread } from "Pages/MessagesPage";
|
||||
import { SearchRelays, SnortPubKey } from "Const";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
import { IndexedUDB } from "State/Users/Db";
|
||||
import { bech32ToHex } from "Util";
|
||||
import { NoteCreator } from "Element/NoteCreator";
|
||||
import { RelaySettings } from "@snort/nostr";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import messages from "./messages";
|
||||
import { db } from "Db";
|
||||
import { UserCache } from "State/Users/UserCache";
|
||||
|
||||
export default function Layout() {
|
||||
const location = useLocation();
|
||||
@ -119,16 +120,13 @@ export default function Layout() {
|
||||
|
||||
useEffect(() => {
|
||||
// check DB support then init
|
||||
IndexedUDB.isAvailable().then(async a => {
|
||||
const dbType = a ? "indexdDb" : "redux";
|
||||
|
||||
// cleanup on load
|
||||
if (dbType === "indexdDb") {
|
||||
IndexedUDB.ready = true;
|
||||
db.isAvailable().then(async a => {
|
||||
db.ready = a;
|
||||
if (a) {
|
||||
await UserCache.preload();
|
||||
}
|
||||
|
||||
console.debug(`Using db: ${dbType}`);
|
||||
dispatch(init(dbType));
|
||||
console.debug(`Using db: ${a ? "IndexedDB" : "In-Memory"}`);
|
||||
dispatch(init());
|
||||
|
||||
try {
|
||||
if ("registerProtocolHandler" in window.navigator) {
|
||||
|
@ -18,7 +18,7 @@ import usePinnedFeed from "Feed/PinnedFeed";
|
||||
import useBookmarkFeed from "Feed/BookmarkFeed";
|
||||
import useFollowersFeed from "Feed/FollowersFeed";
|
||||
import useFollowsFeed from "Feed/FollowsFeed";
|
||||
import { useUserProfile } from "Feed/ProfileFeed";
|
||||
import { useUserProfile } from "Hooks/useUserProfile";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
import useZapsFeed from "Feed/ZapsFeed";
|
||||
import { default as ZapElement } from "Element/Zap";
|
||||
@ -69,7 +69,6 @@ export default function ProfilePage() {
|
||||
const about = Text({
|
||||
content: aboutText,
|
||||
tags: [],
|
||||
users: new Map(),
|
||||
creator: "",
|
||||
});
|
||||
const npub = !id?.startsWith("npub") ? hexToBech32("npub", id || undefined) : id;
|
||||
|
@ -7,7 +7,8 @@ import { debounce } from "Util";
|
||||
import { router } from "index";
|
||||
import { SearchRelays } from "Const";
|
||||
import { System } from "System";
|
||||
import { useQuery } from "State/Users/Hooks";
|
||||
import { MetadataCache } from "State/Users";
|
||||
import { UserCache } from "State/Users/UserCache";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
@ -16,12 +17,13 @@ const SearchPage = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const [search, setSearch] = useState<string>();
|
||||
const [keyword, setKeyword] = useState<string | undefined>(params.keyword);
|
||||
const allUsers = useQuery(keyword || "");
|
||||
const [allUsers, setAllUsers] = useState<MetadataCache[]>();
|
||||
|
||||
useEffect(() => {
|
||||
if (keyword) {
|
||||
// "navigate" changing only url
|
||||
router.navigate(`/search/${encodeURIComponent(keyword)}`);
|
||||
UserCache.search(keyword).then(v => setAllUsers(v));
|
||||
}
|
||||
}, [keyword]);
|
||||
|
||||
|
@ -8,7 +8,7 @@ import { services } from "Pages/Verification";
|
||||
import Nip5Service from "Element/Nip5Service";
|
||||
import ProfileImage from "Element/ProfileImage";
|
||||
import type { RootState } from "State/Store";
|
||||
import { useUserProfile } from "Feed/ProfileFeed";
|
||||
import { useUserProfile } from "Hooks/useUserProfile";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
|
@ -8,7 +8,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faShop } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import { useUserProfile } from "Feed/ProfileFeed";
|
||||
import { useUserProfile } from "Hooks/useUserProfile";
|
||||
import { hexToBech32, openFile } from "Util";
|
||||
import Copy from "Element/Copy";
|
||||
import { RootState } from "State/Store";
|
||||
|
@ -82,14 +82,7 @@ export interface UserPreferences {
|
||||
defaultZapAmount: number;
|
||||
}
|
||||
|
||||
export type DbType = "indexdDb" | "redux";
|
||||
|
||||
export interface LoginStore {
|
||||
/**
|
||||
* Which db we will use to cache data
|
||||
*/
|
||||
useDb: DbType;
|
||||
|
||||
/**
|
||||
* If there is no login
|
||||
*/
|
||||
@ -208,7 +201,6 @@ export const DefaultImgProxy = {
|
||||
};
|
||||
|
||||
export const InitState = {
|
||||
useDb: "redux",
|
||||
loggedOut: undefined,
|
||||
publicKey: undefined,
|
||||
privateKey: undefined,
|
||||
@ -267,8 +259,7 @@ const LoginSlice = createSlice({
|
||||
name: "Login",
|
||||
initialState: InitState,
|
||||
reducers: {
|
||||
init: (state, action: PayloadAction<DbType>) => {
|
||||
state.useDb = action.payload;
|
||||
init: state => {
|
||||
state.privateKey = window.localStorage.getItem(PrivateKeyItem) ?? undefined;
|
||||
if (state.privateKey) {
|
||||
window.localStorage.removeItem(PublicKeyItem); // reset nip07 if using private key
|
||||
|
@ -1,12 +1,10 @@
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import { reducer as LoginReducer } from "State/Login";
|
||||
import { reducer as UsersReducer } from "State/Users";
|
||||
import { reducer as CacheReducer } from "State/Cache";
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
login: LoginReducer,
|
||||
users: UsersReducer,
|
||||
cache: CacheReducer,
|
||||
},
|
||||
});
|
||||
|
@ -1,75 +0,0 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { HexKey, TaggedRawEvent, UserMetadata } from "@snort/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 {
|
||||
const 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<void>;
|
||||
put(user: MetadataCache): Promise<void>;
|
||||
bulkAdd(users: MetadataCache[]): Promise<void>;
|
||||
bulkGet(keys: HexKey[]): Promise<MetadataCache[]>;
|
||||
bulkPut(users: MetadataCache[]): Promise<void>;
|
||||
update(key: HexKey, fields: Record<string, string | number>): Promise<void>;
|
||||
}
|
||||
|
||||
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;
|
@ -1,191 +0,0 @@
|
||||
import { HexKey } from "@snort/nostr";
|
||||
import { db as idb } from "Db";
|
||||
|
||||
import { UsersDb, MetadataCache, setUsers } from "State/Users";
|
||||
import store from "State/Store";
|
||||
import { groupByPubkey, unixNowMs, unwrap } from "Util";
|
||||
|
||||
class IndexedUsersDb implements UsersDb {
|
||||
ready = false;
|
||||
|
||||
isAvailable() {
|
||||
if ("indexedDB" in window) {
|
||||
return new Promise<boolean>(resolve => {
|
||||
const req = window.indexedDB.open("dummy", 1);
|
||||
req.onsuccess = () => {
|
||||
resolve(true);
|
||||
};
|
||||
req.onerror = () => {
|
||||
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();
|
||||
}
|
||||
|
||||
async bulkGet(keys: HexKey[]) {
|
||||
const ret = await idb.users.bulkGet(keys);
|
||||
return ret.filter(a => a !== undefined).map(a_1 => unwrap(a_1));
|
||||
}
|
||||
|
||||
async add(user: MetadataCache) {
|
||||
await idb.users.add(user);
|
||||
}
|
||||
|
||||
async put(user: MetadataCache) {
|
||||
await idb.users.put(user);
|
||||
}
|
||||
|
||||
async bulkAdd(users: MetadataCache[]) {
|
||||
await idb.users.bulkAdd(users);
|
||||
}
|
||||
|
||||
async bulkPut(users: MetadataCache[]) {
|
||||
await idb.users.bulkPut(users);
|
||||
}
|
||||
|
||||
async update(key: HexKey, fields: Record<string, string | number>) {
|
||||
await idb.users.update(key, fields);
|
||||
}
|
||||
}
|
||||
|
||||
export const IndexedUDB = new IndexedUsersDb();
|
||||
|
||||
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)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
querySync(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;
|
||||
let ret: MetadataCache | undefined = users[key];
|
||||
if (IndexedUDB.ready && ret === undefined) {
|
||||
ret = await IndexedUDB.find(key);
|
||||
if (ret) {
|
||||
await this.put(ret);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
async add(user: MetadataCache) {
|
||||
const state = store.getState();
|
||||
const { users } = state.users;
|
||||
store.dispatch(setUsers({ ...users, [user.pubkey]: user }));
|
||||
if (IndexedUDB.ready) {
|
||||
await IndexedUDB.add(user);
|
||||
}
|
||||
}
|
||||
|
||||
async put(user: MetadataCache) {
|
||||
const state = store.getState();
|
||||
const { users } = state.users;
|
||||
store.dispatch(setUsers({ ...users, [user.pubkey]: user }));
|
||||
if (IndexedUDB.ready) {
|
||||
await IndexedUDB.put(user);
|
||||
}
|
||||
}
|
||||
|
||||
async bulkAdd(newUserProfiles: MetadataCache[]) {
|
||||
const state = store.getState();
|
||||
const { users } = state.users;
|
||||
const newUsers = newUserProfiles.reduce(groupByPubkey, {});
|
||||
store.dispatch(setUsers({ ...users, ...newUsers }));
|
||||
if (IndexedUDB.ready) {
|
||||
await IndexedUDB.bulkAdd(newUserProfiles);
|
||||
}
|
||||
}
|
||||
|
||||
async bulkGet(keys: HexKey[]) {
|
||||
const state = store.getState();
|
||||
const { users } = state.users;
|
||||
const ids = new Set([...keys]);
|
||||
let ret = Object.values(users).filter(user => {
|
||||
return ids.has(user.pubkey);
|
||||
});
|
||||
if (IndexedUDB.ready && ret.length !== ids.size) {
|
||||
const startLoad = unixNowMs();
|
||||
const hasKeys = new Set(Object.keys(users));
|
||||
const missing = [...ids].filter(a => !hasKeys.has(a));
|
||||
const missingFromCache = await IndexedUDB.bulkGet(missing);
|
||||
store.dispatch(setUsers({ ...users, ...missingFromCache.reduce(groupByPubkey, {}) }));
|
||||
console.debug(
|
||||
`Loaded ${missingFromCache.length}/${missing.length} profiles from cache in ${(unixNowMs() - startLoad).toFixed(
|
||||
1
|
||||
)} ms`
|
||||
);
|
||||
ret = [...ret, ...missingFromCache];
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
async update(key: HexKey, fields: Record<string, string | number>) {
|
||||
const state = store.getState();
|
||||
const { users } = state.users;
|
||||
const current = users[key];
|
||||
const updated = { ...current, ...fields };
|
||||
store.dispatch(setUsers({ ...users, [key]: updated }));
|
||||
if (IndexedUDB.ready) {
|
||||
await IndexedUDB.update(key, fields);
|
||||
}
|
||||
}
|
||||
|
||||
async bulkPut(newUsers: MetadataCache[]) {
|
||||
const state = store.getState();
|
||||
const { users } = state.users;
|
||||
const newProfiles = newUsers.reduce(groupByPubkey, {});
|
||||
store.dispatch(setUsers({ ...users, ...newProfiles }));
|
||||
if (IndexedUDB.ready) {
|
||||
await IndexedUDB.bulkPut(newUsers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const ReduxUDB = new ReduxUsersDb();
|
@ -1,20 +0,0 @@
|
||||
import { useSelector } from "react-redux";
|
||||
import { MetadataCache } from "State/Users";
|
||||
import type { RootState } from "State/Store";
|
||||
import { HexKey } from "@snort/nostr";
|
||||
import { ReduxUDB } from "./Db";
|
||||
|
||||
export function useQuery(query: string) {
|
||||
// TODO: not observable
|
||||
return ReduxUDB.querySync(query);
|
||||
}
|
||||
|
||||
export function useKey(pubKey?: HexKey) {
|
||||
const { users } = useSelector((state: RootState) => state.users);
|
||||
return pubKey ? users[pubKey] : undefined;
|
||||
}
|
||||
|
||||
export function useKeys(pubKeys?: HexKey[]): Map<HexKey, MetadataCache> {
|
||||
const { users } = useSelector((state: RootState) => state.users);
|
||||
return new Map((pubKeys ?? []).map(a => [a, users[a]]));
|
||||
}
|
145
packages/app/src/State/Users/UserCache.ts
Normal file
145
packages/app/src/State/Users/UserCache.ts
Normal file
@ -0,0 +1,145 @@
|
||||
import { HexKey } from "@snort/nostr";
|
||||
import { db } from "Db";
|
||||
import { unixNowMs, unwrap } from "Util";
|
||||
import { MetadataCache } from ".";
|
||||
|
||||
type HookFn = () => void;
|
||||
|
||||
interface HookFilter {
|
||||
key: HexKey;
|
||||
fn: HookFn;
|
||||
}
|
||||
|
||||
export class UserProfileCache {
|
||||
#cache: Map<HexKey, MetadataCache>;
|
||||
#hooks: Array<HookFilter>;
|
||||
#diskCache: Set<HexKey>;
|
||||
|
||||
constructor() {
|
||||
this.#cache = new Map();
|
||||
this.#hooks = [];
|
||||
this.#diskCache = new Set();
|
||||
setInterval(() => {
|
||||
console.debug(
|
||||
`[UserCache] ${this.#cache.size} loaded, ${this.#diskCache.size} on-disk, ${this.#hooks.length} hooks`
|
||||
);
|
||||
}, 5_000);
|
||||
}
|
||||
|
||||
async preload() {
|
||||
if (db.ready) {
|
||||
const keys = await db.users.toCollection().primaryKeys();
|
||||
this.#diskCache = new Set<HexKey>(keys.map(a => a as string));
|
||||
}
|
||||
}
|
||||
|
||||
async search(q: string): Promise<Array<MetadataCache>> {
|
||||
if (db.ready) {
|
||||
// on-disk cache will always have more data
|
||||
return (
|
||||
await db.users
|
||||
.where("npub")
|
||||
.startsWithIgnoreCase(q)
|
||||
.or("name")
|
||||
.startsWithIgnoreCase(q)
|
||||
.or("display_name")
|
||||
.startsWithIgnoreCase(q)
|
||||
.or("nip05")
|
||||
.startsWithIgnoreCase(q)
|
||||
.toArray()
|
||||
).slice(0, 5);
|
||||
} else {
|
||||
return [...this.#cache.values()]
|
||||
.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)
|
||||
);
|
||||
})
|
||||
.slice(0, 5);
|
||||
}
|
||||
}
|
||||
|
||||
hook(fn: HookFn, key: HexKey | undefined) {
|
||||
if (!key) {
|
||||
return () => {
|
||||
//noop
|
||||
};
|
||||
}
|
||||
|
||||
this.#hooks.push({
|
||||
key,
|
||||
fn,
|
||||
});
|
||||
return () => {
|
||||
const idx = this.#hooks.findIndex(a => a.fn === fn);
|
||||
if (idx >= 0) {
|
||||
this.#hooks.splice(idx, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
get(key?: HexKey) {
|
||||
if (key) {
|
||||
return this.#cache.get(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to update the profile metadata cache with a new version
|
||||
* @param m Profile metadata
|
||||
* @returns
|
||||
*/
|
||||
async update(m: MetadataCache) {
|
||||
const existing = this.get(m.pubkey);
|
||||
const refresh = existing && existing.created === m.created && existing.loaded < m.loaded;
|
||||
if (!existing || existing.created < m.created || refresh) {
|
||||
this.#cache.set(m.pubkey, m);
|
||||
if (db.ready) {
|
||||
await db.users.put(m);
|
||||
this.#diskCache.add(m.pubkey);
|
||||
}
|
||||
this.#notifyChange([m.pubkey]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a list of profiles from disk cache
|
||||
* @param keys List of profiles to load
|
||||
* @returns Keys that do not exist on disk cache
|
||||
*/
|
||||
async buffer(keys: Array<HexKey>): Promise<Array<HexKey>> {
|
||||
const needsBuffer = keys.filter(a => !this.#cache.has(a));
|
||||
if (db.ready && needsBuffer.length > 0) {
|
||||
const mapped = needsBuffer.map(a => ({
|
||||
has: this.#diskCache.has(a),
|
||||
key: a,
|
||||
}));
|
||||
const start = unixNowMs();
|
||||
const fromCache = await db.users.bulkGet(mapped.filter(a => a.has).map(a => a.key));
|
||||
const fromCacheFiltered = fromCache.filter(a => a !== undefined).map(a => unwrap(a));
|
||||
fromCacheFiltered.forEach(a => {
|
||||
this.#cache.set(a.pubkey, a);
|
||||
});
|
||||
this.#notifyChange(fromCacheFiltered.map(a => a.pubkey));
|
||||
console.debug(
|
||||
`Loaded ${fromCacheFiltered.length}/${keys.length} in ${(unixNowMs() - start).toLocaleString()} ms`
|
||||
);
|
||||
return mapped.filter(a => !a.has).map(a => a.key);
|
||||
}
|
||||
|
||||
// no IndexdDB always return all keys
|
||||
return needsBuffer;
|
||||
}
|
||||
|
||||
#notifyChange(keys: Array<HexKey>) {
|
||||
this.#hooks.filter(a => keys.includes(a.key)).forEach(h => h.fn());
|
||||
}
|
||||
}
|
||||
|
||||
export const UserCache = new UserProfileCache();
|
39
packages/app/src/State/Users/index.ts
Normal file
39
packages/app/src/State/Users/index.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { HexKey, TaggedRawEvent, UserMetadata } from "@snort/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 {
|
||||
const 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,10 +1,18 @@
|
||||
import { AuthHandler, HexKey, TaggedRawEvent } from "@snort/nostr";
|
||||
import {
|
||||
AuthHandler,
|
||||
HexKey,
|
||||
TaggedRawEvent,
|
||||
Event as NEvent,
|
||||
EventKind,
|
||||
RelaySettings,
|
||||
Connection,
|
||||
Subscriptions,
|
||||
} from "@snort/nostr";
|
||||
|
||||
import { Event as NEvent, EventKind, RelaySettings, Connection, Subscriptions } from "@snort/nostr";
|
||||
import { ProfileCacheExpire } from "Const";
|
||||
import { mapEventToProfile } from "State/Users";
|
||||
import { ReduxUDB } from "State/Users/Db";
|
||||
import { unwrap } from "Util";
|
||||
import { mapEventToProfile, MetadataCache } from "State/Users";
|
||||
import { UserCache } from "State/Users/UserCache";
|
||||
import { unixNowMs, unwrap } from "Util";
|
||||
|
||||
/**
|
||||
* Manages nostr content retrieval system
|
||||
@ -137,15 +145,15 @@ export class NostrSystem {
|
||||
/**
|
||||
* Request/Response pattern
|
||||
*/
|
||||
RequestSubscription(sub: Subscriptions) {
|
||||
RequestSubscription(sub: Subscriptions, timeout?: number) {
|
||||
return new Promise<TaggedRawEvent[]>(resolve => {
|
||||
const events: TaggedRawEvent[] = [];
|
||||
|
||||
// force timeout returning current results
|
||||
const timeout = setTimeout(() => {
|
||||
const t = setTimeout(() => {
|
||||
this.RemoveSubscription(sub.Id);
|
||||
resolve(events);
|
||||
}, 10_000);
|
||||
}, timeout ?? 10_000);
|
||||
|
||||
const onEventPassthrough = sub.OnEvent;
|
||||
sub.OnEvent = ev => {
|
||||
@ -166,7 +174,7 @@ export class NostrSystem {
|
||||
sub.OnEnd = c => {
|
||||
c.RemoveSubscription(sub.Id);
|
||||
if (sub.IsFinished()) {
|
||||
clearInterval(timeout);
|
||||
clearInterval(t);
|
||||
console.debug(`[${sub.Id}] Finished`);
|
||||
resolve(events);
|
||||
}
|
||||
@ -176,24 +184,15 @@ export class NostrSystem {
|
||||
}
|
||||
|
||||
async _FetchMetadata() {
|
||||
const userDb = ReduxUDB;
|
||||
|
||||
const missing = new Set<HexKey>();
|
||||
const meta = await userDb.bulkGet(Array.from(this.WantsMetadata));
|
||||
const expire = new Date().getTime() - ProfileCacheExpire;
|
||||
for (const pk of this.WantsMetadata) {
|
||||
const m = meta.find(a => a.pubkey === pk);
|
||||
if ((m?.loaded ?? 0) < expire) {
|
||||
missing.add(pk);
|
||||
// cap 100 missing profiles
|
||||
if (missing.size >= 100) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const missingFromCache = await UserCache.buffer([...this.WantsMetadata]);
|
||||
|
||||
const expire = unixNowMs() - ProfileCacheExpire;
|
||||
const expired = [...this.WantsMetadata]
|
||||
.filter(a => !missingFromCache.includes(a))
|
||||
.filter(a => (UserCache.get(a)?.loaded ?? 0) < expire);
|
||||
const missing = new Set([...missingFromCache, ...expired]);
|
||||
if (missing.size > 0) {
|
||||
console.debug("Wants profiles: ", missing);
|
||||
console.debug(`Wants profiles: ${missingFromCache.length} missing, ${expired.length} expired`);
|
||||
|
||||
const sub = new Subscriptions();
|
||||
sub.Id = `profiles:${sub.Id.slice(0, 8)}`;
|
||||
@ -202,29 +201,21 @@ export class NostrSystem {
|
||||
sub.OnEvent = async e => {
|
||||
const profile = mapEventToProfile(e);
|
||||
if (profile) {
|
||||
const existing = await userDb.find(profile.pubkey);
|
||||
if ((existing?.created ?? 0) < profile.created) {
|
||||
await userDb.put(profile);
|
||||
} else if (existing) {
|
||||
await userDb.update(profile.pubkey, {
|
||||
loaded: profile.loaded,
|
||||
});
|
||||
}
|
||||
await UserCache.update(profile);
|
||||
}
|
||||
};
|
||||
const results = await this.RequestSubscription(sub);
|
||||
const couldNotFetch = Array.from(missing).filter(a => !results.some(b => b.pubkey === a));
|
||||
console.debug("No profiles: ", couldNotFetch);
|
||||
const results = await this.RequestSubscription(sub, 5_000);
|
||||
const couldNotFetch = [...missing].filter(a => !results.some(b => b.pubkey === a));
|
||||
if (couldNotFetch.length > 0) {
|
||||
const updates = couldNotFetch
|
||||
.map(a => {
|
||||
return {
|
||||
pubkey: a,
|
||||
loaded: new Date().getTime(),
|
||||
};
|
||||
})
|
||||
.map(a => userDb.update(a.pubkey, a));
|
||||
await Promise.all(updates);
|
||||
console.debug("No profiles: ", couldNotFetch);
|
||||
const empty = couldNotFetch.map(a =>
|
||||
UserCache.update({
|
||||
pubkey: a,
|
||||
loaded: unixNowMs(),
|
||||
created: 69,
|
||||
} as MetadataCache)
|
||||
);
|
||||
await Promise.all(empty);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useUserProfile } from "Feed/ProfileFeed";
|
||||
import { useUserProfile } from "Hooks/useUserProfile";
|
||||
import Icon from "Icons/Icon";
|
||||
import { useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
|
Loading…
x
Reference in New Issue
Block a user