diff --git a/package.json b/package.json index 14e09b75..36ba650f 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "@types/webscopeio__react-textarea-autocomplete": "^4.7.2", "@webscopeio/react-textarea-autocomplete": "^4.9.2", "bech32": "^2.0.0", + "dexie": "^3.2.2", + "dexie-react-hooks": "^1.1.1", "light-bolt11-decoder": "^2.1.0", "qr-code-styling": "^1.6.0-rc.1", "react": "^18.2.0", diff --git a/src/db.ts b/src/db.ts new file mode 100644 index 00000000..147d5023 --- /dev/null +++ b/src/db.ts @@ -0,0 +1,16 @@ +import Dexie, { Table } from 'dexie'; + +import type { User } from './nostr/types'; + +export class MySubClassedDexie extends Dexie { + users!: Table; + + constructor() { + super('snortDB'); + this.version(1).stores({ + users: '++pubkey, name, display_name, about, nip05' // Primary key and indexed props + }); + } +} + +export const db = new MySubClassedDexie(); diff --git a/src/element/Textarea.tsx b/src/element/Textarea.tsx index f4d290d3..2538058b 100644 --- a/src/element/Textarea.tsx +++ b/src/element/Textarea.tsx @@ -1,4 +1,5 @@ -import { Component } from "react"; +import { useSelector } from "react-redux"; +import { useLiveQuery } from "dexie-react-hooks"; import ReactTextareaAutocomplete from "@webscopeio/react-textarea-autocomplete"; @@ -11,10 +12,11 @@ import Nostrich from "../nostrich.jpg"; // @ts-expect-error import { hexToBech32 } from "../Util"; import type { User } from "../nostr/types"; +import { db } from "../db"; -function searchUsers(query: string, users: Record) { +function searchUsers(query: string, users: User[]) { const q = query.toLowerCase() - return Object.values(users).filter(({ name, display_name, about, nip05 }) => { + return users.filter(({ name, display_name, about, nip05 }: User) => { return name?.toLowerCase().includes(q) || display_name?.toLowerCase().includes(q) || about?.toLowerCase().includes(q) @@ -37,29 +39,39 @@ const UserItem = ({ pubkey, display_name, picture, nip05, ...rest }: User) => { ) } -export default class Textarea extends Component { - render() { +function normalizeUser({ pubkey, about, nip05, name, display_name }: User) { + return { pubkey, about, nip05, name, display_name } +} + +const Textarea = ({ onChange, ...rest }: any) => { // @ts-expect-error - const { users, onChange, ...rest } = this.props + const { users } = useSelector(s => s.users) + const dbUsers = useLiveQuery( + () => db.users.toArray().then(usrs => { + return usrs.reduce((acc, usr) => { + return { ...acc, [usr.pubkey]: normalizeUser(usr)} + }, {}) + }) + ) + const cachedUsers = dbUsers ? dbUsers : {} + const allUsers: User[] = Object.values({...cachedUsers, ...users}) + return ( Loading....} placeholder="Say something!" - ref={rta => { - // @ts-expect-error - this.rta = rta; - }} onChange={onChange} trigger={{ "@": { afterWhitespace: true, - dataProvider: token => searchUsers(token, users), + dataProvider: token => dbUsers ? searchUsers(token, allUsers) : [], component: (props: any) => , output: (item: any) => `@${hexToBech32("npub", item.pubkey)}` } }} /> ) - } } + +export default Textarea diff --git a/src/feed/LoginFeed.js b/src/feed/LoginFeed.js index 4d4034cf..5cadfc40 100644 --- a/src/feed/LoginFeed.js +++ b/src/feed/LoginFeed.js @@ -4,6 +4,7 @@ import EventKind from "../nostr/EventKind"; import { Subscriptions } from "../nostr/Subscriptions"; import { addDirectMessage, addNotifications, setFollows, setRelays } from "../state/Login"; import { setUserData } from "../state/Users"; +import { db } from "../db"; import useSubscription from "./Subscription"; import { mapEventToProfile } from "./UsersFeed"; @@ -41,7 +42,8 @@ export default function useLoginFeed() { useEffect(() => { let contactList = notes.filter(a => a.kind === EventKind.ContactList); let notifications = notes.filter(a => a.kind === EventKind.TextNote); - let metadata = notes.filter(a => a.kind === EventKind.SetMetadata).map(a => mapEventToProfile(a)); + let metadata = notes.filter(a => a.kind === EventKind.SetMetadata) + let profiles = metadata.map(a => mapEventToProfile(a)); let dms = notes.filter(a => a.kind === EventKind.DirectMessage); for (let cl of contactList) { @@ -60,7 +62,11 @@ export default function useLoginFeed() { } } dispatch(addNotifications(notifications)); - dispatch(setUserData(metadata)); + dispatch(setUserData(profiles)); + const userMetadata = metadata.map(ev => { + return {...JSON.parse(ev.content), pubkey: ev.pubkey } + }) + db.users.bulkPut(metadata); dispatch(addDirectMessage(dms)); }, [notes]); } \ No newline at end of file diff --git a/src/feed/UsersFeed.js b/src/feed/UsersFeed.js index f9d87e3c..68db3f05 100644 --- a/src/feed/UsersFeed.js +++ b/src/feed/UsersFeed.js @@ -2,6 +2,7 @@ import { useEffect, useMemo } from "react"; import { useDispatch, useSelector } from "react-redux"; import { ProfileCacheExpire } from "../Const"; import EventKind from "../nostr/EventKind"; +import { db } from "../db"; import { Subscriptions } from "../nostr/Subscriptions"; import { setUserData } from "../state/Users"; import useSubscription from "./Subscription"; @@ -34,7 +35,12 @@ export default function useUsersCache() { const results = useSubscription(sub); useEffect(() => { - dispatch(setUserData(results.notes.map(a => mapEventToProfile(a)))); + const userData = results.notes.map(a => mapEventToProfile(a)); + dispatch(setUserData(userData)); + const profiles = results.notes.map(ev => { + return {...JSON.parse(ev.content), pubkey: ev.pubkey } + }); + db.users.bulkPut(profiles); }, [results]); return results; diff --git a/src/nostr/types.tsx b/src/nostr/types.tsx index 9f9df606..8c02463b 100644 --- a/src/nostr/types.tsx +++ b/src/nostr/types.tsx @@ -1,4 +1,4 @@ -export type User = { +export interface User { name?: string about?: string display_name?: string diff --git a/src/state/Users.js b/src/state/Users.js index d61b95cb..5455b739 100644 --- a/src/state/Users.js +++ b/src/state/Users.js @@ -1,5 +1,6 @@ import { createSlice } from '@reduxjs/toolkit' import { ProfileCacheExpire } from '../Const'; +import { db } from '../db'; const UsersSlice = createSlice({ name: "Users", @@ -67,6 +68,14 @@ const UsersSlice = createSlice({ }; } state.users[x.pubkey] = x; + db.users.put({ + pubkey: x.pubkey, + name: x.name, + display_name: x.display_name, + nip05: x.nip05, + picture: x.picture, + about: x.about, + }) window.localStorage.setItem(`user:${x.pubkey}`, JSON.stringify(x)); state.users = { diff --git a/yarn.lock b/yarn.lock index 1b412655..4a279293 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3704,6 +3704,16 @@ detective@^5.2.1: defined "^1.0.0" minimist "^1.2.6" +dexie-react-hooks@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/dexie-react-hooks/-/dexie-react-hooks-1.1.1.tgz#ff405cc89e5d899ddbac5e40d593f83f9a74106a" + integrity sha512-Cam5JP6PxHN564RvWEoe8cqLhosW0O4CAZ9XEVYeGHJBa6KEJlOpd9CUpV3kmU9dm2MrW97/lk7qkf1xpij7gA== + +dexie@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/dexie/-/dexie-3.2.2.tgz#fa6f2a3c0d6ed0766f8d97a03720056f88fe0e01" + integrity sha512-q5dC3HPmir2DERlX+toCBbHQXW5MsyrFqPFcovkH9N2S/UW/H3H5AWAB6iEOExeraAu+j+zRDG+zg/D7YhH0qg== + didyoumean@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037"