Profile fetcher

This commit is contained in:
Kieran 2023-01-16 00:07:27 +00:00
parent bd247991bc
commit b74f8f33dd
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
7 changed files with 80 additions and 66 deletions

View File

@ -1,9 +1,8 @@
import Dexie, { Table } from 'dexie'; import Dexie, { Table } from 'dexie';
import { MetadataCache } from './state/Users';
import type { User } from './nostr/types';
export class SnortDB extends Dexie { export class SnortDB extends Dexie {
users!: Table<User>; users!: Table<MetadataCache>;
constructor() { constructor() {
super('snortDB'); super('snortDB');

View File

@ -11,12 +11,12 @@ import "./Textarea.css";
// @ts-expect-error // @ts-expect-error
import Nostrich from "../nostrich.jpg"; import Nostrich from "../nostrich.jpg";
import { hexToBech32 } from "../Util"; import { hexToBech32 } from "../Util";
import type { User } from "../nostr/types";
import { db } from "../db"; import { db } from "../db";
import { MetadataCache } from "../state/Users";
function searchUsers(query: string, users: User[]) { function searchUsers(query: string, users: MetadataCache[]) {
const q = query.toLowerCase() const q = query.toLowerCase()
return users.filter(({ name, display_name, about, nip05 }: User) => { return users.filter(({ name, display_name, about, nip05 }: MetadataCache) => {
return name?.toLowerCase().includes(q) return name?.toLowerCase().includes(q)
|| display_name?.toLowerCase().includes(q) || display_name?.toLowerCase().includes(q)
|| about?.toLowerCase().includes(q) || about?.toLowerCase().includes(q)
@ -24,7 +24,7 @@ function searchUsers(query: string, users: User[]) {
}).slice(0, 3) }).slice(0, 3)
} }
const UserItem = ({ pubkey, display_name, picture, nip05, ...rest }: User) => { const UserItem = ({ pubkey, display_name, picture, nip05, ...rest }: MetadataCache) => {
return ( return (
<div key={pubkey} className="user-item"> <div key={pubkey} className="user-item">
<div className="user-picture"> <div className="user-picture">
@ -38,22 +38,22 @@ const UserItem = ({ pubkey, display_name, picture, nip05, ...rest }: User) => {
) )
} }
function normalizeUser({ pubkey, picture, nip05, name, display_name }: User) { function normalizeUser({ pubkey, picture, nip05, name, display_name }: MetadataCache) {
return { pubkey, nip05, name, picture, display_name } return { pubkey, nip05, name, picture, display_name }
} }
const Textarea = ({ users, onChange, ...rest }: any) => { const Textarea = ({ users, onChange, ...rest }: any) => {
const normalizedUsers = Object.keys(users).reduce((acc, pk) => { const normalizedUsers = Object.keys(users).reduce((acc, pk) => {
return {...acc, [pk]: normalizeUser(users[pk]) } return { ...acc, [pk]: normalizeUser(users[pk]) }
}, {}) }, {})
const dbUsers = useLiveQuery( const dbUsers = useLiveQuery(
() => db.users.toArray().then(usrs => { () => db.users.toArray().then(usrs => {
return usrs.reduce((acc, usr) => { return usrs.reduce((acc, usr) => {
return { ...acc, [usr.pubkey]: normalizeUser(usr)} return { ...acc, [usr.pubkey]: normalizeUser(usr) }
}, {}) }, {})
}) })
) )
const allUsers: User[] = Object.values({...normalizedUsers, ...dbUsers}) const allUsers: MetadataCache[] = Object.values({ ...normalizedUsers, ...dbUsers })
return ( return (
<ReactTextareaAutocomplete <ReactTextareaAutocomplete

View File

@ -1,17 +1,16 @@
import { useLiveQuery } from "dexie-react-hooks";
import { useEffect } from "react"; import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux"; import { db } from "../db";
import { HexKey } from "../nostr"; import { HexKey } from "../nostr";
import { RootState } from "../state/Store"; import { System } from "../nostr/System";
import { addPubKey, MetadataCache } from "../state/Users";
export default function useProfile(pubKey: HexKey) { export default function useProfile(pubKey: HexKey) {
const dispatch = useDispatch(); const user = useLiveQuery(async () => {
const user = useSelector<RootState, MetadataCache>(s => s.users.users[pubKey]); return await db.users.get(pubKey);
}, [pubKey]);
useEffect(() => { useEffect(() => {
if (pubKey) { System.GetMetadata(pubKey);
dispatch(addPubKey(pubKey));
}
}, [pubKey]); }, [pubKey]);
return user; return user;

View File

@ -42,16 +42,13 @@ export default function useUsersCache() {
.filter(a => a !== undefined) .filter(a => a !== undefined)
.map(a => a!); .map(a => a!);
dispatch(setUserData(profiles)); dispatch(setUserData(profiles));
const dbProfiles = results.notes.map(ev => { db.users.bulkPut(profiles);
return { ...JSON.parse(ev.content), pubkey: ev.pubkey }
});
db.users.bulkPut(dbProfiles);
}, [results]); }, [results]);
return results; return results;
} }
export function mapEventToProfile(ev: TaggedRawEvent): MetadataCache | undefined { export function mapEventToProfile(ev: TaggedRawEvent) {
try { try {
let data: UserMetadata = JSON.parse(ev.content); let data: UserMetadata = JSON.parse(ev.content);
return { return {
@ -59,7 +56,7 @@ export function mapEventToProfile(ev: TaggedRawEvent): MetadataCache | undefined
created: ev.created_at, created: ev.created_at,
loaded: new Date().getTime(), loaded: new Date().getTime(),
...data ...data
}; } as MetadataCache;
} catch (e) { } catch (e) {
console.error("Failed to parse JSON", ev, e); console.error("Failed to parse JSON", ev, e);
} }

View File

@ -1,6 +1,10 @@
import { TaggedRawEvent } from "."; import { HexKey, TaggedRawEvent } from ".";
import { ProfileCacheExpire } from "../Const";
import { db } from "../db";
import { mapEventToProfile } from "../feed/UsersFeed";
import Connection, { RelaySettings } from "./Connection"; import Connection, { RelaySettings } from "./Connection";
import Event from "./Event"; import Event from "./Event";
import EventKind from "./EventKind";
import { Subscriptions } from "./Subscriptions"; import { Subscriptions } from "./Subscriptions";
/** /**
@ -22,10 +26,17 @@ export class NostrSystem {
*/ */
PendingSubscriptions: Subscriptions[]; PendingSubscriptions: Subscriptions[];
/**
* List of pubkeys to fetch metadata for
*/
WantsMetadata: Set<HexKey>;
constructor() { constructor() {
this.Sockets = new Map(); this.Sockets = new Map();
this.Subscriptions = new Map(); this.Subscriptions = new Map();
this.PendingSubscriptions = []; this.PendingSubscriptions = [];
this.WantsMetadata = new Set();
this._FetchMetadata()
} }
/** /**
@ -79,11 +90,17 @@ export class NostrSystem {
} }
} }
GetMetadata(pk: HexKey) {
if (pk.length > 0) {
this.WantsMetadata.add(pk);
}
}
/** /**
* Request/Response pattern * Request/Response pattern
*/ */
RequestSubscription(sub: Subscriptions) { RequestSubscription(sub: Subscriptions) {
return new Promise((resolve, reject) => { return new Promise<TaggedRawEvent[]>((resolve, reject) => {
let events: TaggedRawEvent[] = []; let events: TaggedRawEvent[] = [];
// force timeout returning current results // force timeout returning current results
@ -119,6 +136,37 @@ export class NostrSystem {
this.AddSubscription(sub); this.AddSubscription(sub);
}); });
} }
async _FetchMetadata() {
let missing = new Set<HexKey>();
for (let pk of this.WantsMetadata) {
let meta = await db.users.get(pk);
let now = new Date().getTime();
if (!meta || meta.loaded < now - ProfileCacheExpire) {
missing.add(pk);
} else {
this.WantsMetadata.delete(pk);
}
}
if (missing.size > 0) {
console.debug("Wants: ", missing);
let sub = new Subscriptions();
sub.Id = `profiles:${sub.Id}`;
sub.Kinds = new Set([EventKind.SetMetadata]);
sub.Authors = missing;
sub.OnEvent = (e) => {
let profile = mapEventToProfile(e);
if (profile) {
db.users.put(profile);
}
}
await this.RequestSubscription(sub);
}
setTimeout(() => this._FetchMetadata(), 500);
}
} }
export const System = new NostrSystem(); export const System = new NostrSystem();

View File

@ -1,9 +0,0 @@
export interface User {
name?: string
about?: string
display_name?: string
nip05?: string
pubkey: string
picture?: string
}

View File

@ -1,6 +1,4 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { ProfileCacheExpire } from '../Const';
import { db } from '../db';
import { HexKey, UserMetadata } from '../nostr'; import { HexKey, UserMetadata } from '../nostr';
export interface MetadataCache extends UserMetadata { export interface MetadataCache extends UserMetadata {
@ -27,10 +25,10 @@ export interface UsersStore {
const UsersSlice = createSlice({ const UsersSlice = createSlice({
name: "Users", name: "Users",
initialState: <UsersStore>{ initialState: {
pubKeys: [], pubKeys: [],
users: {}, users: {},
}, } as UsersStore,
reducers: { reducers: {
addPubKey: (state, action: PayloadAction<string | Array<string>>) => { addPubKey: (state, action: PayloadAction<string | Array<string>>) => {
let keys = action.payload; let keys = action.payload;
@ -38,31 +36,15 @@ const UsersSlice = createSlice({
keys = [keys]; keys = [keys];
} }
let changes = false; let changes = false;
let fromCache = false;
let temp = new Set(state.pubKeys); let temp = new Set(state.pubKeys);
for (let k of keys) { for (let k of keys) {
if (!temp.has(k)) { if (!temp.has(k)) {
changes = true; changes = true;
temp.add(k); temp.add(k);
// load from cache
let cache = window.localStorage.getItem(`user:${k}`);
if (cache) {
let ud: MetadataCache = JSON.parse(cache);
if (ud.loaded > new Date().getTime() - ProfileCacheExpire) {
state.users[ud.pubkey] = ud;
fromCache = true;
}
}
} }
} }
if (changes) { if (changes) {
state.pubKeys = Array.from(temp); state.pubKeys = Array.from(temp);
if (fromCache) {
state.users = {
...state.users
};
}
} }
}, },
setUserData: (state, action: PayloadAction<MetadataCache | Array<MetadataCache>>) => { setUserData: (state, action: PayloadAction<MetadataCache | Array<MetadataCache>>) => {
@ -84,8 +66,6 @@ const UsersSlice = createSlice({
}; };
} }
state.users[x.pubkey] = x; state.users[x.pubkey] = x;
db.users.put(x)
state.users = { state.users = {
...state.users ...state.users
}; };