dm cache
This commit is contained in:
parent
8c44d123bd
commit
c731c65661
11
.vscode/settings.json
vendored
Normal file
11
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"files.exclude": {
|
||||||
|
"**/.git": true,
|
||||||
|
"**/.svn": true,
|
||||||
|
"**/.hg": true,
|
||||||
|
"**/CVS": true,
|
||||||
|
"**/.DS_Store": true,
|
||||||
|
"**/Thumbs.db": true,
|
||||||
|
"**/node_modules": true
|
||||||
|
}
|
||||||
|
}
|
36
packages/app/src/Cache/DMCache.ts
Normal file
36
packages/app/src/Cache/DMCache.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { RawEvent } from "@snort/nostr";
|
||||||
|
import { db } from "Db";
|
||||||
|
import { dedupe } from "Util";
|
||||||
|
import FeedCache from "./FeedCache";
|
||||||
|
|
||||||
|
class DMCache extends FeedCache<RawEvent> {
|
||||||
|
constructor() {
|
||||||
|
super("DMCache", db.dms);
|
||||||
|
}
|
||||||
|
|
||||||
|
key(of: RawEvent): string {
|
||||||
|
return of.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
override async preload(): Promise<void> {
|
||||||
|
await super.preload();
|
||||||
|
// load all dms to memory
|
||||||
|
await this.buffer([...this.onTable]);
|
||||||
|
}
|
||||||
|
|
||||||
|
newest(): number {
|
||||||
|
let ret = 0;
|
||||||
|
this.cache.forEach(v => (ret = v.created_at > ret ? v.created_at : ret));
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
allDms(): Array<RawEvent> {
|
||||||
|
return [...this.cache.values()];
|
||||||
|
}
|
||||||
|
|
||||||
|
takeSnapshot(): Array<RawEvent> {
|
||||||
|
return this.allDms();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DmCache = new DMCache();
|
162
packages/app/src/Cache/FeedCache.ts
Normal file
162
packages/app/src/Cache/FeedCache.ts
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
import { db } from "Db";
|
||||||
|
import { Table } from "dexie";
|
||||||
|
import { unixNowMs, unwrap } from "Util";
|
||||||
|
|
||||||
|
type HookFn = () => void;
|
||||||
|
|
||||||
|
interface HookFilter {
|
||||||
|
key: string;
|
||||||
|
fn: HookFn;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default abstract class FeedCache<TCached> {
|
||||||
|
#name: string;
|
||||||
|
#table: Table<TCached>;
|
||||||
|
#hooks: Array<HookFilter> = [];
|
||||||
|
#snapshot: Readonly<Array<TCached>> = [];
|
||||||
|
#changed = true;
|
||||||
|
protected onTable: Set<string> = new Set();
|
||||||
|
protected cache: Map<string, TCached> = new Map();
|
||||||
|
|
||||||
|
constructor(name: string, table: Table<TCached>) {
|
||||||
|
this.#name = name;
|
||||||
|
this.#table = table;
|
||||||
|
setInterval(() => {
|
||||||
|
console.debug(
|
||||||
|
`[${this.#name}] ${this.cache.size} loaded, ${this.onTable.size} on-disk, ${this.#hooks.length} hooks`
|
||||||
|
);
|
||||||
|
}, 5_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async preload() {
|
||||||
|
if (db.ready) {
|
||||||
|
const keys = await this.#table.toCollection().primaryKeys();
|
||||||
|
this.onTable = new Set<string>(keys.map(a => a as string));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hook(fn: HookFn, key: string | 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getFromCache(key?: string) {
|
||||||
|
if (key) {
|
||||||
|
return this.cache.get(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(key?: string) {
|
||||||
|
if (key && !this.cache.has(key) && db.ready) {
|
||||||
|
const cached = await this.#table.get(key);
|
||||||
|
if (cached) {
|
||||||
|
this.cache.set(this.key(cached), cached);
|
||||||
|
this.notifyChange([key]);
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return key ? this.cache.get(key) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async bulkGet(keys: Array<string>) {
|
||||||
|
const missing = keys.filter(a => !this.cache.has(a));
|
||||||
|
if (missing.length > 0 && db.ready) {
|
||||||
|
const cached = await this.#table.bulkGet(missing);
|
||||||
|
cached.forEach(a => {
|
||||||
|
if (a) {
|
||||||
|
this.cache.set(this.key(a), a);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
.map(a => this.cache.get(a))
|
||||||
|
.filter(a => a)
|
||||||
|
.map(a => unwrap(a));
|
||||||
|
}
|
||||||
|
|
||||||
|
async set(obj: TCached) {
|
||||||
|
const k = this.key(obj);
|
||||||
|
this.cache.set(k, obj);
|
||||||
|
if (db.ready) {
|
||||||
|
await this.#table.put(obj);
|
||||||
|
this.onTable.add(k);
|
||||||
|
}
|
||||||
|
this.notifyChange([k]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async bulkSet(obj: Array<TCached>) {
|
||||||
|
if (db.ready) {
|
||||||
|
await this.#table.bulkPut(obj);
|
||||||
|
obj.forEach(a => this.onTable.add(this.key(a)));
|
||||||
|
}
|
||||||
|
obj.forEach(v => this.cache.set(this.key(v), v));
|
||||||
|
this.notifyChange(obj.map(a => this.key(a)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a list of rows from disk cache
|
||||||
|
* @param keys List of ids to load
|
||||||
|
* @returns Keys that do not exist on disk cache
|
||||||
|
*/
|
||||||
|
async buffer(keys: Array<string>): Promise<Array<string>> {
|
||||||
|
const needsBuffer = keys.filter(a => !this.cache.has(a));
|
||||||
|
if (db.ready && needsBuffer.length > 0) {
|
||||||
|
const mapped = needsBuffer.map(a => ({
|
||||||
|
has: this.onTable.has(a),
|
||||||
|
key: a,
|
||||||
|
}));
|
||||||
|
const start = unixNowMs();
|
||||||
|
const fromCache = await this.#table.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(this.key(a), a);
|
||||||
|
});
|
||||||
|
this.notifyChange(fromCacheFiltered.map(a => this.key(a)));
|
||||||
|
console.debug(
|
||||||
|
`[${this.#name}] 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
async clear() {
|
||||||
|
await this.#table.clear();
|
||||||
|
this.cache.clear();
|
||||||
|
this.onTable.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot() {
|
||||||
|
if (this.#changed) {
|
||||||
|
this.#snapshot = this.takeSnapshot();
|
||||||
|
this.#changed = false;
|
||||||
|
}
|
||||||
|
return this.#snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected notifyChange(keys: Array<string>) {
|
||||||
|
this.#changed = true;
|
||||||
|
this.#hooks.filter(a => keys.includes(a.key)).forEach(h => h.fn());
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract key(of: TCached): string;
|
||||||
|
abstract takeSnapshot(): Array<TCached>;
|
||||||
|
}
|
83
packages/app/src/Cache/UserCache.ts
Normal file
83
packages/app/src/Cache/UserCache.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import FeedCache from "Cache/FeedCache";
|
||||||
|
import { db } from "Db";
|
||||||
|
import { LNURL } from "LNURL";
|
||||||
|
import { MetadataCache } from "Cache";
|
||||||
|
|
||||||
|
class UserProfileCache extends FeedCache<MetadataCache> {
|
||||||
|
constructor() {
|
||||||
|
super("UserCache", db.users);
|
||||||
|
}
|
||||||
|
|
||||||
|
key(of: MetadataCache): string {
|
||||||
|
return of.pubkey;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to update the profile metadata cache with a new version
|
||||||
|
* @param m Profile metadata
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async update(m: MetadataCache) {
|
||||||
|
const existing = this.getFromCache(m.pubkey);
|
||||||
|
const refresh = existing && existing.created === m.created && existing.loaded < m.loaded;
|
||||||
|
if (!existing || existing.created < m.created || refresh) {
|
||||||
|
// fetch zapper key
|
||||||
|
const lnurl = m.lud16 || m.lud06;
|
||||||
|
if (lnurl) {
|
||||||
|
try {
|
||||||
|
const svc = new LNURL(lnurl);
|
||||||
|
await svc.load();
|
||||||
|
m.zapService = svc.zapperPubkey;
|
||||||
|
} catch {
|
||||||
|
console.debug("Failed to load LNURL for zapper pubkey", lnurl);
|
||||||
|
}
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cache.set(m.pubkey, m);
|
||||||
|
if (db.ready) {
|
||||||
|
await db.users.put(m);
|
||||||
|
this.onTable.add(m.pubkey);
|
||||||
|
}
|
||||||
|
this.notifyChange([m.pubkey]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
takeSnapshot(): MetadataCache[] {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserCache = new UserProfileCache();
|
@ -1,5 +1,7 @@
|
|||||||
import { HexKey, TaggedRawEvent, UserMetadata } from "@snort/nostr";
|
import { HexKey, TaggedRawEvent, UserMetadata } from "@snort/nostr";
|
||||||
import { hexToBech32, unixNowMs } from "Util";
|
import { hexToBech32, unixNowMs } from "Util";
|
||||||
|
import { DmCache } from "./DMCache";
|
||||||
|
import { UserCache } from "./UserCache";
|
||||||
|
|
||||||
export interface MetadataCache extends UserMetadata {
|
export interface MetadataCache extends UserMetadata {
|
||||||
/**
|
/**
|
||||||
@ -42,3 +44,10 @@ export function mapEventToProfile(ev: TaggedRawEvent) {
|
|||||||
console.error("Failed to parse JSON", ev, e);
|
console.error("Failed to parse JSON", ev, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function preload() {
|
||||||
|
await UserCache.preload();
|
||||||
|
await DmCache.preload();
|
||||||
|
}
|
||||||
|
|
||||||
|
export { UserCache, DmCache };
|
@ -1,9 +1,9 @@
|
|||||||
import Dexie, { Table } from "dexie";
|
import Dexie, { Table } from "dexie";
|
||||||
import { FullRelaySettings, HexKey, u256 } from "@snort/nostr";
|
import { FullRelaySettings, HexKey, RawEvent, u256 } from "@snort/nostr";
|
||||||
import { MetadataCache } from "State/Users";
|
import { MetadataCache } from "Cache";
|
||||||
|
|
||||||
export const NAME = "snortDB";
|
export const NAME = "snortDB";
|
||||||
export const VERSION = 6;
|
export const VERSION = 7;
|
||||||
|
|
||||||
export interface SubCache {
|
export interface SubCache {
|
||||||
id: string;
|
id: string;
|
||||||
@ -28,6 +28,8 @@ const STORES = {
|
|||||||
users: "++pubkey, name, display_name, picture, nip05, npub",
|
users: "++pubkey, name, display_name, picture, nip05, npub",
|
||||||
relays: "++addr",
|
relays: "++addr",
|
||||||
userRelays: "++pubkey",
|
userRelays: "++pubkey",
|
||||||
|
events: "++id, pubkey, created_at",
|
||||||
|
dms: "++id, pubkey",
|
||||||
};
|
};
|
||||||
|
|
||||||
export class SnortDB extends Dexie {
|
export class SnortDB extends Dexie {
|
||||||
@ -35,6 +37,8 @@ export class SnortDB extends Dexie {
|
|||||||
users!: Table<MetadataCache>;
|
users!: Table<MetadataCache>;
|
||||||
relayMetrics!: Table<RelayMetrics>;
|
relayMetrics!: Table<RelayMetrics>;
|
||||||
userRelays!: Table<UsersRelays>;
|
userRelays!: Table<UsersRelays>;
|
||||||
|
events!: Table<RawEvent>;
|
||||||
|
dms!: Table<RawEvent>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(NAME);
|
super(NAME);
|
||||||
|
@ -5,7 +5,7 @@ import { HexKey, TaggedRawEvent } from "@snort/nostr";
|
|||||||
|
|
||||||
import Note from "Element/Note";
|
import Note from "Element/Note";
|
||||||
import { RootState } from "State/Store";
|
import { RootState } from "State/Store";
|
||||||
import { UserCache } from "State/Users/UserCache";
|
import { UserCache } from "Cache/UserCache";
|
||||||
|
|
||||||
import messages from "./messages";
|
import messages from "./messages";
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ const Bookmarks = ({ pubkey, bookmarks, related }: BookmarksProps) => {
|
|||||||
}, [bookmarks]);
|
}, [bookmarks]);
|
||||||
|
|
||||||
function renderOption(p: HexKey) {
|
function renderOption(p: HexKey) {
|
||||||
const profile = UserCache.get(p);
|
const profile = UserCache.getFromCache(p);
|
||||||
return profile ? <option value={p}>{profile?.display_name || profile?.name}</option> : null;
|
return profile ? <option value={p}>{profile?.display_name || profile?.name}</option> : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,19 +1,24 @@
|
|||||||
import { useDispatch } from "react-redux";
|
import { useDispatch } from "react-redux";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
import { logout } from "State/Login";
|
import { logout } from "State/Login";
|
||||||
|
|
||||||
import messages from "./messages";
|
import messages from "./messages";
|
||||||
|
|
||||||
export default function LogoutButton() {
|
export default function LogoutButton() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="secondary"
|
className="secondary"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
dispatch(logout());
|
dispatch(
|
||||||
window.location.href = "/";
|
logout(() => {
|
||||||
|
navigate("/");
|
||||||
|
})
|
||||||
|
);
|
||||||
}}>
|
}}>
|
||||||
<FormattedMessage {...messages.Logout} />
|
<FormattedMessage {...messages.Logout} />
|
||||||
</button>
|
</button>
|
||||||
|
@ -26,7 +26,7 @@ import NoteTime from "Element/NoteTime";
|
|||||||
import useModeration from "Hooks/useModeration";
|
import useModeration from "Hooks/useModeration";
|
||||||
import { setPinned, setBookmarked } from "State/Login";
|
import { setPinned, setBookmarked } from "State/Login";
|
||||||
import type { RootState } from "State/Store";
|
import type { RootState } from "State/Store";
|
||||||
import { UserCache } from "State/Users/UserCache";
|
import { UserCache } from "Cache/UserCache";
|
||||||
|
|
||||||
import messages from "./messages";
|
import messages from "./messages";
|
||||||
import { EventExt } from "System/EventExt";
|
import { EventExt } from "System/EventExt";
|
||||||
@ -204,7 +204,7 @@ export default function Note(props: NoteProps) {
|
|||||||
const replyRelayHints = thread?.replyTo?.Relay ?? thread.root?.Relay;
|
const replyRelayHints = thread?.replyTo?.Relay ?? thread.root?.Relay;
|
||||||
const mentions: { pk: string; name: string; link: ReactNode }[] = [];
|
const mentions: { pk: string; name: string; link: ReactNode }[] = [];
|
||||||
for (const pk of thread?.pubKeys ?? []) {
|
for (const pk of thread?.pubKeys ?? []) {
|
||||||
const u = UserCache.get(pk);
|
const u = UserCache.getFromCache(pk);
|
||||||
const npub = hexToBech32(NostrPrefix.PublicKey, pk);
|
const npub = hexToBech32(NostrPrefix.PublicKey, pk);
|
||||||
const shortNpub = npub.substring(0, 12);
|
const shortNpub = npub.substring(0, 12);
|
||||||
mentions.push({
|
mentions.push({
|
||||||
|
@ -7,7 +7,7 @@ import { hexToBech32, profileLink } from "Util";
|
|||||||
import Avatar from "Element/Avatar";
|
import Avatar from "Element/Avatar";
|
||||||
import Nip05 from "Element/Nip05";
|
import Nip05 from "Element/Nip05";
|
||||||
import { HexKey, NostrPrefix } from "@snort/nostr";
|
import { HexKey, NostrPrefix } from "@snort/nostr";
|
||||||
import { MetadataCache } from "State/Users";
|
import { MetadataCache } from "Cache";
|
||||||
import usePageWidth from "Hooks/usePageWidth";
|
import usePageWidth from "Hooks/usePageWidth";
|
||||||
|
|
||||||
export interface ProfileImageProps {
|
export interface ProfileImageProps {
|
||||||
|
@ -10,8 +10,8 @@ import { NostrPrefix } from "@snort/nostr";
|
|||||||
import Avatar from "Element/Avatar";
|
import Avatar from "Element/Avatar";
|
||||||
import Nip05 from "Element/Nip05";
|
import Nip05 from "Element/Nip05";
|
||||||
import { hexToBech32 } from "Util";
|
import { hexToBech32 } from "Util";
|
||||||
import { MetadataCache } from "State/Users";
|
import { MetadataCache } from "Cache";
|
||||||
import { UserCache } from "State/Users/UserCache";
|
import { UserCache } from "Cache/UserCache";
|
||||||
|
|
||||||
import messages from "./messages";
|
import messages from "./messages";
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ import Text from "Element/Text";
|
|||||||
import ProfileImage from "Element/ProfileImage";
|
import ProfileImage from "Element/ProfileImage";
|
||||||
import { RootState } from "State/Store";
|
import { RootState } from "State/Store";
|
||||||
import { findTag } from "Util";
|
import { findTag } from "Util";
|
||||||
import { UserCache } from "State/Users/UserCache";
|
import { UserCache } from "Cache/UserCache";
|
||||||
|
|
||||||
import messages from "./messages";
|
import messages from "./messages";
|
||||||
|
|
||||||
@ -65,7 +65,7 @@ export function parseZap(zapReceipt: TaggedRawEvent): ParsedZap {
|
|||||||
ret.valid = false;
|
ret.valid = false;
|
||||||
ret.errors.push("amount tag does not match invoice amount");
|
ret.errors.push("amount tag does not match invoice amount");
|
||||||
}
|
}
|
||||||
if (UserCache.get(ret.receiver)?.zapService !== ret.zapService) {
|
if (UserCache.getFromCache(ret.receiver)?.zapService !== ret.zapService) {
|
||||||
ret.valid = false;
|
ret.valid = false;
|
||||||
ret.errors.push("zap service pubkey doesn't match");
|
ret.errors.push("zap service pubkey doesn't match");
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ import { TaggedRawEvent, HexKey, Lists, EventKind } from "@snort/nostr";
|
|||||||
import { getNewest, getNewestEventTagsByKey, unwrap } from "Util";
|
import { getNewest, getNewestEventTagsByKey, unwrap } from "Util";
|
||||||
import { makeNotification } from "Notifications";
|
import { makeNotification } from "Notifications";
|
||||||
import {
|
import {
|
||||||
addDirectMessage,
|
|
||||||
setFollows,
|
setFollows,
|
||||||
setRelays,
|
setRelays,
|
||||||
setMuted,
|
setMuted,
|
||||||
@ -24,6 +23,7 @@ import useModeration from "Hooks/useModeration";
|
|||||||
import { FlatNoteStore, RequestBuilder } from "System";
|
import { FlatNoteStore, RequestBuilder } from "System";
|
||||||
import useRequestBuilder from "Hooks/useRequestBuilder";
|
import useRequestBuilder from "Hooks/useRequestBuilder";
|
||||||
import { EventExt } from "System/EventExt";
|
import { EventExt } from "System/EventExt";
|
||||||
|
import { DmCache } from "Cache";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Managed loading data for the current logged in user
|
* Managed loading data for the current logged in user
|
||||||
@ -45,9 +45,12 @@ export default function useLoginFeed() {
|
|||||||
b.withOptions({
|
b.withOptions({
|
||||||
leaveOpen: true,
|
leaveOpen: true,
|
||||||
});
|
});
|
||||||
b.withFilter().authors([pubKey]).kinds([EventKind.ContactList, EventKind.DirectMessage]);
|
b.withFilter().authors([pubKey]).kinds([EventKind.ContactList]);
|
||||||
b.withFilter().kinds([EventKind.TextNote]).tag("p", [pubKey]).limit(1);
|
b.withFilter().kinds([EventKind.TextNote]).tag("p", [pubKey]).limit(1);
|
||||||
b.withFilter().kinds([EventKind.DirectMessage]).tag("p", [pubKey]);
|
|
||||||
|
const dmSince = DmCache.newest();
|
||||||
|
b.withFilter().authors([pubKey]).kinds([EventKind.DirectMessage]).since(dmSince);
|
||||||
|
b.withFilter().kinds([EventKind.DirectMessage]).tag("p", [pubKey]).since(dmSince);
|
||||||
return b;
|
return b;
|
||||||
}, [pubKey]);
|
}, [pubKey]);
|
||||||
|
|
||||||
@ -81,7 +84,7 @@ export default function useLoginFeed() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const dms = loginFeed.data.filter(a => a.kind === EventKind.DirectMessage);
|
const dms = loginFeed.data.filter(a => a.kind === EventKind.DirectMessage);
|
||||||
dispatch(addDirectMessage(dms));
|
DmCache.bulkSet(dms);
|
||||||
}
|
}
|
||||||
}, [dispatch, loginFeed]);
|
}, [dispatch, loginFeed]);
|
||||||
|
|
||||||
|
9
packages/app/src/Hooks/useDmsCache.tsx
Normal file
9
packages/app/src/Hooks/useDmsCache.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { DmCache } from "Cache";
|
||||||
|
import { useSyncExternalStore } from "react";
|
||||||
|
|
||||||
|
export function useDmCache() {
|
||||||
|
return useSyncExternalStore(
|
||||||
|
c => DmCache.hook(c, undefined),
|
||||||
|
() => DmCache.snapshot()
|
||||||
|
);
|
||||||
|
}
|
@ -1,14 +1,14 @@
|
|||||||
import { useEffect, useSyncExternalStore } from "react";
|
import { useEffect, useSyncExternalStore } from "react";
|
||||||
import { HexKey } from "@snort/nostr";
|
import { HexKey } from "@snort/nostr";
|
||||||
|
|
||||||
import { MetadataCache } from "State/Users";
|
import { MetadataCache } from "Cache";
|
||||||
import { UserCache } from "State/Users/UserCache";
|
import { UserCache } from "Cache/UserCache";
|
||||||
import { ProfileLoader } from "System/ProfileCache";
|
import { ProfileLoader } from "System/ProfileCache";
|
||||||
|
|
||||||
export function useUserProfile(pubKey?: HexKey): MetadataCache | undefined {
|
export function useUserProfile(pubKey?: HexKey): MetadataCache | undefined {
|
||||||
const user = useSyncExternalStore<MetadataCache | undefined>(
|
const user = useSyncExternalStore<MetadataCache | undefined>(
|
||||||
h => UserCache.hook(h, pubKey),
|
h => UserCache.hook(h, pubKey),
|
||||||
() => UserCache.get(pubKey)
|
() => UserCache.getFromCache(pubKey)
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -3,11 +3,11 @@ import Nostrich from "nostrich.webp";
|
|||||||
import { TaggedRawEvent } from "@snort/nostr";
|
import { TaggedRawEvent } from "@snort/nostr";
|
||||||
import { EventKind } from "@snort/nostr";
|
import { EventKind } from "@snort/nostr";
|
||||||
import type { NotificationRequest } from "State/Login";
|
import type { NotificationRequest } from "State/Login";
|
||||||
import { MetadataCache } from "State/Users";
|
import { MetadataCache } from "Cache";
|
||||||
import { getDisplayName } from "Element/ProfileImage";
|
import { getDisplayName } from "Element/ProfileImage";
|
||||||
import { MentionRegex } from "Const";
|
import { MentionRegex } from "Const";
|
||||||
import { tagFilterOfTextRepost, unwrap } from "Util";
|
import { tagFilterOfTextRepost, unwrap } from "Util";
|
||||||
import { UserCache } from "State/Users/UserCache";
|
import { UserCache } from "Cache/UserCache";
|
||||||
|
|
||||||
export async function makeNotification(ev: TaggedRawEvent): Promise<NotificationRequest | null> {
|
export async function makeNotification(ev: TaggedRawEvent): Promise<NotificationRequest | null> {
|
||||||
switch (ev.kind) {
|
switch (ev.kind) {
|
||||||
@ -18,10 +18,10 @@ export async function makeNotification(ev: TaggedRawEvent): Promise<Notification
|
|||||||
const pubkeys = new Set([ev.pubkey, ...ev.tags.filter(a => a[0] === "p").map(a => a[1])]);
|
const pubkeys = new Set([ev.pubkey, ...ev.tags.filter(a => a[0] === "p").map(a => a[1])]);
|
||||||
await UserCache.buffer([...pubkeys]);
|
await UserCache.buffer([...pubkeys]);
|
||||||
const allUsers = [...pubkeys]
|
const allUsers = [...pubkeys]
|
||||||
.map(a => UserCache.get(a))
|
.map(a => UserCache.getFromCache(a))
|
||||||
.filter(a => a)
|
.filter(a => a)
|
||||||
.map(a => unwrap(a));
|
.map(a => unwrap(a));
|
||||||
const fromUser = UserCache.get(ev.pubkey);
|
const fromUser = UserCache.getFromCache(ev.pubkey);
|
||||||
const name = getDisplayName(fromUser, ev.pubkey);
|
const name = getDisplayName(fromUser, ev.pubkey);
|
||||||
const avatarUrl = fromUser?.picture || Nostrich;
|
const avatarUrl = fromUser?.picture || Nostrich;
|
||||||
return {
|
return {
|
||||||
|
@ -8,11 +8,12 @@ import { bech32ToHex } from "Util";
|
|||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Feed/EventPublisher";
|
||||||
|
|
||||||
import DM from "Element/DM";
|
import DM from "Element/DM";
|
||||||
import { TaggedRawEvent } from "@snort/nostr";
|
import { RawEvent, TaggedRawEvent } from "@snort/nostr";
|
||||||
import { dmsInChat, isToSelf } from "Pages/MessagesPage";
|
import { dmsInChat, isToSelf } from "Pages/MessagesPage";
|
||||||
import NoteToSelf from "Element/NoteToSelf";
|
import NoteToSelf from "Element/NoteToSelf";
|
||||||
import { RootState } from "State/Store";
|
import { RootState } from "State/Store";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
import { useDmCache } from "Hooks/useDmsCache";
|
||||||
|
|
||||||
type RouterParams = {
|
type RouterParams = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -23,11 +24,11 @@ export default function ChatPage() {
|
|||||||
const publisher = useEventPublisher();
|
const publisher = useEventPublisher();
|
||||||
const id = bech32ToHex(params.id ?? "");
|
const id = bech32ToHex(params.id ?? "");
|
||||||
const pubKey = useSelector((s: RootState) => s.login.publicKey);
|
const pubKey = useSelector((s: RootState) => s.login.publicKey);
|
||||||
const dms = useSelector((s: RootState) => filterDms(s.login.dms));
|
|
||||||
const [content, setContent] = useState<string>();
|
const [content, setContent] = useState<string>();
|
||||||
const dmListRef = useRef<HTMLDivElement>(null);
|
const dmListRef = useRef<HTMLDivElement>(null);
|
||||||
|
const dms = filterDms(useDmCache());
|
||||||
|
|
||||||
function filterDms(dms: TaggedRawEvent[]) {
|
function filterDms(dms: readonly RawEvent[]) {
|
||||||
return dmsInChat(id === pubKey ? dms.filter(d => isToSelf(d, pubKey)) : dms, id);
|
return dmsInChat(id === pubKey ? dms.filter(d => isToSelf(d, pubKey)) : dms, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,11 +18,11 @@ import { totalUnread } from "Pages/MessagesPage";
|
|||||||
import useModeration from "Hooks/useModeration";
|
import useModeration from "Hooks/useModeration";
|
||||||
import { NoteCreator } from "Element/NoteCreator";
|
import { NoteCreator } from "Element/NoteCreator";
|
||||||
import { db } from "Db";
|
import { db } from "Db";
|
||||||
import { UserCache } from "State/Users/UserCache";
|
|
||||||
import { FollowsRelays } from "State/Relays";
|
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Feed/EventPublisher";
|
||||||
import { SnortPubKey } from "Const";
|
import { SnortPubKey } from "Const";
|
||||||
import SubDebug from "Element/SubDebug";
|
import SubDebug from "Element/SubDebug";
|
||||||
|
import { preload } from "Cache";
|
||||||
|
import { useDmCache } from "Hooks/useDmsCache";
|
||||||
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -101,8 +101,7 @@ export default function Layout() {
|
|||||||
db.isAvailable().then(async a => {
|
db.isAvailable().then(async a => {
|
||||||
db.ready = a;
|
db.ready = a;
|
||||||
if (a) {
|
if (a) {
|
||||||
await UserCache.preload();
|
await preload();
|
||||||
await FollowsRelays.preload();
|
|
||||||
}
|
}
|
||||||
console.debug(`Using db: ${a ? "IndexedDB" : "In-Memory"}`);
|
console.debug(`Using db: ${a ? "IndexedDB" : "In-Memory"}`);
|
||||||
dispatch(init());
|
dispatch(init());
|
||||||
@ -192,7 +191,8 @@ const AccountHeader = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { isMuted } = useModeration();
|
const { isMuted } = useModeration();
|
||||||
const { publicKey, latestNotification, readNotifications, dms } = useSelector((s: RootState) => s.login);
|
const { publicKey, latestNotification, readNotifications } = useSelector((s: RootState) => s.login);
|
||||||
|
const dms = useDmCache();
|
||||||
|
|
||||||
const hasNotifications = useMemo(
|
const hasNotifications = useMemo(
|
||||||
() => latestNotification > readNotifications,
|
() => latestNotification > readNotifications,
|
||||||
|
@ -12,6 +12,7 @@ import NoteToSelf from "Element/NoteToSelf";
|
|||||||
import useModeration from "Hooks/useModeration";
|
import useModeration from "Hooks/useModeration";
|
||||||
|
|
||||||
import messages from "./messages";
|
import messages from "./messages";
|
||||||
|
import { useDmCache } from "Hooks/useDmsCache";
|
||||||
|
|
||||||
type DmChat = {
|
type DmChat = {
|
||||||
pubkey: HexKey;
|
pubkey: HexKey;
|
||||||
@ -22,9 +23,9 @@ type DmChat = {
|
|||||||
export default function MessagesPage() {
|
export default function MessagesPage() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const myPubKey = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
|
const myPubKey = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
|
||||||
const dms = useSelector<RootState, RawEvent[]>(s => s.login.dms);
|
|
||||||
const dmInteraction = useSelector<RootState, number>(s => s.login.dmInteraction);
|
const dmInteraction = useSelector<RootState, number>(s => s.login.dmInteraction);
|
||||||
const { isMuted } = useModeration();
|
const { isMuted } = useModeration();
|
||||||
|
const dms = useDmCache();
|
||||||
|
|
||||||
const chats = useMemo(() => {
|
const chats = useMemo(() => {
|
||||||
return extractChats(
|
return extractChats(
|
||||||
@ -105,11 +106,11 @@ export function dmTo(e: RawEvent) {
|
|||||||
return firstP ? firstP[1] : "";
|
return firstP ? firstP[1] : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isToSelf(e: RawEvent, pk: HexKey) {
|
export function isToSelf(e: Readonly<RawEvent>, pk: HexKey) {
|
||||||
return e.pubkey === pk && dmTo(e) === pk;
|
return e.pubkey === pk && dmTo(e) === pk;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dmsInChat(dms: RawEvent[], pk: HexKey) {
|
export function dmsInChat(dms: readonly RawEvent[], pk: HexKey) {
|
||||||
return dms.filter(a => a.pubkey === pk || dmTo(a) === pk);
|
return dms.filter(a => a.pubkey === pk || dmTo(a) === pk);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,8 +7,8 @@ import { debounce } from "Util";
|
|||||||
import { router } from "index";
|
import { router } from "index";
|
||||||
import { SearchRelays } from "Const";
|
import { SearchRelays } from "Const";
|
||||||
import { System } from "System";
|
import { System } from "System";
|
||||||
import { MetadataCache } from "State/Users";
|
import { MetadataCache } from "Cache";
|
||||||
import { UserCache } from "State/Users/UserCache";
|
import { UserCache } from "Cache/UserCache";
|
||||||
|
|
||||||
import messages from "./messages";
|
import messages from "./messages";
|
||||||
|
|
||||||
|
@ -12,8 +12,11 @@ const SettingsIndex = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
function handleLogout() {
|
function handleLogout() {
|
||||||
dispatch(logout());
|
dispatch(
|
||||||
window.location.href = "/";
|
logout(() => {
|
||||||
|
navigate("/");
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import { AnyAction, createSlice, PayloadAction, ThunkAction } from "@reduxjs/toolkit";
|
import { AnyAction, createSlice, PayloadAction, ThunkAction } from "@reduxjs/toolkit";
|
||||||
import * as secp from "@noble/secp256k1";
|
import * as secp from "@noble/secp256k1";
|
||||||
|
import { HexKey } from "@snort/nostr";
|
||||||
|
|
||||||
import { DefaultRelays } from "Const";
|
import { DefaultRelays } from "Const";
|
||||||
import { HexKey, TaggedRawEvent } from "@snort/nostr";
|
|
||||||
import { RelaySettings } from "@snort/nostr";
|
import { RelaySettings } from "@snort/nostr";
|
||||||
import type { AppDispatch, RootState } from "State/Store";
|
import type { AppDispatch, RootState } from "State/Store";
|
||||||
import { ImgProxySettings } from "Hooks/useImgProxy";
|
import { ImgProxySettings } from "Hooks/useImgProxy";
|
||||||
import { sanitizeRelayUrl } from "Util";
|
import { sanitizeRelayUrl } from "Util";
|
||||||
|
import { DmCache } from "Cache";
|
||||||
|
|
||||||
const PrivateKeyItem = "secret";
|
const PrivateKeyItem = "secret";
|
||||||
const PublicKeyItem = "pubkey";
|
const PublicKeyItem = "pubkey";
|
||||||
@ -194,11 +196,6 @@ export interface LoginStore {
|
|||||||
*/
|
*/
|
||||||
readNotifications: number;
|
readNotifications: number;
|
||||||
|
|
||||||
/**
|
|
||||||
* Encrypted DM's
|
|
||||||
*/
|
|
||||||
dms: TaggedRawEvent[];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Counter to trigger refresh of unread dms
|
* Counter to trigger refresh of unread dms
|
||||||
*/
|
*/
|
||||||
@ -320,11 +317,6 @@ const LoginSlice = createSlice({
|
|||||||
// preferences
|
// preferences
|
||||||
const pref = ReadPreferences();
|
const pref = ReadPreferences();
|
||||||
state.preferences = pref;
|
state.preferences = pref;
|
||||||
|
|
||||||
// disable reactions for logged out
|
|
||||||
if (state.loggedOut === true) {
|
|
||||||
state.preferences.enableReactions = false;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
setPrivateKey: (state, action: PayloadAction<HexKey>) => {
|
setPrivateKey: (state, action: PayloadAction<HexKey>) => {
|
||||||
state.loggedOut = false;
|
state.loggedOut = false;
|
||||||
@ -445,34 +437,20 @@ const LoginSlice = createSlice({
|
|||||||
state.latestMuted = createdAt;
|
state.latestMuted = createdAt;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
addDirectMessage: (state, action: PayloadAction<TaggedRawEvent | Array<TaggedRawEvent>>) => {
|
|
||||||
let n = action.payload;
|
|
||||||
if (!Array.isArray(n)) {
|
|
||||||
n = [n];
|
|
||||||
}
|
|
||||||
|
|
||||||
let didChange = false;
|
|
||||||
for (const x of n) {
|
|
||||||
if (!state.dms.some(a => a.id === x.id)) {
|
|
||||||
state.dms.push(x);
|
|
||||||
didChange = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (didChange) {
|
|
||||||
state.dms = [...state.dms];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
incDmInteraction: state => {
|
incDmInteraction: state => {
|
||||||
state.dmInteraction += 1;
|
state.dmInteraction += 1;
|
||||||
},
|
},
|
||||||
logout: state => {
|
logout: (state, payload: PayloadAction<() => void>) => {
|
||||||
const relays = { ...state.relays };
|
const relays = { ...state.relays };
|
||||||
state = Object.assign(state, InitState);
|
state = Object.assign(state, InitState);
|
||||||
state.loggedOut = true;
|
state.loggedOut = true;
|
||||||
window.localStorage.clear();
|
window.localStorage.clear();
|
||||||
state.relays = relays;
|
state.relays = relays;
|
||||||
window.localStorage.setItem(RelayListKey, JSON.stringify(relays));
|
window.localStorage.setItem(RelayListKey, JSON.stringify(relays));
|
||||||
|
queueMicrotask(async () => {
|
||||||
|
await DmCache.clear();
|
||||||
|
payload.payload();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
markNotificationsRead: state => {
|
markNotificationsRead: state => {
|
||||||
state.readNotifications = Math.ceil(new Date().getTime() / 1000);
|
state.readNotifications = Math.ceil(new Date().getTime() / 1000);
|
||||||
@ -502,7 +480,6 @@ export const {
|
|||||||
setPinned,
|
setPinned,
|
||||||
setBookmarked,
|
setBookmarked,
|
||||||
setBlocked,
|
setBlocked,
|
||||||
addDirectMessage,
|
|
||||||
incDmInteraction,
|
incDmInteraction,
|
||||||
logout,
|
logout,
|
||||||
markNotificationsRead,
|
markNotificationsRead,
|
||||||
|
@ -1,159 +0,0 @@
|
|||||||
import { HexKey } from "@snort/nostr";
|
|
||||||
import { db } from "Db";
|
|
||||||
import { LNURL } from "LNURL";
|
|
||||||
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) {
|
|
||||||
// fetch zapper key
|
|
||||||
const lnurl = m.lud16 || m.lud06;
|
|
||||||
if (lnurl) {
|
|
||||||
try {
|
|
||||||
const svc = new LNURL(lnurl);
|
|
||||||
await svc.load();
|
|
||||||
m.zapService = svc.zapperPubkey;
|
|
||||||
} catch {
|
|
||||||
console.debug("Failed to load LNURL for zapper pubkey", lnurl);
|
|
||||||
}
|
|
||||||
// ignored
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
@ -1,11 +1,11 @@
|
|||||||
import { EventKind, HexKey, TaggedRawEvent } from "@snort/nostr";
|
import { EventKind, HexKey, TaggedRawEvent } from "@snort/nostr";
|
||||||
import { ProfileCacheExpire } from "Const";
|
import { ProfileCacheExpire } from "Const";
|
||||||
import { mapEventToProfile, MetadataCache } from "State/Users";
|
import { mapEventToProfile, MetadataCache } from "Cache";
|
||||||
import { UserCache } from "State/Users/UserCache";
|
import { UserCache } from "Cache/UserCache";
|
||||||
import { PubkeyReplaceableNoteStore, RequestBuilder, System } from "System";
|
import { PubkeyReplaceableNoteStore, RequestBuilder, System } from "System";
|
||||||
import { unixNowMs } from "Util";
|
import { unixNowMs } from "Util";
|
||||||
|
|
||||||
class ProfileCache {
|
class ProfileLoaderService {
|
||||||
/**
|
/**
|
||||||
* List of pubkeys to fetch metadata for
|
* List of pubkeys to fetch metadata for
|
||||||
*/
|
*/
|
||||||
@ -43,7 +43,7 @@ class ProfileCache {
|
|||||||
const expire = unixNowMs() - ProfileCacheExpire;
|
const expire = unixNowMs() - ProfileCacheExpire;
|
||||||
const expired = [...this.WantsMetadata]
|
const expired = [...this.WantsMetadata]
|
||||||
.filter(a => !missingFromCache.includes(a))
|
.filter(a => !missingFromCache.includes(a))
|
||||||
.filter(a => (UserCache.get(a)?.loaded ?? 0) < expire);
|
.filter(a => (UserCache.getFromCache(a)?.loaded ?? 0) < expire);
|
||||||
const missing = new Set([...missingFromCache, ...expired]);
|
const missing = new Set([...missingFromCache, ...expired]);
|
||||||
if (missing.size > 0) {
|
if (missing.size > 0) {
|
||||||
console.debug(`Wants profiles: ${missingFromCache.length} missing, ${expired.length} expired`);
|
console.debug(`Wants profiles: ${missingFromCache.length} missing, ${expired.length} expired`);
|
||||||
@ -97,4 +97,4 @@ class ProfileCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProfileLoader = new ProfileCache();
|
export const ProfileLoader = new ProfileLoaderService();
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { MetadataCache } from "State/Users";
|
import { MetadataCache } from "Cache";
|
||||||
import { BaseUITask } from "Tasks";
|
import { BaseUITask } from "Tasks";
|
||||||
|
|
||||||
export class Nip5Task extends BaseUITask {
|
export class Nip5Task extends BaseUITask {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { MetadataCache } from "State/Users";
|
import { MetadataCache } from "Cache";
|
||||||
|
|
||||||
export interface UITask {
|
export interface UITask {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -6,7 +6,7 @@ import { decode as invoiceDecode } from "light-bolt11-decoder";
|
|||||||
import { bech32 } from "bech32";
|
import { bech32 } from "bech32";
|
||||||
import base32Decode from "base32-decode";
|
import base32Decode from "base32-decode";
|
||||||
import { HexKey, TaggedRawEvent, u256, EventKind, encodeTLV, NostrPrefix, decodeTLV, TLVEntryType } from "@snort/nostr";
|
import { HexKey, TaggedRawEvent, u256, EventKind, encodeTLV, NostrPrefix, decodeTLV, TLVEntryType } from "@snort/nostr";
|
||||||
import { MetadataCache } from "State/Users";
|
import { MetadataCache } from "Cache";
|
||||||
|
|
||||||
export const sha256 = (str: string | Uint8Array): u256 => {
|
export const sha256 = (str: string | Uint8Array): u256 => {
|
||||||
return secp.utils.bytesToHex(hash(str));
|
return secp.utils.bytesToHex(hash(str));
|
||||||
|
Loading…
x
Reference in New Issue
Block a user