Remove Users store

Performance improvements for profile loader
This commit is contained in:
Kieran 2023-01-16 13:17:29 +00:00
parent f456c09dbe
commit 514616b170
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
16 changed files with 141 additions and 254 deletions

32
src/db/User.ts Normal file
View File

@ -0,0 +1,32 @@
import { HexKey, TaggedRawEvent, UserMetadata } from "../nostr";
export interface MetadataCache extends UserMetadata {
/**
* When the object was saved in cache
*/
loaded: number,
/**
* When the source metadata event was created
*/
created: number,
/**
* The pubkey of the owner of this metadata
*/
pubkey: HexKey
};
export function mapEventToProfile(ev: TaggedRawEvent) {
try {
let data: UserMetadata = JSON.parse(ev.content);
return {
pubkey: ev.pubkey,
created: ev.created_at,
loaded: new Date().getTime(),
...data
} as MetadataCache;
} catch (e) {
console.error("Failed to parse JSON", ev, e);
}
}

View File

@ -1,5 +1,6 @@
import Dexie, { Table } from 'dexie'; import Dexie, { Table } from 'dexie';
import { MetadataCache } from './state/Users'; import { MetadataCache } from './User';
export class SnortDB extends Dexie { export class SnortDB extends Dexie {
users!: Table<MetadataCache>; users!: Table<MetadataCache>;

View File

@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { import {
ServiceProvider, ServiceProvider,
@ -15,14 +15,10 @@ import AsyncButton from "./AsyncButton";
import LNURLTip from "./LNURLTip"; import LNURLTip from "./LNURLTip";
// @ts-ignore // @ts-ignore
import Copy from "./Copy"; import Copy from "./Copy";
// @ts-ignore
import useProfile from "../feed/ProfileFeed"; import useProfile from "../feed/ProfileFeed";
// @ts-ignore
import useEventPublisher from "../feed/EventPublisher"; import useEventPublisher from "../feed/EventPublisher";
// @ts-ignore
import { resetProfile } from "../state/Users";
// @ts-ignore
import { hexToBech32 } from "../Util"; import { hexToBech32 } from "../Util";
import { UserMetadata } from "../nostr";
type Nip05ServiceProps = { type Nip05ServiceProps = {
name: string, name: string,
@ -35,10 +31,9 @@ type Nip05ServiceProps = {
type ReduxStore = any; type ReduxStore = any;
export default function Nip5Service(props: Nip05ServiceProps) { export default function Nip5Service(props: Nip05ServiceProps) {
const dispatch = useDispatch();
const navigate = useNavigate(); const navigate = useNavigate();
const pubkey = useSelector<ReduxStore, string>(s => s.login.publicKey); const pubkey = useSelector<ReduxStore, string>(s => s.login.publicKey);
const user: any = useProfile(pubkey); const user = useProfile(pubkey);
const publisher = useEventPublisher(); const publisher = useEventPublisher();
const svc = new ServiceProvider(props.service); const svc = new ServiceProvider(props.service);
const [serviceConfig, setServiceConfig] = useState<ServiceConfig>(); const [serviceConfig, setServiceConfig] = useState<ServiceConfig>();
@ -71,11 +66,11 @@ export default function Nip5Service(props: Nip05ServiceProps) {
setError(undefined); setError(undefined);
setAvailabilityResponse(undefined); setAvailabilityResponse(undefined);
if (handle && domain) { if (handle && domain) {
if(handle.length < (domainConfig?.length[0] ?? 2)) { if (handle.length < (domainConfig?.length[0] ?? 2)) {
setAvailabilityResponse({ available: false, why: "TOO_SHORT" }); setAvailabilityResponse({ available: false, why: "TOO_SHORT" });
return; return;
} }
if(handle.length > (domainConfig?.length[1] ?? 20)) { if (handle.length > (domainConfig?.length[1] ?? 20)) {
setAvailabilityResponse({ available: false, why: "TOO_LONG" }); setAvailabilityResponse({ available: false, why: "TOO_LONG" });
return; return;
} }
@ -149,17 +144,15 @@ export default function Nip5Service(props: Nip05ServiceProps) {
} }
async function updateProfile(handle: string, domain: string) { async function updateProfile(handle: string, domain: string) {
let newProfile = { if (user) {
...user, let newProfile = {
nip05: `${handle}@${domain}` ...user,
}; nip05: `${handle}@${domain}`
delete newProfile["loaded"]; } as UserMetadata;
delete newProfile["fromEvent"]; let ev = await publisher.metadata(newProfile);
delete newProfile["pubkey"]; publisher.broadcast(ev);
let ev = await publisher.metadata(newProfile); navigate("/settings");
dispatch(resetProfile(pubkey)); }
publisher.broadcast(ev);
navigate("/settings");
} }
return ( return (

View File

@ -1,6 +1,5 @@
import "./Note.css"; import "./Note.css";
import { useCallback } from "react"; import { useCallback, useMemo } from "react";
import { useSelector } from "react-redux";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import Event from "../nostr/Event"; import Event from "../nostr/Event";
@ -10,15 +9,15 @@ import { eventLink, hexToBech32 } from "../Util";
import NoteFooter from "./NoteFooter"; import NoteFooter from "./NoteFooter";
import NoteTime from "./NoteTime"; import NoteTime from "./NoteTime";
import EventKind from "../nostr/EventKind"; import EventKind from "../nostr/EventKind";
import useProfile from "../feed/ProfileFeed";
export default function Note(props) { export default function Note(props) {
const navigate = useNavigate(); const navigate = useNavigate();
const opt = props.options; const { data, isThread, reactions, deletion, hightlight, options: opt, ["data-ev"]: parsedEvent } = props
const dataEvent = props["data-ev"]; const ev = useMemo(() => parsedEvent ?? new Event(data), [data]);
const { data, isThread, reactions, deletion, hightlight } = props const pubKeys = useMemo(() => ev.Thread?.PubKeys || [], [ev]);
const users = useSelector(s => s.users?.users); const users = useProfile(pubKeys);
const ev = dataEvent ?? new Event(data);
const options = { const options = {
showHeader: true, showHeader: true,
@ -32,8 +31,8 @@ export default function Note(props) {
if (deletion?.length > 0) { if (deletion?.length > 0) {
return (<b className="error">Deleted</b>); return (<b className="error">Deleted</b>);
} }
return <Text content={body} tags={ev.Tags} users={users} />; return <Text content={body} tags={ev.Tags} users={users || []} />;
}, [data, dataEvent, reactions, deletion]); }, [props]);
function goToEvent(e, id) { function goToEvent(e, id) {
if (!window.location.pathname.startsWith("/e/")) { if (!window.location.pathname.startsWith("/e/")) {
@ -49,7 +48,7 @@ export default function Note(props) {
const maxMentions = 2; const maxMentions = 2;
let replyId = ev.Thread?.ReplyTo?.Event ?? ev.Thread?.Root?.Event; let replyId = ev.Thread?.ReplyTo?.Event ?? ev.Thread?.Root?.Event;
let mentions = ev.Thread?.PubKeys?.map(a => [a, users[a]])?.map(a => (a[1]?.name?.length ?? 0) > 0 ? a[1].name : hexToBech32("npub", a[0]).substring(0, 12)) let mentions = ev.Thread?.PubKeys?.map(a => [a, users ? users[a] : null])?.map(a => (a[1]?.name?.length ?? 0) > 0 ? a[1].name : hexToBech32("npub", a[0]).substring(0, 12))
.sort((a, b) => a.startsWith("npub") ? 1 : -1); .sort((a, b) => a.startsWith("npub") ? 1 : -1);
let othersLength = mentions.length - maxMentions let othersLength = mentions.length - maxMentions
let pubMentions = mentions.length > maxMentions ? `${mentions?.slice(0, maxMentions).join(", ")} & ${othersLength} other${othersLength > 1 ? 's' : ''}` : mentions?.join(", "); let pubMentions = mentions.length > maxMentions ? `${mentions?.slice(0, maxMentions).join(", ")} & ${othersLength} other${othersLength > 1 ? 's' : ''}` : mentions?.join(", ");

View File

@ -1,5 +1,4 @@
import { useState, Component } from "react"; import { useState } from "react";
import { useSelector } from "react-redux";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPaperclip } from "@fortawesome/free-solid-svg-icons"; import { faPaperclip } from "@fortawesome/free-solid-svg-icons";
@ -20,7 +19,6 @@ export function NoteCreator(props) {
const [note, setNote] = useState(""); const [note, setNote] = useState("");
const [error, setError] = useState(""); const [error, setError] = useState("");
const [active, setActive] = useState(false); const [active, setActive] = useState(false);
const users = useSelector((state) => state.users.users)
async function sendNote() { async function sendNote() {
let ev = replyTo ? await publisher.reply(replyTo, note) : await publisher.note(note); let ev = replyTo ? await publisher.reply(replyTo, note) : await publisher.note(note);
@ -71,7 +69,6 @@ export function NoteCreator(props) {
<Textarea <Textarea
autoFocus={autoFocus} autoFocus={autoFocus}
className={`textarea ${active ? "textarea--focused" : ""}`} className={`textarea ${active ? "textarea--focused" : ""}`}
users={users}
onChange={onChange} onChange={onChange}
value={note} value={note}
onFocus={() => setActive(true)} onFocus={() => setActive(true)}

View File

@ -7,13 +7,14 @@ import useEventPublisher from "../feed/EventPublisher";
import { normalizeReaction, Reaction } from "../Util"; import { normalizeReaction, Reaction } from "../Util";
import { NoteCreator } from "./NoteCreator"; import { NoteCreator } from "./NoteCreator";
import LNURLTip from "./LNURLTip"; import LNURLTip from "./LNURLTip";
import useProfile from "../feed/ProfileFeed";
export default function NoteFooter(props) { export default function NoteFooter(props) {
const reactions = props.reactions; const reactions = props.reactions;
const ev = props.ev; const ev = props.ev;
const login = useSelector(s => s.login.publicKey); const login = useSelector(s => s.login.publicKey);
const author = useSelector(s => s.users.users[ev.RootPubKey]); const author = useProfile(ev.RootPubKey);
const publisher = useEventPublisher(); const publisher = useEventPublisher();
const [reply, setReply] = useState(false); const [reply, setReply] = useState(false);
const [tip, setTip] = useState(false); const [tip, setTip] = useState(false);
@ -105,10 +106,10 @@ export default function NoteFooter(props) {
})} })}
</div> </div>
<NoteCreator <NoteCreator
autoFocus={true} autoFocus={true}
replyTo={ev} replyTo={ev}
onSend={(e) => setReply(false)} onSend={(e) => setReply(false)}
show={reply} show={reply}
/> />
<LNURLTip svc={author?.lud16 || author?.lud06} onClose={(e) => setTip(false)} show={tip} /> <LNURLTip svc={author?.lud16 || author?.lud06} onClose={(e) => setTip(false)} show={tip} />
</> </>

View File

@ -38,11 +38,11 @@ function transformHttpLink(a) {
return <a key={url} href={url} onClick={(e) => e.stopPropagation()}>{url.toString()}</a> return <a key={url} href={url} onClick={(e) => e.stopPropagation()}>{url.toString()}</a>
} }
} else if (tweetId) { } else if (tweetId) {
return ( return (
<div className="tweet" key={tweetId}> <div className="tweet" key={tweetId}>
<TwitterTweetEmbed tweetId={tweetId} /> <TwitterTweetEmbed tweetId={tweetId} />
</div> </div>
) )
} else if (youtubeId) { } else if (youtubeId) {
return ( return (
<> <>

View File

@ -1,4 +1,3 @@
import { useSelector } from "react-redux";
import { useLiveQuery } from "dexie-react-hooks"; import { useLiveQuery } from "dexie-react-hooks";
import ReactTextareaAutocomplete from "@webscopeio/react-textarea-autocomplete"; import ReactTextareaAutocomplete from "@webscopeio/react-textarea-autocomplete";
@ -12,7 +11,7 @@ import "./Textarea.css";
import Nostrich from "../nostrich.jpg"; import Nostrich from "../nostrich.jpg";
import { hexToBech32 } from "../Util"; import { hexToBech32 } from "../Util";
import { db } from "../db"; import { db } from "../db";
import { MetadataCache } from "../state/Users"; import { MetadataCache } from "../db/User";
function searchUsers(query: string, users: MetadataCache[]) { function searchUsers(query: string, users: MetadataCache[]) {
const q = query.toLowerCase() const q = query.toLowerCase()
@ -38,40 +37,28 @@ const UserItem = ({ pubkey, display_name, picture, nip05, ...rest }: MetadataCac
) )
} }
function normalizeUser({ pubkey, picture, nip05, name, display_name }: MetadataCache) {
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 allUsers = useLiveQuery(
return { ...acc, [pk]: normalizeUser(users[pk]) } () => db.users.toArray()
}, {}) );
const dbUsers = useLiveQuery(
() => db.users.toArray().then(usrs => {
return usrs.reduce((acc, usr) => {
return { ...acc, [usr.pubkey]: normalizeUser(usr) }
}, {})
})
)
const allUsers: MetadataCache[] = Object.values({ ...normalizedUsers, ...dbUsers })
return ( return (
<ReactTextareaAutocomplete <ReactTextareaAutocomplete
{...rest} {...rest}
loadingComponent={() => <span>Loading....</span>} loadingComponent={() => <span>Loading....</span>}
placeholder="Say something!" placeholder="Say something!"
onChange={onChange} onChange={onChange}
textAreaComponent={TextareaAutosize} textAreaComponent={TextareaAutosize}
trigger={{ trigger={{
"@": { "@": {
afterWhitespace: true, afterWhitespace: true,
dataProvider: token => dbUsers ? searchUsers(token, allUsers) : [], dataProvider: token => allUsers ? searchUsers(token, allUsers) : [],
component: (props: any) => <UserItem {...props.entity} />, component: (props: any) => <UserItem {...props.entity} />,
output: (item: any) => `@${hexToBech32("npub", item.pubkey)}` output: (item: any) => `@${hexToBech32("npub", item.pubkey)}`
} }
}} }}
/> />
) )
} }
export default Textarea export default Textarea

View File

@ -5,10 +5,9 @@ import EventKind from "../nostr/EventKind";
import { Subscriptions } from "../nostr/Subscriptions"; import { Subscriptions } from "../nostr/Subscriptions";
import { addDirectMessage, addNotifications, setFollows, setRelays } from "../state/Login"; import { addDirectMessage, addNotifications, setFollows, setRelays } from "../state/Login";
import { RootState } from "../state/Store"; import { RootState } from "../state/Store";
import { setUserData } from "../state/Users";
import { db } from "../db"; import { db } from "../db";
import useSubscription from "./Subscription"; import useSubscription from "./Subscription";
import { mapEventToProfile } from "./UsersFeed"; import { mapEventToProfile } from "../db/User";
/** /**
* Managed loading data for the current logged in user * Managed loading data for the current logged in user
@ -63,7 +62,6 @@ export default function useLoginFeed() {
} }
} }
dispatch(addNotifications(notifications)); dispatch(addNotifications(notifications));
dispatch(setUserData(profiles));
db.users.bulkPut(profiles); db.users.bulkPut(profiles);
dispatch(addDirectMessage(dms)); dispatch(addDirectMessage(dms));
}, [main]); }, [main]);

View File

@ -1,16 +1,26 @@
import { useLiveQuery } from "dexie-react-hooks"; import { useLiveQuery } from "dexie-react-hooks";
import { useEffect } from "react"; import { useEffect, useMemo } from "react";
import { db } from "../db"; import { db } from "../db";
import { HexKey } from "../nostr"; import { HexKey } from "../nostr";
import { System } from "../nostr/System"; import { System } from "../nostr/System";
export default function useProfile(pubKey: HexKey) { export default function useProfile(pubKey: HexKey | Array<HexKey>) {
const user = useLiveQuery(async () => { const user = useLiveQuery(async () => {
return await db.users.get(pubKey); if (pubKey) {
if (Array.isArray(pubKey)) {
let ret = await db.users.bulkGet(pubKey);
return ret.filter(a => a !== undefined).map(a => a!);
} else {
return await db.users.get(pubKey);
}
}
}, [pubKey]); }, [pubKey]);
useEffect(() => { useEffect(() => {
System.GetMetadata(pubKey); if (pubKey) {
System.TrackMetadata(pubKey);
return () => System.UntrackMetadata(pubKey);
}
}, [pubKey]); }, [pubKey]);
return user; return user;

View File

@ -1,63 +0,0 @@
import { useEffect, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { ProfileCacheExpire } from "../Const";
import { HexKey, TaggedRawEvent, UserMetadata } from "../nostr";
import EventKind from "../nostr/EventKind";
import { db } from "../db";
import { Subscriptions } from "../nostr/Subscriptions";
import { RootState } from "../state/Store";
import { MetadataCache, setUserData } from "../state/Users";
import useSubscription from "./Subscription";
export default function useUsersCache() {
const dispatch = useDispatch();
const pKeys = useSelector<RootState, HexKey[]>(s => s.users.pubKeys);
const users = useSelector<RootState, any>(s => s.users.users);
function isUserCached(id: HexKey) {
let expire = new Date().getTime() - ProfileCacheExpire;
let u = users[id];
return u !== undefined && u.loaded > expire;
}
const sub = useMemo(() => {
let needProfiles = pKeys.filter(a => !isUserCached(a));
if (needProfiles.length === 0) {
return null;
}
let sub = new Subscriptions();
sub.Id = `profiles:${sub.Id}`;
sub.Authors = new Set(needProfiles.slice(0, 20));
sub.Kinds = new Set([EventKind.SetMetadata]);
return sub;
}, [pKeys]);
const results = useSubscription(sub);
useEffect(() => {
let profiles: MetadataCache[] = results.notes
.map(a => mapEventToProfile(a))
.filter(a => a !== undefined)
.map(a => a!);
dispatch(setUserData(profiles));
db.users.bulkPut(profiles);
}, [results]);
return results;
}
export function mapEventToProfile(ev: TaggedRawEvent) {
try {
let data: UserMetadata = JSON.parse(ev.content);
return {
pubkey: ev.pubkey,
created: ev.created_at,
loaded: new Date().getTime(),
...data
} as MetadataCache;
} catch (e) {
console.error("Failed to parse JSON", ev, e);
}
}

View File

@ -1,7 +1,7 @@
import { HexKey, TaggedRawEvent } from "."; import { HexKey, TaggedRawEvent } from ".";
import { ProfileCacheExpire } from "../Const"; import { ProfileCacheExpire } from "../Const";
import { db } from "../db"; import { db } from "../db";
import { mapEventToProfile } from "../feed/UsersFeed"; import { mapEventToProfile, MetadataCache } from "../db/User";
import Connection, { RelaySettings } from "./Connection"; import Connection, { RelaySettings } from "./Connection";
import Event from "./Event"; import Event from "./Event";
import EventKind from "./EventKind"; import EventKind from "./EventKind";
@ -90,9 +90,25 @@ export class NostrSystem {
} }
} }
GetMetadata(pk: HexKey) { /**
if (pk.length > 0) { * Request profile metadata for a set of pubkeys
this.WantsMetadata.add(pk); */
TrackMetadata(pk: HexKey | Array<HexKey>) {
for (let p of Array.isArray(pk) ? pk : [pk]) {
if (p.length > 0) {
this.WantsMetadata.add(p);
}
}
}
/**
* Stop tracking metadata for a set of pubkeys
*/
UntrackMetadata(pk: HexKey | Array<HexKey>) {
for (let p of Array.isArray(pk) ? pk : [pk]) {
if (p.length > 0) {
this.WantsMetadata.delete(p);
}
} }
} }
@ -139,11 +155,11 @@ export class NostrSystem {
async _FetchMetadata() { async _FetchMetadata() {
let missing = new Set<HexKey>(); let missing = new Set<HexKey>();
let meta = await db.users.bulkGet(Array.from(this.WantsMetadata));
let now = new Date().getTime();
for (let pk of this.WantsMetadata) { for (let pk of this.WantsMetadata) {
let meta = await db.users.get(pk); let m = meta.find(a => a?.pubkey === pk);
let now = new Date().getTime(); if (!m || m.loaded < (now - ProfileCacheExpire)) {
this.WantsMetadata.delete(pk); // always remove from wants list
if (!meta || meta.loaded < (now - ProfileCacheExpire)) {
missing.add(pk); missing.add(pk);
// cap 100 missing profiles // cap 100 missing profiles
if (missing.size >= 100) { if (missing.size >= 100) {
@ -153,19 +169,27 @@ export class NostrSystem {
} }
if (missing.size > 0) { if (missing.size > 0) {
console.debug("Wants: ", missing); console.debug("Wants profiles: ", missing);
let sub = new Subscriptions(); let sub = new Subscriptions();
sub.Id = `profiles:${sub.Id}`; sub.Id = `profiles:${sub.Id}`;
sub.Kinds = new Set([EventKind.SetMetadata]); sub.Kinds = new Set([EventKind.SetMetadata]);
sub.Authors = missing; sub.Authors = missing;
sub.OnEvent = (e) => { sub.OnEvent = async (e) => {
let profile = mapEventToProfile(e); let profile = mapEventToProfile(e);
if (profile) { if (profile) {
db.users.put(profile); await db.users.put(profile);
} }
} }
await this.RequestSubscription(sub); let results = await this.RequestSubscription(sub);
let couldNotFetch = Array.from(missing).filter(a => !results.some(b => b.pubkey === a));
console.debug("No profiles: ", couldNotFetch);
await db.users.bulkPut(couldNotFetch.map(a => {
return {
pubkey: a,
loaded: new Date().getTime()
} as MetadataCache;
}));
} }
setTimeout(() => this._FetchMetadata(), 500); setTimeout(() => this._FetchMetadata(), 500);

View File

@ -9,7 +9,6 @@ import { System } from "../nostr/System"
import ProfileImage from "../element/ProfileImage"; import ProfileImage from "../element/ProfileImage";
import { init } from "../state/Login"; import { init } from "../state/Login";
import useLoginFeed from "../feed/LoginFeed"; import useLoginFeed from "../feed/LoginFeed";
import useUsersCache from "../feed/UsersFeed";
export default function Layout(props) { export default function Layout(props) {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -19,7 +18,6 @@ export default function Layout(props) {
const relays = useSelector(s => s.login.relays); const relays = useSelector(s => s.login.relays);
const notifications = useSelector(s => s.login.notifications); const notifications = useSelector(s => s.login.notifications);
const readNotifications = useSelector(s => s.login.readNotifications); const readNotifications = useSelector(s => s.login.readNotifications);
useUsersCache();
useLoginFeed(); useLoginFeed();
useEffect(() => { useEffect(() => {

View File

@ -11,7 +11,6 @@ import useEventPublisher from "../feed/EventPublisher";
import useProfile from "../feed/ProfileFeed"; import useProfile from "../feed/ProfileFeed";
import VoidUpload from "../feed/VoidUpload"; import VoidUpload from "../feed/VoidUpload";
import { logout, setRelays } from "../state/Login"; import { logout, setRelays } from "../state/Login";
import { resetProfile } from "../state/Users";
import { hexToBech32, openFile } from "../Util"; import { hexToBech32, openFile } from "../Util";
import Relay from "../element/Relay"; import Relay from "../element/Relay";
import Copy from "../element/Copy"; import Copy from "../element/Copy";
@ -87,7 +86,6 @@ export default function SettingsPage(props) {
let ev = await publisher.metadata(userCopy); let ev = await publisher.metadata(userCopy);
console.debug(ev); console.debug(ev);
dispatch(resetProfile(id));
publisher.broadcast(ev); publisher.broadcast(ev);
} }

View File

@ -1,10 +1,8 @@
import { configureStore } from "@reduxjs/toolkit"; import { configureStore } from "@reduxjs/toolkit";
import { reducer as UsersReducer } from "./Users";
import { reducer as LoginReducer } from "./Login"; import { reducer as LoginReducer } from "./Login";
const store = configureStore({ const store = configureStore({
reducer: { reducer: {
users: UsersReducer,
login: LoginReducer login: LoginReducer
} }
}); });

View File

@ -1,86 +0,0 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { HexKey, UserMetadata } from '../nostr';
export interface MetadataCache extends UserMetadata {
/**
* When the object was saved in cache
*/
loaded: number,
/**
* When the source metadata event was created
*/
created: number,
/**
* The pubkey of the owner of this metadata
*/
pubkey: HexKey
};
export interface UsersStore {
pubKeys: HexKey[],
users: any
};
const UsersSlice = createSlice({
name: "Users",
initialState: {
pubKeys: [],
users: {},
} as UsersStore,
reducers: {
addPubKey: (state, action: PayloadAction<string | Array<string>>) => {
let keys = action.payload;
if (!Array.isArray(keys)) {
keys = [keys];
}
let changes = false;
let temp = new Set(state.pubKeys);
for (let k of keys) {
if (!temp.has(k)) {
changes = true;
temp.add(k);
}
}
if (changes) {
state.pubKeys = Array.from(temp);
}
},
setUserData: (state, action: PayloadAction<MetadataCache | Array<MetadataCache>>) => {
let ud = action.payload;
if (!Array.isArray(ud)) {
ud = [ud];
}
for (let x of ud) {
let existing = state.users[x.pubkey];
if (existing) {
if (existing.created > x.created) {
// prevent patching with older metadata
continue;
}
x = {
...existing,
...x
};
}
state.users[x.pubkey] = x;
state.users = {
...state.users
};
}
},
resetProfile: (state, action: PayloadAction<HexKey>) => {
if (action.payload in state.users) {
delete state.users[action.payload];
state.users = {
...state.users
};
}
}
}
});
export const { addPubKey, setUserData, resetProfile } = UsersSlice.actions;
export const reducer = UsersSlice.reducer;