feat: in-memory fallback for storing user profiles (#110)
This commit is contained in:
parent
2e3fe92b1f
commit
d070185322
@ -1,39 +0,0 @@
|
|||||||
import { HexKey, TaggedRawEvent, UserMetadata } from "Nostr";
|
|
||||||
import { hexToBech32 } from "../Util";
|
|
||||||
|
|
||||||
export interface MetadataCache extends UserMetadata {
|
|
||||||
/**
|
|
||||||
* When the object was saved in cache
|
|
||||||
*/
|
|
||||||
loaded: number,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When the source metadata event was created
|
|
||||||
*/
|
|
||||||
created: number,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The pubkey of the owner of this metadata
|
|
||||||
*/
|
|
||||||
pubkey: HexKey,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* bech32 encoded pub key
|
|
||||||
*/
|
|
||||||
npub: string
|
|
||||||
};
|
|
||||||
|
|
||||||
export function mapEventToProfile(ev: TaggedRawEvent) {
|
|
||||||
try {
|
|
||||||
let data: UserMetadata = JSON.parse(ev.content);
|
|
||||||
return {
|
|
||||||
pubkey: ev.pubkey,
|
|
||||||
npub: hexToBech32("npub", ev.pubkey),
|
|
||||||
created: ev.created_at,
|
|
||||||
loaded: new Date().getTime(),
|
|
||||||
...data
|
|
||||||
} as MetadataCache;
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to parse JSON", ev, e);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +1,20 @@
|
|||||||
import Dexie, { Table } from "dexie";
|
import Dexie, { Table } from "dexie";
|
||||||
import { MetadataCache } from "Db/User";
|
import { MetadataCache } from "State/Users";
|
||||||
import { hexToBech32 } from "Util";
|
import { hexToBech32 } from "Util";
|
||||||
|
|
||||||
|
export const NAME = 'snortDB'
|
||||||
|
export const VERSION = 2
|
||||||
|
|
||||||
|
const STORES = {
|
||||||
|
users: '++pubkey, name, display_name, picture, nip05, npub'
|
||||||
|
}
|
||||||
|
|
||||||
export class SnortDB extends Dexie {
|
export class SnortDB extends Dexie {
|
||||||
users!: Table<MetadataCache>;
|
users!: Table<MetadataCache>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super('snortDB');
|
super(NAME);
|
||||||
this.version(2).stores({
|
this.version(VERSION).stores(STORES).upgrade(tx => {
|
||||||
users: '++pubkey, name, display_name, picture, nip05, npub'
|
|
||||||
}).upgrade(tx => {
|
|
||||||
return tx.table("users").toCollection().modify(user => {
|
return tx.table("users").toCollection().modify(user => {
|
||||||
user.npub = hexToBech32("npub", user.pubkey)
|
user.npub = hexToBech32("npub", user.pubkey)
|
||||||
})
|
})
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import useProfile from "Feed/ProfileFeed";
|
import { useUserProfile } from "Feed/ProfileFeed";
|
||||||
import { HexKey } from "Nostr";
|
import { HexKey } from "Nostr";
|
||||||
import { hexToBech32, profileLink } from "Util";
|
import { hexToBech32, profileLink } from "Util";
|
||||||
|
|
||||||
export default function Mention({ pubkey }: { pubkey: HexKey }) {
|
export default function Mention({ pubkey }: { pubkey: HexKey }) {
|
||||||
const user = useProfile(pubkey)?.get(pubkey);
|
const user = useUserProfile(pubkey)
|
||||||
|
|
||||||
const name = useMemo(() => {
|
const name = useMemo(() => {
|
||||||
let name = hexToBech32("npub", pubkey).substring(0, 12);
|
let name = hexToBech32("npub", pubkey).substring(0, 12);
|
||||||
|
@ -13,7 +13,7 @@ import {
|
|||||||
import AsyncButton from "Element/AsyncButton";
|
import AsyncButton from "Element/AsyncButton";
|
||||||
import LNURLTip from "Element/LNURLTip";
|
import LNURLTip from "Element/LNURLTip";
|
||||||
import Copy from "Element/Copy";
|
import Copy from "Element/Copy";
|
||||||
import useProfile from "Feed/ProfileFeed";
|
import { useUserProfile }from "Feed/ProfileFeed";
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Feed/EventPublisher";
|
||||||
import { debounce, hexToBech32 } from "Util";
|
import { debounce, hexToBech32 } from "Util";
|
||||||
import { UserMetadata } from "Nostr";
|
import { UserMetadata } from "Nostr";
|
||||||
@ -31,7 +31,7 @@ type ReduxStore = any;
|
|||||||
export default function Nip5Service(props: Nip05ServiceProps) {
|
export default function Nip5Service(props: Nip05ServiceProps) {
|
||||||
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 = useProfile(pubkey);
|
const user = useUserProfile(pubkey);
|
||||||
const publisher = useEventPublisher();
|
const publisher = useEventPublisher();
|
||||||
const svc = useMemo(() => new ServiceProvider(props.service), [props.service]);
|
const svc = useMemo(() => new ServiceProvider(props.service), [props.service]);
|
||||||
const [serviceConfig, setServiceConfig] = useState<ServiceConfig>();
|
const [serviceConfig, setServiceConfig] = useState<ServiceConfig>();
|
||||||
|
@ -9,7 +9,7 @@ import { eventLink, getReactions, hexToBech32 } from "Util";
|
|||||||
import NoteFooter from "Element/NoteFooter";
|
import NoteFooter from "Element/NoteFooter";
|
||||||
import NoteTime from "Element/NoteTime";
|
import NoteTime from "Element/NoteTime";
|
||||||
import EventKind from "Nostr/EventKind";
|
import EventKind from "Nostr/EventKind";
|
||||||
import useProfile from "Feed/ProfileFeed";
|
import { useUserProfiles } from "Feed/ProfileFeed";
|
||||||
import { TaggedRawEvent, u256 } from "Nostr";
|
import { TaggedRawEvent, u256 } from "Nostr";
|
||||||
import { useInView } from "react-intersection-observer";
|
import { useInView } from "react-intersection-observer";
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ export default function Note(props: NoteProps) {
|
|||||||
const { data, isThread, related, highlight, options: opt, ["data-ev"]: parsedEvent } = props
|
const { data, isThread, related, highlight, options: opt, ["data-ev"]: parsedEvent } = props
|
||||||
const ev = useMemo(() => parsedEvent ?? new NEvent(data), [data]);
|
const ev = useMemo(() => parsedEvent ?? new NEvent(data), [data]);
|
||||||
const pubKeys = useMemo(() => ev.Thread?.PubKeys || [], [ev]);
|
const pubKeys = useMemo(() => ev.Thread?.PubKeys || [], [ev]);
|
||||||
const users = useProfile(pubKeys);
|
const users = useUserProfiles(pubKeys);
|
||||||
const deletions = useMemo(() => getReactions(related, ev.Id, EventKind.Deletion), [related]);
|
const deletions = useMemo(() => getReactions(related, ev.Id, EventKind.Deletion), [related]);
|
||||||
const { ref, inView } = useInView({ triggerOnce: true });
|
const { ref, inView } = useInView({ triggerOnce: true });
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import useEventPublisher from "Feed/EventPublisher";
|
|||||||
import { getReactions, hexToBech32, normalizeReaction, Reaction } from "Util";
|
import { getReactions, hexToBech32, normalizeReaction, Reaction } from "Util";
|
||||||
import { NoteCreator } from "Element/NoteCreator";
|
import { NoteCreator } from "Element/NoteCreator";
|
||||||
import LNURLTip from "Element/LNURLTip";
|
import LNURLTip from "Element/LNURLTip";
|
||||||
import useProfile from "Feed/ProfileFeed";
|
import { useUserProfile } from "Feed/ProfileFeed";
|
||||||
import { default as NEvent } from "Nostr/Event";
|
import { default as NEvent } from "Nostr/Event";
|
||||||
import { RootState } from "State/Store";
|
import { RootState } from "State/Store";
|
||||||
import { HexKey, TaggedRawEvent } from "Nostr";
|
import { HexKey, TaggedRawEvent } from "Nostr";
|
||||||
@ -26,7 +26,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||||||
|
|
||||||
const login = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
|
const login = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
|
||||||
const prefs = useSelector<RootState, UserPreferences>(s => s.login.preferences);
|
const prefs = useSelector<RootState, UserPreferences>(s => s.login.preferences);
|
||||||
const author = useProfile(ev.RootPubKey)?.get(ev.RootPubKey);
|
const author = useUserProfile(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);
|
||||||
|
@ -3,7 +3,7 @@ import "./NoteToSelf.css";
|
|||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faBook, faCertificate } from "@fortawesome/free-solid-svg-icons"
|
import { faBook, faCertificate } from "@fortawesome/free-solid-svg-icons"
|
||||||
import useProfile from "Feed/ProfileFeed";
|
import { useUserProfile } from "Feed/ProfileFeed";
|
||||||
import Nip05 from "Element/Nip05";
|
import Nip05 from "Element/Nip05";
|
||||||
import { profileLink } from "Util";
|
import { profileLink } from "Util";
|
||||||
|
|
||||||
@ -15,7 +15,7 @@ export interface NoteToSelfProps {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function NoteLabel({pubkey, link}:NoteToSelfProps) {
|
function NoteLabel({pubkey, link}:NoteToSelfProps) {
|
||||||
const user = useProfile(pubkey)?.get(pubkey);
|
const user = useUserProfile(pubkey);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
Note to Self <FontAwesomeIcon icon={faCertificate} size="xs" />
|
Note to Self <FontAwesomeIcon icon={faCertificate} size="xs" />
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import "./ProfileImage.css";
|
import "./ProfileImage.css";
|
||||||
|
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import useProfile from "Feed/ProfileFeed";
|
import { useUserProfile } from "Feed/ProfileFeed";
|
||||||
import { hexToBech32, profileLink } from "Util";
|
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 } from "Nostr";
|
import { HexKey } from "Nostr";
|
||||||
import { MetadataCache } from "Db/User";
|
import { MetadataCache } from "State/Users";
|
||||||
|
|
||||||
export interface ProfileImageProps {
|
export interface ProfileImageProps {
|
||||||
pubkey: HexKey,
|
pubkey: HexKey,
|
||||||
@ -19,7 +19,7 @@ export interface ProfileImageProps {
|
|||||||
|
|
||||||
export default function ProfileImage({ pubkey, subHeader, showUsername = true, className, link }: ProfileImageProps) {
|
export default function ProfileImage({ pubkey, subHeader, showUsername = true, className, link }: ProfileImageProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const user = useProfile(pubkey)?.get(pubkey);
|
const user = useUserProfile(pubkey);
|
||||||
|
|
||||||
const name = useMemo(() => {
|
const name = useMemo(() => {
|
||||||
return getDisplayName(user, pubkey);
|
return getDisplayName(user, pubkey);
|
||||||
|
@ -3,7 +3,7 @@ import { ReactNode } from "react";
|
|||||||
|
|
||||||
import ProfileImage from "Element/ProfileImage";
|
import ProfileImage from "Element/ProfileImage";
|
||||||
import FollowButton from "Element/FollowButton";
|
import FollowButton from "Element/FollowButton";
|
||||||
import useProfile from "Feed/ProfileFeed";
|
import { useUserProfile } from "Feed/ProfileFeed";
|
||||||
import { HexKey } from "Nostr";
|
import { HexKey } from "Nostr";
|
||||||
import { useInView } from "react-intersection-observer";
|
import { useInView } from "react-intersection-observer";
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ export interface ProfilePreviewProps {
|
|||||||
}
|
}
|
||||||
export default function ProfilePreview(props: ProfilePreviewProps) {
|
export default function ProfilePreview(props: ProfilePreviewProps) {
|
||||||
const pubkey = props.pubkey;
|
const pubkey = props.pubkey;
|
||||||
const user = useProfile(pubkey)?.get(pubkey);
|
const user = useUserProfile(pubkey);
|
||||||
const { ref, inView } = useInView({ triggerOnce: true });
|
const { ref, inView } = useInView({ triggerOnce: true });
|
||||||
const options = {
|
const options = {
|
||||||
about: true,
|
about: true,
|
||||||
|
@ -10,7 +10,7 @@ import Invoice from "Element/Invoice";
|
|||||||
import Hashtag from "Element/Hashtag";
|
import Hashtag from "Element/Hashtag";
|
||||||
|
|
||||||
import Tag from "Nostr/Tag";
|
import Tag from "Nostr/Tag";
|
||||||
import { MetadataCache } from "Db/User";
|
import { MetadataCache } from "State/Users";
|
||||||
import Mention from "Element/Mention";
|
import Mention from "Element/Mention";
|
||||||
import TidalEmbed from "Element/TidalEmbed";
|
import TidalEmbed from "Element/TidalEmbed";
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
@ -2,7 +2,6 @@ import "@webscopeio/react-textarea-autocomplete/style.css";
|
|||||||
import "./Textarea.css";
|
import "./Textarea.css";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useLiveQuery } from "dexie-react-hooks";
|
|
||||||
import ReactTextareaAutocomplete from "@webscopeio/react-textarea-autocomplete";
|
import ReactTextareaAutocomplete from "@webscopeio/react-textarea-autocomplete";
|
||||||
import emoji from "@jukben/emoji-search";
|
import emoji from "@jukben/emoji-search";
|
||||||
import TextareaAutosize from "react-textarea-autosize";
|
import TextareaAutosize from "react-textarea-autosize";
|
||||||
@ -10,8 +9,8 @@ import TextareaAutosize from "react-textarea-autosize";
|
|||||||
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 { db } from "Db";
|
import { MetadataCache } from "State/Users";
|
||||||
import { MetadataCache } from "Db/User";
|
import { useQuery } from "State/Users/Hooks";
|
||||||
|
|
||||||
interface EmojiItemProps {
|
interface EmojiItemProps {
|
||||||
name: string
|
name: string
|
||||||
@ -45,16 +44,7 @@ const UserItem = (metadata: MetadataCache) => {
|
|||||||
const Textarea = ({ users, onChange, ...rest }: any) => {
|
const Textarea = ({ users, onChange, ...rest }: any) => {
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
|
|
||||||
const allUsers = useLiveQuery(
|
const allUsers = useQuery(query)
|
||||||
() => db.users
|
|
||||||
.where("npub").startsWithIgnoreCase(query)
|
|
||||||
.or("name").startsWithIgnoreCase(query)
|
|
||||||
.or("display_name").startsWithIgnoreCase(query)
|
|
||||||
.or("nip05").startsWithIgnoreCase(query)
|
|
||||||
.limit(5)
|
|
||||||
.toArray(),
|
|
||||||
[query],
|
|
||||||
);
|
|
||||||
|
|
||||||
const userDataProvider = (token: string) => {
|
const userDataProvider = (token: string) => {
|
||||||
setQuery(token)
|
setQuery(token)
|
||||||
|
@ -2,12 +2,13 @@ import "./ZapButton.css";
|
|||||||
import { faBolt } from "@fortawesome/free-solid-svg-icons";
|
import { faBolt } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import useProfile from "Feed/ProfileFeed";
|
import { useUserProfile } from "Feed/ProfileFeed";
|
||||||
import { HexKey } from "Nostr";
|
import { HexKey } from "Nostr";
|
||||||
import LNURLTip from "Element/LNURLTip";
|
import LNURLTip from "Element/LNURLTip";
|
||||||
|
|
||||||
|
|
||||||
const ZapButton = ({ pubkey, svc }: { pubkey?: HexKey, svc?: string }) => {
|
const ZapButton = ({ pubkey, svc }: { pubkey?: HexKey, svc?: string }) => {
|
||||||
const profile = useProfile(pubkey)?.get(pubkey ?? "");
|
const profile = useUserProfile(pubkey!)
|
||||||
const [zap, setZap] = useState(false);
|
const [zap, setZap] = useState(false);
|
||||||
const service = svc ?? (profile?.lud16 || profile?.lud06);
|
const service = svc ?? (profile?.lud16 || profile?.lud06);
|
||||||
|
|
||||||
|
@ -7,9 +7,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 { db } from "Db";
|
import { mapEventToProfile, MetadataCache } from "State/Users";
|
||||||
|
import { getDb } from "State/Users/Db";
|
||||||
import useSubscription from "Feed/Subscription";
|
import useSubscription from "Feed/Subscription";
|
||||||
import { mapEventToProfile, MetadataCache } from "Db/User";
|
|
||||||
import { getDisplayName } from "Element/ProfileImage";
|
import { getDisplayName } from "Element/ProfileImage";
|
||||||
import { MentionRegex } from "Const";
|
import { MentionRegex } from "Const";
|
||||||
|
|
||||||
@ -87,9 +87,10 @@ export default function useLoginFeed() {
|
|||||||
return acc;
|
return acc;
|
||||||
}, { created: 0, profile: <MetadataCache | null>null });
|
}, { created: 0, profile: <MetadataCache | null>null });
|
||||||
if (maxProfile.profile) {
|
if (maxProfile.profile) {
|
||||||
let existing = await db.users.get(maxProfile.profile.pubkey);
|
const db = getDb()
|
||||||
|
let existing = await db.find(maxProfile.profile.pubkey);
|
||||||
if ((existing?.created ?? 0) < maxProfile.created) {
|
if ((existing?.created ?? 0) < maxProfile.created) {
|
||||||
await db.users.put(maxProfile.profile);
|
await db.put(maxProfile.profile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})().catch(console.warn);
|
})().catch(console.warn);
|
||||||
@ -115,10 +116,12 @@ export default function useLoginFeed() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function makeNotification(ev: TaggedRawEvent) {
|
async function makeNotification(ev: TaggedRawEvent) {
|
||||||
|
const db = getDb()
|
||||||
switch (ev.kind) {
|
switch (ev.kind) {
|
||||||
case EventKind.TextNote: {
|
case EventKind.TextNote: {
|
||||||
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]!)]);
|
||||||
const users = (await db.users.bulkGet(Array.from(pubkeys))).filter(a => a !== undefined).map(a => a!);
|
const users = await db.bulkGet(Array.from(pubkeys))
|
||||||
|
// @ts-ignore
|
||||||
const fromUser = users.find(a => a?.pubkey === ev.pubkey);
|
const fromUser = users.find(a => a?.pubkey === ev.pubkey);
|
||||||
const name = getDisplayName(fromUser, ev.pubkey);
|
const name = getDisplayName(fromUser, ev.pubkey);
|
||||||
const avatarUrl = (fromUser?.picture?.length ?? 0) === 0 ? Nostrich : fromUser?.picture;
|
const avatarUrl = (fromUser?.picture?.length ?? 0) === 0 ? Nostrich : fromUser?.picture;
|
||||||
|
@ -1,27 +1,13 @@
|
|||||||
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 { RootState } from "State/Store";
|
||||||
import { MetadataCache } from "Db/User";
|
import { MetadataCache } from "State/Users";
|
||||||
|
import { useKey, useKeys } from "State/Users/Hooks";
|
||||||
import { HexKey } from "Nostr";
|
import { HexKey } from "Nostr";
|
||||||
import { System } from "Nostr/System";
|
import { System } from "Nostr/System";
|
||||||
|
|
||||||
export default function useProfile(pubKey?: HexKey | Array<HexKey> | undefined): Map<HexKey, MetadataCache> | undefined {
|
export function useUserProfile(pubKey: HexKey): MetadataCache | undefined {
|
||||||
const user = useLiveQuery(async () => {
|
const users = useKey(pubKey);
|
||||||
let userList = new Map<HexKey, MetadataCache>();
|
|
||||||
if (pubKey) {
|
|
||||||
if (Array.isArray(pubKey)) {
|
|
||||||
let ret = await db.users.bulkGet(pubKey);
|
|
||||||
let filtered = ret.filter(a => a !== undefined).map(a => a!);
|
|
||||||
return new Map(filtered.map(a => [a.pubkey, a]))
|
|
||||||
} else {
|
|
||||||
let ret = await db.users.get(pubKey);
|
|
||||||
if (ret) {
|
|
||||||
userList.set(ret.pubkey, ret);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return userList;
|
|
||||||
}, [pubKey]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (pubKey) {
|
if (pubKey) {
|
||||||
@ -30,5 +16,19 @@ export default function useProfile(pubKey?: HexKey | Array<HexKey> | undefined):
|
|||||||
}
|
}
|
||||||
}, [pubKey]);
|
}, [pubKey]);
|
||||||
|
|
||||||
return user;
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function useUserProfiles(pubKeys: Array<HexKey>): Map<HexKey, MetadataCache> | undefined {
|
||||||
|
const users = useKeys(pubKeys);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (pubKeys) {
|
||||||
|
System.TrackMetadata(pubKeys);
|
||||||
|
return () => System.UntrackMetadata(pubKeys);
|
||||||
|
}
|
||||||
|
}, [pubKeys]);
|
||||||
|
|
||||||
|
return users;
|
||||||
}
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import { HexKey, TaggedRawEvent } from "Nostr";
|
import { HexKey, TaggedRawEvent } from "Nostr";
|
||||||
import { ProfileCacheExpire } from "Const";
|
import { ProfileCacheExpire } from "Const";
|
||||||
import { db } from "Db";
|
import { mapEventToProfile, MetadataCache, } from "State/Users";
|
||||||
import { mapEventToProfile, MetadataCache } from "Db/User";
|
import { getDb } from "State/Users/Db";
|
||||||
import Connection, { RelaySettings } from "Nostr/Connection";
|
import Connection, { RelaySettings } from "Nostr/Connection";
|
||||||
import Event from "Nostr/Event";
|
import Event from "Nostr/Event";
|
||||||
import EventKind from "Nostr/EventKind";
|
import EventKind from "Nostr/EventKind";
|
||||||
@ -167,7 +167,8 @@ 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));
|
const db = getDb()
|
||||||
|
let meta = await db.bulkGet(Array.from(this.WantsMetadata));
|
||||||
let expire = new Date().getTime() - ProfileCacheExpire;
|
let expire = new Date().getTime() - ProfileCacheExpire;
|
||||||
for (let pk of this.WantsMetadata) {
|
for (let pk of this.WantsMetadata) {
|
||||||
let m = meta.find(a => a?.pubkey === pk);
|
let m = meta.find(a => a?.pubkey === pk);
|
||||||
@ -190,18 +191,18 @@ export class NostrSystem {
|
|||||||
sub.OnEvent = async (e) => {
|
sub.OnEvent = async (e) => {
|
||||||
let profile = mapEventToProfile(e);
|
let profile = mapEventToProfile(e);
|
||||||
if (profile) {
|
if (profile) {
|
||||||
let existing = await db.users.get(profile.pubkey);
|
let existing = await db.find(profile.pubkey);
|
||||||
if ((existing?.created ?? 0) < profile.created) {
|
if((existing?.created ?? 0) < profile.created) {
|
||||||
await db.users.put(profile);
|
await db.put(profile);
|
||||||
} else if (existing) {
|
} else if(existing) {
|
||||||
await db.users.update(profile.pubkey, { loaded: new Date().getTime() });
|
await db.update(profile.pubkey, { loaded: new Date().getTime() });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let results = await this.RequestSubscription(sub);
|
let results = await this.RequestSubscription(sub);
|
||||||
let couldNotFetch = Array.from(missing).filter(a => !results.some(b => b.pubkey === a));
|
let couldNotFetch = Array.from(missing).filter(a => !results.some(b => b.pubkey === a));
|
||||||
console.debug("No profiles: ", couldNotFetch);
|
console.debug("No profiles: ", couldNotFetch);
|
||||||
await db.users.bulkPut(couldNotFetch.map(a => {
|
await db.bulkPut(couldNotFetch.map(a => {
|
||||||
return {
|
return {
|
||||||
pubkey: a,
|
pubkey: a,
|
||||||
loaded: new Date().getTime()
|
loaded: new Date().getTime()
|
||||||
|
@ -6,7 +6,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
import { faGear, faEnvelope } from "@fortawesome/free-solid-svg-icons";
|
import { faGear, faEnvelope } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
|
||||||
import useProfile from "Feed/ProfileFeed";
|
import { useUserProfile } from "Feed/ProfileFeed";
|
||||||
import FollowButton from "Element/FollowButton";
|
import FollowButton from "Element/FollowButton";
|
||||||
import { extractLnAddress, parseId, hexToBech32 } from "Util";
|
import { extractLnAddress, parseId, hexToBech32 } from "Util";
|
||||||
import Avatar from "Element/Avatar";
|
import Avatar from "Element/Avatar";
|
||||||
@ -33,7 +33,7 @@ export default function ProfilePage() {
|
|||||||
const params = useParams();
|
const params = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const id = useMemo(() => parseId(params.id!), [params]);
|
const id = useMemo(() => parseId(params.id!), [params]);
|
||||||
const user = useProfile(id)?.get(id);
|
const user = useUserProfile(id);
|
||||||
const loginPubKey = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
|
const loginPubKey = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
|
||||||
const follows = useSelector<RootState, HexKey[]>(s => s.login.follows);
|
const follows = useSelector<RootState, HexKey[]>(s => s.login.follows);
|
||||||
const isMe = loginPubKey === id;
|
const isMe = loginPubKey === id;
|
||||||
|
@ -8,7 +8,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
import { faShop } from "@fortawesome/free-solid-svg-icons";
|
import { faShop } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Feed/EventPublisher";
|
||||||
import useProfile from "Feed/ProfileFeed";
|
import { useUserProfile } from "Feed/ProfileFeed";
|
||||||
import VoidUpload from "Feed/VoidUpload";
|
import VoidUpload from "Feed/VoidUpload";
|
||||||
import { logout } from "State/Login";
|
import { logout } from "State/Login";
|
||||||
import { hexToBech32, openFile } from "Util";
|
import { hexToBech32, openFile } from "Util";
|
||||||
@ -22,7 +22,7 @@ export default function ProfileSettings() {
|
|||||||
const id = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
|
const id = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
|
||||||
const privKey = useSelector<RootState, HexKey | undefined>(s => s.login.privateKey);
|
const privKey = useSelector<RootState, HexKey | undefined>(s => s.login.privateKey);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const user = useProfile(id)?.get(id || "");
|
const user = useUserProfile(id!);
|
||||||
const publisher = useEventPublisher();
|
const publisher = useEventPublisher();
|
||||||
|
|
||||||
const [name, setName] = useState<string>();
|
const [name, setName] = useState<string>();
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { configureStore } from "@reduxjs/toolkit";
|
import { configureStore } from "@reduxjs/toolkit";
|
||||||
import { reducer as LoginReducer } from "State/Login";
|
import { reducer as LoginReducer } from "State/Login";
|
||||||
|
import { reducer as UsersReducer } from "State/Users";
|
||||||
|
|
||||||
const store = configureStore({
|
const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
login: LoginReducer
|
login: LoginReducer,
|
||||||
|
users: UsersReducer,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
75
src/State/Users.ts
Normal file
75
src/State/Users.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||||
|
import { HexKey, TaggedRawEvent, UserMetadata } from "Nostr";
|
||||||
|
import { hexToBech32 } from "../Util";
|
||||||
|
|
||||||
|
export interface MetadataCache extends UserMetadata {
|
||||||
|
/**
|
||||||
|
* When the object was saved in cache
|
||||||
|
*/
|
||||||
|
loaded: number,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the source metadata event was created
|
||||||
|
*/
|
||||||
|
created: number,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The pubkey of the owner of this metadata
|
||||||
|
*/
|
||||||
|
pubkey: HexKey
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The bech32 encoded pubkey
|
||||||
|
*/
|
||||||
|
npub: string
|
||||||
|
};
|
||||||
|
|
||||||
|
export function mapEventToProfile(ev: TaggedRawEvent) {
|
||||||
|
try {
|
||||||
|
let data: UserMetadata = JSON.parse(ev.content);
|
||||||
|
return {
|
||||||
|
pubkey: ev.pubkey,
|
||||||
|
npub: hexToBech32("npub", ev.pubkey),
|
||||||
|
created: ev.created_at,
|
||||||
|
loaded: new Date().getTime(),
|
||||||
|
...data
|
||||||
|
} as MetadataCache;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to parse JSON", ev, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UsersDb {
|
||||||
|
isAvailable(): Promise<boolean>
|
||||||
|
query(str: string): Promise<MetadataCache[]>
|
||||||
|
find(key: HexKey): Promise<MetadataCache | undefined>
|
||||||
|
add(user: MetadataCache): Promise<any>
|
||||||
|
put(user: MetadataCache): Promise<any>
|
||||||
|
bulkAdd(users: MetadataCache[]): Promise<any>
|
||||||
|
bulkGet(keys: HexKey[]): Promise<MetadataCache[]>
|
||||||
|
bulkPut(users: MetadataCache[]): Promise<any>
|
||||||
|
update(key: HexKey, fields: Record<string, any>): Promise<any>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UsersStore {
|
||||||
|
/**
|
||||||
|
* A list of seen users
|
||||||
|
*/
|
||||||
|
users: Record<HexKey, MetadataCache>,
|
||||||
|
};
|
||||||
|
|
||||||
|
const InitState = { users: {} } as UsersStore;
|
||||||
|
|
||||||
|
const UsersSlice = createSlice({
|
||||||
|
name: "Users",
|
||||||
|
initialState: InitState,
|
||||||
|
reducers: {
|
||||||
|
setUsers(state, action: PayloadAction<Record<HexKey, MetadataCache>>) {
|
||||||
|
state.users = action.payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { setUsers } = UsersSlice.actions
|
||||||
|
|
||||||
|
export const reducer = UsersSlice.reducer;
|
149
src/State/Users/Db.ts
Normal file
149
src/State/Users/Db.ts
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
import { HexKey } from "Nostr";
|
||||||
|
import { db as idb } from "Db";
|
||||||
|
|
||||||
|
import { UsersDb, MetadataCache, setUsers } from "State/Users";
|
||||||
|
import store from "State/Store";
|
||||||
|
|
||||||
|
class IndexedDb implements UsersDb {
|
||||||
|
isAvailable() {
|
||||||
|
if ("indexedDB" in window) {
|
||||||
|
return new Promise<boolean>((resolve) => {
|
||||||
|
const req = window.indexedDB.open("dummy", 1)
|
||||||
|
req.onsuccess = (ev) => {
|
||||||
|
resolve(true)
|
||||||
|
}
|
||||||
|
req.onerror = (ev) => {
|
||||||
|
resolve(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return Promise.resolve(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
find(key: HexKey) {
|
||||||
|
return idb.users.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
query(q: string) {
|
||||||
|
return idb.users
|
||||||
|
.where("npub").startsWithIgnoreCase(q)
|
||||||
|
.or("name").startsWithIgnoreCase(q)
|
||||||
|
.or("display_name").startsWithIgnoreCase(q)
|
||||||
|
.or("nip05").startsWithIgnoreCase(q)
|
||||||
|
.limit(5)
|
||||||
|
.toArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
bulkGet(keys: HexKey[]) {
|
||||||
|
return idb.users.bulkGet(keys).then(ret => ret.filter(a => a !== undefined).map(a => a!));
|
||||||
|
}
|
||||||
|
|
||||||
|
add(user: MetadataCache) {
|
||||||
|
return idb.users.add(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
put(user: MetadataCache) {
|
||||||
|
return idb.users.put(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
bulkAdd(users: MetadataCache[]) {
|
||||||
|
return idb.users.bulkAdd(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
bulkPut(users: MetadataCache[]) {
|
||||||
|
return idb.users.bulkPut(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
update(key: HexKey, fields: Record<string, any>) {
|
||||||
|
return idb.users.update(key, fields)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupByPubkey(acc: Record<HexKey, MetadataCache>, user: MetadataCache) {
|
||||||
|
return { ...acc, [user.pubkey]: user }
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReduxUsersDb implements UsersDb {
|
||||||
|
async isAvailable() { return true }
|
||||||
|
|
||||||
|
async query(q: string) {
|
||||||
|
const state = store.getState()
|
||||||
|
const { users } = state.users
|
||||||
|
return Object.values(users).filter(user => {
|
||||||
|
const profile = user as MetadataCache
|
||||||
|
return profile.name?.includes(q)
|
||||||
|
|| profile.npub?.includes(q)
|
||||||
|
|| profile.display_name?.includes(q)
|
||||||
|
|| profile.nip05?.includes(q)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async find(key: HexKey) {
|
||||||
|
const state = store.getState()
|
||||||
|
const { users } = state.users
|
||||||
|
return users[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async add(user: MetadataCache) {
|
||||||
|
const state = store.getState()
|
||||||
|
const { users } = state.users
|
||||||
|
store.dispatch(setUsers({...users, [user.pubkey]: user }))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async put(user: MetadataCache) {
|
||||||
|
const state = store.getState()
|
||||||
|
const { users } = state.users
|
||||||
|
store.dispatch(setUsers({...users, [user.pubkey]: user }))
|
||||||
|
}
|
||||||
|
|
||||||
|
async bulkAdd(newUserProfiles: MetadataCache[]) {
|
||||||
|
const state = store.getState()
|
||||||
|
const { users } = state.users
|
||||||
|
const newUsers = newUserProfiles.reduce(groupByPubkey, {})
|
||||||
|
store.dispatch(setUsers({...users, ...newUsers }))
|
||||||
|
}
|
||||||
|
|
||||||
|
async bulkGet(keys: HexKey[]) {
|
||||||
|
const state = store.getState()
|
||||||
|
const { users } = state.users
|
||||||
|
const ids = new Set([...keys])
|
||||||
|
return Object.values(users).filter(user => {
|
||||||
|
return ids.has(user.pubkey)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(key: HexKey, fields: Record<string, any>) {
|
||||||
|
const state = store.getState()
|
||||||
|
const { users } = state.users
|
||||||
|
const current = users[key]
|
||||||
|
const updated = {...current, ...fields }
|
||||||
|
store.dispatch(setUsers({...users, [key]: updated }))
|
||||||
|
}
|
||||||
|
|
||||||
|
async bulkPut(newUsers: MetadataCache[]) {
|
||||||
|
const state = store.getState()
|
||||||
|
const { users } = state.users
|
||||||
|
const newProfiles = newUsers.reduce(groupByPubkey, {})
|
||||||
|
store.dispatch(setUsers({ ...users, ...newProfiles }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const indexedDb = new IndexedDb()
|
||||||
|
export const inMemoryDb = new ReduxUsersDb()
|
||||||
|
|
||||||
|
let db: UsersDb = inMemoryDb
|
||||||
|
indexedDb.isAvailable().then((available) => {
|
||||||
|
if (available) {
|
||||||
|
console.debug('Using Indexed DB')
|
||||||
|
db = indexedDb;
|
||||||
|
} else {
|
||||||
|
console.debug('Using in-memory DB')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export function getDb() {
|
||||||
|
return db
|
||||||
|
}
|
60
src/State/Users/Hooks.ts
Normal file
60
src/State/Users/Hooks.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { useSelector } from "react-redux"
|
||||||
|
import { useLiveQuery } from "dexie-react-hooks";
|
||||||
|
import { MetadataCache } from "State/Users";
|
||||||
|
import { getDb, inMemoryDb } from "State/Users/Db";
|
||||||
|
import type { RootState } from "State/Store"
|
||||||
|
import { HexKey } from "Nostr";
|
||||||
|
|
||||||
|
export function useQuery(query: string, limit: number = 5) {
|
||||||
|
const db = getDb()
|
||||||
|
|
||||||
|
const allUsers = useLiveQuery(
|
||||||
|
() => db.query(query)
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
}).then(() => {
|
||||||
|
return inMemoryDb.query(query)
|
||||||
|
}),
|
||||||
|
[query],
|
||||||
|
)
|
||||||
|
|
||||||
|
return allUsers
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useKey(pubKey: HexKey) {
|
||||||
|
const db = getDb()
|
||||||
|
const { users } = useSelector((state: RootState) => state.users)
|
||||||
|
const defaultUser = users[pubKey]
|
||||||
|
|
||||||
|
const user = useLiveQuery(async () => {
|
||||||
|
if (pubKey) {
|
||||||
|
try {
|
||||||
|
return await db.find(pubKey);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
return defaultUser
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [pubKey, defaultUser]);
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useKeys(pubKeys: HexKey[]): Map<HexKey, MetadataCache> {
|
||||||
|
const db = getDb()
|
||||||
|
const dbUsers = useLiveQuery(async () => {
|
||||||
|
if (pubKeys) {
|
||||||
|
try {
|
||||||
|
const ret = await db.bulkGet(pubKeys);
|
||||||
|
return new Map(ret.map(a => [a.pubkey, a]))
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
const ret = await inMemoryDb.bulkGet(pubKeys);
|
||||||
|
return new Map(ret.map(a => [a.pubkey, a]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Map()
|
||||||
|
}, [pubKeys]);
|
||||||
|
|
||||||
|
return dbUsers!
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user