forked from Kieran/snort
Remove Users store
Performance improvements for profile loader
This commit is contained in:
parent
f456c09dbe
commit
514616b170
32
src/db/User.ts
Normal file
32
src/db/User.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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>;
|
@ -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>();
|
||||||
@ -149,18 +144,16 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function updateProfile(handle: string, domain: string) {
|
async function updateProfile(handle: string, domain: string) {
|
||||||
|
if (user) {
|
||||||
let newProfile = {
|
let newProfile = {
|
||||||
...user,
|
...user,
|
||||||
nip05: `${handle}@${domain}`
|
nip05: `${handle}@${domain}`
|
||||||
};
|
} as UserMetadata;
|
||||||
delete newProfile["loaded"];
|
|
||||||
delete newProfile["fromEvent"];
|
|
||||||
delete newProfile["pubkey"];
|
|
||||||
let ev = await publisher.metadata(newProfile);
|
let ev = await publisher.metadata(newProfile);
|
||||||
dispatch(resetProfile(pubkey));
|
|
||||||
publisher.broadcast(ev);
|
publisher.broadcast(ev);
|
||||||
navigate("/settings");
|
navigate("/settings");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -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(", ");
|
||||||
|
@ -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)}
|
||||||
|
@ -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);
|
||||||
|
@ -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,22 +37,10 @@ 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
|
||||||
@ -65,7 +52,7 @@ const Textarea = ({ users, onChange, ...rest }: any) => {
|
|||||||
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)}`
|
||||||
}
|
}
|
||||||
|
@ -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]);
|
||||||
|
@ -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 () => {
|
||||||
|
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);
|
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;
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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>();
|
||||||
for (let pk of this.WantsMetadata) {
|
let meta = await db.users.bulkGet(Array.from(this.WantsMetadata));
|
||||||
let meta = await db.users.get(pk);
|
|
||||||
let now = new Date().getTime();
|
let now = new Date().getTime();
|
||||||
this.WantsMetadata.delete(pk); // always remove from wants list
|
for (let pk of this.WantsMetadata) {
|
||||||
if (!meta || meta.loaded < (now - ProfileCacheExpire)) {
|
let m = meta.find(a => a?.pubkey === pk);
|
||||||
|
if (!m || m.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);
|
||||||
|
@ -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(() => {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -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;
|
|
Loading…
Reference in New Issue
Block a user