perf: improve loading speed (#116)

This commit is contained in:
Cody Tseng
2025-02-12 22:09:00 +08:00
committed by GitHub
parent 6f91c200a9
commit d5f46690c4
16 changed files with 705 additions and 514 deletions

View File

@@ -25,7 +25,7 @@ export default function SaveButton({
setPushing(true)
const event = createRelayListDraftEvent(mailboxRelays)
const relayListEvent = await publish(event)
updateRelayListEvent(relayListEvent)
await updateRelayListEvent(relayListEvent)
toast({
title: 'Save Successful',
description: 'Successfully saved mailbox relays'

View File

@@ -10,7 +10,7 @@ import { useNostr } from '@/providers/NostrProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import client from '@/services/client.service'
import relayInfoService from '@/services/relay-info.service'
import storage from '@/services/storage.service'
import storage from '@/services/local-storage.service'
import { TNoteListMode } from '@/types'
import dayjs from 'dayjs'
import { Event, Filter, kinds } from 'nostr-tools'

View File

@@ -7,11 +7,11 @@ export const StorageKey = {
CURRENT_ACCOUNT: 'currentAccount',
ADD_CLIENT_TAG: 'addClientTag',
NOTE_LIST_MODE: 'noteListMode',
ACCOUNT_RELAY_LIST_EVENT_MAP: 'accountRelayListEventMap',
ACCOUNT_FOLLOW_LIST_EVENT_MAP: 'accountFollowListEventMap',
ACCOUNT_MUTE_LIST_EVENT_MAP: 'accountMuteListEventMap',
ACCOUNT_MUTE_DECRYPTED_TAGS_MAP: 'accountMuteDecryptedTagsMap',
ACCOUNT_PROFILE_EVENT_MAP: 'accountProfileEventMap'
ACCOUNT_RELAY_LIST_EVENT_MAP: 'accountRelayListEventMap', // deprecated
ACCOUNT_FOLLOW_LIST_EVENT_MAP: 'accountFollowListEventMap', // deprecated
ACCOUNT_MUTE_LIST_EVENT_MAP: 'accountMuteListEventMap', // deprecated
ACCOUNT_MUTE_DECRYPTED_TAGS_MAP: 'accountMuteDecryptedTagsMap', // deprecated
ACCOUNT_PROFILE_EVENT_MAP: 'accountProfileEventMap' // deprecated
}
export const BIG_RELAY_URLS = [

View File

@@ -2,8 +2,9 @@ import { getProfileFromProfileEvent } from '@/lib/event'
import { userIdToPubkey } from '@/lib/pubkey'
import { useNostr } from '@/providers/NostrProvider'
import client from '@/services/client.service'
import storage from '@/services/storage.service'
import indexedDb from '@/services/indexed-db.service'
import { TProfile } from '@/types'
import { kinds } from 'nostr-tools'
import { useEffect, useState } from 'react'
export function useFetchProfile(id?: string) {
@@ -27,7 +28,7 @@ export function useFetchProfile(id?: string) {
const pubkey = userIdToPubkey(id)
setPubkey(pubkey)
const storedProfileEvent = storage.getAccountProfileEvent(pubkey)
const storedProfileEvent = await indexedDb.getReplaceableEvent(pubkey, kinds.Metadata)
if (storedProfileEvent) {
const profile = getProfileFromProfileEvent(storedProfileEvent)
setProfile(profile)

View File

@@ -74,7 +74,7 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
profileEvent?.tags
)
const newProfileEvent = await publish(profileDraftEvent)
updateProfileEvent(newProfileEvent)
await updateProfileEvent(newProfileEvent)
setSaving(false)
pop()
}

View File

@@ -2,7 +2,7 @@ import { BIG_RELAY_URLS } from '@/constants'
import { checkAlgoRelay } from '@/lib/relay'
import { isWebsocketUrl, normalizeUrl } from '@/lib/url'
import client from '@/services/client.service'
import storage from '@/services/storage.service'
import storage from '@/services/local-storage.service'
import relayInfoService from '@/services/relay-info.service'
import { TFeedType } from '@/types'
import { Filter } from 'nostr-tools'

View File

@@ -1,8 +1,8 @@
import { createFollowListDraftEvent } from '@/lib/draft-event'
import { extractPubkeysFromEventTags } from '@/lib/tag'
import client from '@/services/client.service'
import storage from '@/services/storage.service'
import { Event } from 'nostr-tools'
import indexedDb from '@/services/indexed-db.service'
import { Event, kinds } from 'nostr-tools'
import { createContext, useContext, useEffect, useMemo, useState } from 'react'
import { useNostr } from './NostrProvider'
@@ -40,13 +40,16 @@ export function FollowListProvider({ children }: { children: React.ReactNode })
const init = async () => {
setIsFetching(true)
setFollowListEvent(undefined)
const storedFollowListEvent = storage.getAccountFollowListEvent(accountPubkey)
const storedFollowListEvent = await indexedDb.getReplaceableEvent(
accountPubkey,
kinds.Contacts
)
if (storedFollowListEvent) {
setFollowListEvent(storedFollowListEvent)
}
const event = await client.fetchFollowListEvent(accountPubkey)
const event = await client.fetchFollowListEvent(accountPubkey, true)
if (event) {
updateFollowListEvent(event)
await updateFollowListEvent(event)
}
setIsFetching(false)
}
@@ -54,8 +57,8 @@ export function FollowListProvider({ children }: { children: React.ReactNode })
init()
}, [accountPubkey])
const updateFollowListEvent = (event: Event) => {
const isNew = storage.setAccountFollowListEvent(event)
const updateFollowListEvent = async (event: Event) => {
const isNew = await indexedDb.putReplaceableEvent(event)
if (!isNew) return
setFollowListEvent(event)
}
@@ -69,7 +72,7 @@ export function FollowListProvider({ children }: { children: React.ReactNode })
)
const newFollowListEvent = await publish(newFollowListDraftEvent)
client.updateFollowListCache(accountPubkey, newFollowListEvent)
updateFollowListEvent(newFollowListEvent)
await updateFollowListEvent(newFollowListEvent)
}
const unfollow = async (pubkey: string) => {
@@ -81,11 +84,11 @@ export function FollowListProvider({ children }: { children: React.ReactNode })
)
const newFollowListEvent = await publish(newFollowListDraftEvent)
client.updateFollowListCache(accountPubkey, newFollowListEvent)
updateFollowListEvent(newFollowListEvent)
await updateFollowListEvent(newFollowListEvent)
}
const getFollowings = async (pubkey: string) => {
const followListEvent = storage.getAccountFollowListEvent(pubkey)
const followListEvent = await indexedDb.getReplaceableEvent(pubkey, kinds.Contacts)
if (followListEvent) {
return extractPubkeysFromEventTags(followListEvent.tags)
}

View File

@@ -2,7 +2,7 @@ import { createMuteListDraftEvent } from '@/lib/draft-event'
import { getLatestEvent } from '@/lib/event'
import { extractPubkeysFromEventTags, isSameTag } from '@/lib/tag'
import client from '@/services/client.service'
import storage from '@/services/storage.service'
import indexedDb from '@/services/indexed-db.service'
import { Event, kinds } from 'nostr-tools'
import { createContext, useContext, useEffect, useMemo, useState } from 'react'
import { z } from 'zod'
@@ -35,7 +35,7 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
const init = async () => {
setMuteListEvent(undefined)
const storedMuteListEvent = storage.getAccountMuteListEvent(accountPubkey)
const storedMuteListEvent = await indexedDb.getReplaceableEvent(accountPubkey, kinds.Mutelist)
if (storedMuteListEvent) {
setMuteListEvent(storedMuteListEvent)
const tags = await extractMuteTags(storedMuteListEvent)
@@ -47,6 +47,7 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
})
const muteEvent = getLatestEvent(events) as Event | undefined
if (muteEvent) {
await indexedDb.putReplaceableEvent(muteEvent)
setMuteListEvent(muteEvent)
const tags = await extractMuteTags(muteEvent)
setTags(tags)
@@ -59,7 +60,7 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
const extractMuteTags = async (muteListEvent: Event) => {
const tags = [...muteListEvent.tags]
if (muteListEvent.content) {
const storedDecryptedTags = storage.getAccountMuteDecryptedTags(muteListEvent)
const storedDecryptedTags = await indexedDb.getMuteDecryptedTags(muteListEvent.id)
if (storedDecryptedTags) {
tags.push(...storedDecryptedTags)
@@ -67,7 +68,7 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
try {
const plainText = await nip04Decrypt(muteListEvent.pubkey, muteListEvent.content)
const contentTags = z.array(z.array(z.string())).parse(JSON.parse(plainText))
storage.setAccountMuteDecryptedTags(muteListEvent, contentTags)
await indexedDb.putMuteDecryptedTags(muteListEvent.id, contentTags)
tags.push(...contentTags.filter((tag) => tags.every((t) => !isSameTag(t, tag))))
} catch (error) {
console.error('Failed to decrypt mute list content', error)
@@ -77,10 +78,10 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
return tags
}
const update = (event: Event, tags: string[][]) => {
const isNew = storage.setAccountMuteListEvent(event)
const update = async (event: Event, tags: string[][]) => {
const isNew = await indexedDb.putReplaceableEvent(event)
if (!isNew) return
storage.setAccountMuteDecryptedTags(event, tags)
await indexedDb.putMuteDecryptedTags(event.id, tags)
setMuteListEvent(event)
setTags(tags)
}
@@ -92,7 +93,7 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
const cipherText = await nip04Encrypt(accountPubkey, JSON.stringify(newTags))
const newMuteListDraftEvent = createMuteListDraftEvent(muteListEvent?.tags ?? [], cipherText)
const newMuteListEvent = await publish(newMuteListDraftEvent)
update(newMuteListEvent, newTags)
await update(newMuteListEvent, newTags)
}
const unmutePubkey = async (pubkey: string) => {
@@ -105,7 +106,7 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
cipherText
)
const newMuteListEvent = await publish(newMuteListDraftEvent)
update(newMuteListEvent, newTags)
await update(newMuteListEvent, newTags)
}
return (

View File

@@ -4,7 +4,8 @@ import { useToast } from '@/hooks'
import { getProfileFromProfileEvent, getRelayListFromRelayListEvent } from '@/lib/event'
import { formatPubkey } from '@/lib/pubkey'
import client from '@/services/client.service'
import storage from '@/services/storage.service'
import storage from '@/services/local-storage.service'
import indexedDb from '@/services/indexed-db.service'
import { ISigner, TAccount, TAccountPointer, TDraftEvent, TProfile, TRelayList } from '@/types'
import dayjs from 'dayjs'
import { Event, kinds, VerifiedEvent } from 'nostr-tools'
@@ -45,8 +46,8 @@ type TNostrContext = {
startLogin: () => void
checkLogin: <T>(cb?: () => T) => Promise<T | void>
getRelayList: (pubkey: string) => Promise<TRelayList>
updateRelayListEvent: (relayListEvent: Event) => void
updateProfileEvent: (profileEvent: Event) => void
updateRelayListEvent: (relayListEvent: Event) => Promise<void>
updateProfileEvent: (profileEvent: Event) => Promise<void>
}
const NostrContext = createContext<TNostrContext | undefined>(undefined)
@@ -99,64 +100,79 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
}, [])
useEffect(() => {
setRelayList(null)
setProfile(null)
setProfileEvent(null)
setNsec(null)
if (!account) {
return
}
const storedNsec = storage.getAccountNsec(account.pubkey)
if (storedNsec) {
setNsec(storedNsec)
} else {
const init = async () => {
setRelayList(null)
setProfile(null)
setProfileEvent(null)
setNsec(null)
}
const storedNcryptsec = storage.getAccountNcryptsec(account.pubkey)
if (storedNcryptsec) {
setNcryptsec(storedNcryptsec)
} else {
setNcryptsec(null)
}
const storedRelayListEvent = storage.getAccountRelayListEvent(account.pubkey)
if (storedRelayListEvent) {
setRelayList(
storedRelayListEvent ? getRelayListFromRelayListEvent(storedRelayListEvent) : null
)
}
const storedProfileEvent = storage.getAccountProfileEvent(account.pubkey)
if (storedProfileEvent) {
setProfileEvent(storedProfileEvent)
setProfile(getProfileFromProfileEvent(storedProfileEvent))
}
client.fetchRelayListEvent(account.pubkey).then(async (relayListEvent) => {
if (!relayListEvent) {
if (storedRelayListEvent) return
setRelayList({ write: BIG_RELAY_URLS, read: BIG_RELAY_URLS, originalRelays: [] })
if (!account) {
return
}
const isNew = storage.setAccountRelayListEvent(relayListEvent)
if (!isNew) return
setRelayList(getRelayListFromRelayListEvent(relayListEvent))
})
client.fetchProfileEvent(account.pubkey).then(async (profileEvent) => {
if (!profileEvent) {
if (storedProfileEvent) return
setProfile({
pubkey: account.pubkey,
username: formatPubkey(account.pubkey)
})
return
const controller = new AbortController()
const storedNsec = storage.getAccountNsec(account.pubkey)
if (storedNsec) {
setNsec(storedNsec)
} else {
setNsec(null)
}
const isNew = storage.setAccountProfileEvent(profileEvent)
if (!isNew) return
setProfileEvent(profileEvent)
setProfile(getProfileFromProfileEvent(profileEvent))
})
client.initUserIndexFromFollowings(account.pubkey)
const storedNcryptsec = storage.getAccountNcryptsec(account.pubkey)
if (storedNcryptsec) {
setNcryptsec(storedNcryptsec)
} else {
setNcryptsec(null)
}
const [storedRelayListEvent, storedProfileEvent] = await Promise.all([
indexedDb.getReplaceableEvent(account.pubkey, kinds.RelayList),
indexedDb.getReplaceableEvent(account.pubkey, kinds.Metadata)
])
if (storedRelayListEvent) {
setRelayList(
storedRelayListEvent ? getRelayListFromRelayListEvent(storedRelayListEvent) : null
)
}
if (storedProfileEvent) {
setProfileEvent(storedProfileEvent)
setProfile(getProfileFromProfileEvent(storedProfileEvent))
}
client.fetchRelayListEvent(account.pubkey).then(async (relayListEvent) => {
if (!relayListEvent) {
if (storedRelayListEvent) return
setRelayList({ write: BIG_RELAY_URLS, read: BIG_RELAY_URLS, originalRelays: [] })
return
}
const event = await indexedDb.getReplaceableEvent(account.pubkey, kinds.RelayList)
if (event) {
setRelayList(getRelayListFromRelayListEvent(event))
}
})
client.fetchProfileEvent(account.pubkey).then(async (profileEvent) => {
if (!profileEvent) {
if (storedProfileEvent) return
setProfile({
pubkey: account.pubkey,
username: formatPubkey(account.pubkey)
})
return
}
const event = await indexedDb.getReplaceableEvent(account.pubkey, kinds.Metadata)
if (event) {
setProfileEvent(event)
setProfile(getProfileFromProfileEvent(event))
}
})
client.initUserIndexFromFollowings(account.pubkey, controller.signal)
return controller
}
const promise = init()
return () => {
promise.then((controller) => {
controller?.abort()
})
}
}, [account])
useEffect(() => {
@@ -379,21 +395,21 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
}
const getRelayList = async (pubkey: string) => {
const storedRelayListEvent = storage.getAccountRelayListEvent(pubkey)
const storedRelayListEvent = await indexedDb.getReplaceableEvent(pubkey, kinds.RelayList)
if (storedRelayListEvent) {
return getRelayListFromRelayListEvent(storedRelayListEvent)
}
return await client.fetchRelayList(pubkey)
}
const updateRelayListEvent = (relayListEvent: Event) => {
const isNew = storage.setAccountRelayListEvent(relayListEvent)
const updateRelayListEvent = async (relayListEvent: Event) => {
const isNew = await indexedDb.putReplaceableEvent(relayListEvent)
if (!isNew) return
setRelayList(getRelayListFromRelayListEvent(relayListEvent))
}
const updateProfileEvent = (profileEvent: Event) => {
const isNew = storage.setAccountProfileEvent(profileEvent)
const updateProfileEvent = async (profileEvent: Event) => {
const isNew = await indexedDb.putReplaceableEvent(profileEvent)
if (!isNew) return
setProfileEvent(profileEvent)
setProfile(getProfileFromProfileEvent(profileEvent))

View File

@@ -1,6 +1,6 @@
import { randomString } from '@/lib/random'
import { isWebsocketUrl, normalizeUrl } from '@/lib/url'
import storage from '@/services/storage.service'
import storage from '@/services/local-storage.service'
import { TRelaySet } from '@/types'
import { createContext, useContext, useEffect, useState } from 'react'

View File

@@ -1,4 +1,4 @@
import storage from '@/services/storage.service'
import storage from '@/services/local-storage.service'
import { TTheme, TThemeSetting } from '@/types'
import { createContext, useContext, useEffect, useState } from 'react'

View File

@@ -18,6 +18,7 @@ import {
VerifiedEvent
} from 'nostr-tools'
import { AbstractRelay } from 'nostr-tools/abstract-relay'
import indexedDb from './indexed-db.service'
type TTimelineRef = [string, number]
@@ -46,24 +47,29 @@ class ClientService extends EventTarget {
this.eventBatchLoadFn.bind(this),
{ cache: false }
)
private profileEventCache = new LRUCache<string, Promise<NEvent | undefined>>({ max: 10000 })
private profileEventDataloader = new DataLoader<string, NEvent | undefined>(
(ids) => Promise.all(ids.map((id) => this._fetchProfileEvent(id))),
{ cacheMap: this.profileEventCache, maxBatchSize: 50 }
{
cache: false,
maxBatchSize: 50
}
)
private fetchProfileEventFromDefaultRelaysDataloader = new DataLoader<string, NEvent | undefined>(
this.profileEventBatchLoadFn.bind(this),
{ cache: false, maxBatchSize: 50 }
{
cacheMap: new LRUCache<string, Promise<NEvent | undefined>>({ max: 1000 }),
maxBatchSize: 50
}
)
private relayListEventDataLoader = new DataLoader<string, NEvent | undefined>(
this.relayListEventBatchLoadFn.bind(this),
{
cacheMap: new LRUCache<string, Promise<NEvent | undefined>>({ max: 10000 }),
cacheMap: new LRUCache<string, Promise<NEvent | undefined>>({ max: 1000 }),
maxBatchSize: 50
}
)
private followListCache = new LRUCache<string, Promise<NEvent | undefined>>({
max: 10000,
max: 2000,
fetchMethod: this._fetchFollowListEvent.bind(this)
})
@@ -448,12 +454,6 @@ class ClientService extends EventTarget {
}
async fetchProfileEvent(id: string): Promise<NEvent | undefined> {
const pubkey = userIdToPubkey(id)
const cache = await this.profileEventCache.get(pubkey)
if (cache) {
return cache
}
return await this.profileEventDataloader.load(id)
}
@@ -504,8 +504,12 @@ class ClientService extends EventTarget {
return getRelayListFromRelayListEvent(event)
}
async fetchFollowListEvent(pubkey: string) {
return this.followListCache.fetch(pubkey)
async fetchFollowListEvent(pubkey: string, storeToIndexedDb = false) {
const event = await this.followListCache.fetch(pubkey)
if (storeToIndexedDb && event) {
await indexedDb.putReplaceableEvent(event)
}
return event
}
async fetchFollowings(pubkey: string) {
@@ -576,9 +580,11 @@ class ClientService extends EventTarget {
)
}
async initUserIndexFromFollowings(pubkey: string) {
async initUserIndexFromFollowings(pubkey: string, signal: AbortSignal) {
const followings = await this.fetchFollowings(pubkey)
for (let i = 0; i * 50 < followings.length; i++) {
if (signal.aborted) return
await this.profileEventDataloader.loadMany(followings.slice(i * 50, (i + 1) * 50))
await new Promise((resolve) => setTimeout(resolve, 30000))
}
@@ -686,10 +692,16 @@ class ClientService extends EventTarget {
if (!pubkey) {
throw new Error('Invalid id')
}
const localProfile = await indexedDb.getReplaceableEvent(pubkey, kinds.Metadata)
if (localProfile) {
this.addUsernameToIndex(localProfile)
return localProfile
}
const profileFromDefaultRelays =
await this.fetchProfileEventFromDefaultRelaysDataloader.load(pubkey)
if (profileFromDefaultRelays) {
this.addUsernameToIndex(profileFromDefaultRelays)
await indexedDb.putReplaceableEvent(profileFromDefaultRelays)
return profileFromDefaultRelays
}
@@ -707,7 +719,10 @@ class ClientService extends EventTarget {
}
if (profileEvent) {
await this.addUsernameToIndex(profileEvent)
await Promise.allSettled([
this.addUsernameToIndex(profileEvent),
indexedDb.putReplaceableEvent(profileEvent)
])
}
return profileEvent
@@ -778,28 +793,50 @@ class ClientService extends EventTarget {
eventsMap.set(pubkey, event)
}
}
return pubkeys.map((pubkey) => {
const profileEvents = pubkeys.map((pubkey) => {
return eventsMap.get(pubkey)
})
await Promise.allSettled(
profileEvents.map(
(profileEvent) => profileEvent && indexedDb.putReplaceableEvent(profileEvent)
)
)
return profileEvents
}
private async relayListEventBatchLoadFn(pubkeys: readonly string[]) {
const events = await this.query(this.defaultRelayUrls, {
authors: pubkeys as string[],
kinds: [kinds.RelayList],
limit: pubkeys.length
})
const eventsMap = new Map<string, NEvent>()
for (const event of events) {
const pubkey = event.pubkey
const existing = eventsMap.get(pubkey)
if (!existing || existing.created_at < event.created_at) {
eventsMap.set(pubkey, event)
const relayEvents = await Promise.all(
pubkeys.map((pubkey) => indexedDb.getReplaceableEvent(pubkey, kinds.RelayList))
)
const nonExistingPubkeys = pubkeys.filter((_, i) => !relayEvents[i])
if (nonExistingPubkeys.length) {
const events = await this.query(this.defaultRelayUrls, {
authors: pubkeys as string[],
kinds: [kinds.RelayList],
limit: pubkeys.length
})
const eventsMap = new Map<string, NEvent>()
for (const event of events) {
const pubkey = event.pubkey
const existing = eventsMap.get(pubkey)
if (!existing || existing.created_at < event.created_at) {
eventsMap.set(pubkey, event)
}
}
await Promise.allSettled(
Array.from(eventsMap.values()).map((evt) => indexedDb.putReplaceableEvent(evt))
)
nonExistingPubkeys.forEach((pubkey) => {
const event = eventsMap.get(pubkey)
if (event) {
const index = pubkeys.indexOf(pubkey)
relayEvents[index] = event
}
})
}
return pubkeys.map((pubkey) => eventsMap.get(pubkey))
return relayEvents
}
private async _fetchFollowListEvent(pubkey: string) {

View File

@@ -0,0 +1,271 @@
import { tagNameEquals } from '@/lib/tag'
import { Event, kinds } from 'nostr-tools'
type TValue<T = any> = {
key: string
value: T
addedAt: number
}
const StoreNames = {
PROFILE_EVENTS: 'profileEvents',
RELAY_LIST_EVENTS: 'relayListEvents',
FOLLOW_LIST_EVENTS: 'followListEvents',
MUTE_LIST_EVENTS: 'muteListEvents',
MUTE_DECRYPTED_TAGS: 'muteDecryptedTags',
RELAY_INFO_EVENTS: 'relayInfoEvents'
}
class IndexedDbService {
static instance: IndexedDbService
static getInstance(): IndexedDbService {
if (!IndexedDbService.instance) {
IndexedDbService.instance = new IndexedDbService()
IndexedDbService.instance.init()
}
return IndexedDbService.instance
}
private db: IDBDatabase | null = null
private initPromise: Promise<void> | null = null
init(): Promise<void> {
if (!this.initPromise) {
this.initPromise = new Promise((resolve, reject) => {
const request = window.indexedDB.open('jumble', 2)
request.onerror = (event) => {
reject(event)
}
request.onsuccess = () => {
this.db = request.result
resolve()
}
request.onupgradeneeded = () => {
this.db = request.result
if (!this.db.objectStoreNames.contains(StoreNames.PROFILE_EVENTS)) {
this.db.createObjectStore(StoreNames.PROFILE_EVENTS, { keyPath: 'key' })
}
if (!this.db.objectStoreNames.contains(StoreNames.RELAY_LIST_EVENTS)) {
this.db.createObjectStore(StoreNames.RELAY_LIST_EVENTS, { keyPath: 'key' })
}
if (!this.db.objectStoreNames.contains(StoreNames.FOLLOW_LIST_EVENTS)) {
this.db.createObjectStore(StoreNames.FOLLOW_LIST_EVENTS, { keyPath: 'key' })
}
if (!this.db.objectStoreNames.contains(StoreNames.MUTE_LIST_EVENTS)) {
this.db.createObjectStore(StoreNames.MUTE_LIST_EVENTS, { keyPath: 'key' })
}
if (!this.db.objectStoreNames.contains(StoreNames.MUTE_DECRYPTED_TAGS)) {
this.db.createObjectStore(StoreNames.MUTE_DECRYPTED_TAGS, { keyPath: 'key' })
}
if (!this.db.objectStoreNames.contains(StoreNames.RELAY_INFO_EVENTS)) {
this.db.createObjectStore(StoreNames.RELAY_INFO_EVENTS, { keyPath: 'key' })
}
}
})
setTimeout(() => this.cleanUp(), 1000 * 60) // 1 minute
}
return this.initPromise
}
async putReplaceableEvent(event: Event): Promise<boolean> {
const storeName = this.getStoreNameByKind(event.kind)
if (!storeName) {
return Promise.reject('store name not found')
}
await this.initPromise
return new Promise((resolve, reject) => {
if (!this.db) {
return reject('database not initialized')
}
const transaction = this.db.transaction(storeName, 'readwrite')
const store = transaction.objectStore(storeName)
const getRequest = store.get(event.pubkey)
getRequest.onsuccess = () => {
const oldValue = getRequest.result as TValue<Event> | undefined
if (oldValue && oldValue.value.created_at >= event.created_at) {
return resolve(false)
}
const putRequest = store.put(this.formatValue(event.pubkey, event))
putRequest.onsuccess = () => {
resolve(true)
}
putRequest.onerror = (event) => {
reject(event)
}
}
})
}
async getReplaceableEvent(pubkey: string, kind: number): Promise<Event | undefined> {
const storeName = this.getStoreNameByKind(kind)
if (!storeName) {
return Promise.reject('store name not found')
}
await this.initPromise
return new Promise((resolve, reject) => {
if (!this.db) {
return reject('database not initialized')
}
const transaction = this.db.transaction(storeName, 'readonly')
const store = transaction.objectStore(storeName)
const request = store.get(pubkey)
request.onsuccess = () => {
resolve((request.result as TValue<Event>)?.value)
}
request.onerror = (event) => {
reject(event)
}
})
}
async getMuteDecryptedTags(id: string): Promise<string[][]> {
await this.initPromise
return new Promise((resolve, reject) => {
if (!this.db) {
return reject('database not initialized')
}
const transaction = this.db.transaction(StoreNames.MUTE_DECRYPTED_TAGS, 'readonly')
const store = transaction.objectStore(StoreNames.MUTE_DECRYPTED_TAGS)
const request = store.get(id)
request.onsuccess = () => {
resolve((request.result as TValue<string[][]>)?.value)
}
request.onerror = (event) => {
reject(event)
}
})
}
async putMuteDecryptedTags(id: string, tags: string[][]): Promise<void> {
await this.initPromise
return new Promise((resolve, reject) => {
if (!this.db) {
return reject('database not initialized')
}
const transaction = this.db.transaction(StoreNames.MUTE_DECRYPTED_TAGS, 'readwrite')
const store = transaction.objectStore(StoreNames.MUTE_DECRYPTED_TAGS)
const putRequest = store.put(this.formatValue(id, tags))
putRequest.onsuccess = () => {
resolve()
}
putRequest.onerror = (event) => {
reject(event)
}
})
}
async getAllRelayInfoEvents(): Promise<Event[]> {
await this.initPromise
return new Promise((resolve, reject) => {
if (!this.db) {
return reject('database not initialized')
}
const transaction = this.db.transaction(StoreNames.RELAY_INFO_EVENTS, 'readonly')
const store = transaction.objectStore(StoreNames.RELAY_INFO_EVENTS)
const request = store.getAll()
request.onsuccess = () => {
resolve((request.result as TValue<Event>[])?.map((item) => item.value))
}
request.onerror = (event) => {
reject(event)
}
})
}
async putRelayInfoEvent(event: Event): Promise<void> {
await this.initPromise
return new Promise((resolve, reject) => {
if (!this.db) {
return reject('database not initialized')
}
const dValue = event.tags.find(tagNameEquals('d'))?.[1]
if (!dValue) {
return resolve()
}
const transaction = this.db.transaction(StoreNames.RELAY_INFO_EVENTS, 'readwrite')
const store = transaction.objectStore(StoreNames.RELAY_INFO_EVENTS)
const putRequest = store.put(this.formatValue(dValue, event))
putRequest.onsuccess = () => {
resolve()
}
putRequest.onerror = (event) => {
reject(event)
}
})
}
private getStoreNameByKind(kind: number): string | undefined {
switch (kind) {
case kinds.Metadata:
return StoreNames.PROFILE_EVENTS
case kinds.RelayList:
return StoreNames.RELAY_LIST_EVENTS
case kinds.Contacts:
return StoreNames.FOLLOW_LIST_EVENTS
case kinds.Mutelist:
return StoreNames.MUTE_LIST_EVENTS
default:
return undefined
}
}
private formatValue<T>(key: string, value: T): TValue<T> {
return {
key,
value,
addedAt: Date.now()
}
}
private async cleanUp() {
await this.initPromise
if (!this.db) {
return
}
const expirationTimestamp = Date.now() - 1000 * 60 * 60 * 24 // 1 day
const transaction = this.db!.transaction(Object.values(StoreNames), 'readwrite')
await Promise.allSettled(
Object.values(StoreNames).map((storeName) => {
return new Promise<void>((resolve, reject) => {
const store = transaction.objectStore(storeName)
const request = store.openCursor()
request.onsuccess = (event) => {
const cursor = (event.target as IDBRequest).result
if (cursor) {
const value: TValue = cursor.value
if (value.addedAt < expirationTimestamp) {
cursor.delete()
}
cursor.continue()
} else {
resolve()
}
}
request.onerror = (event) => {
reject(event)
}
})
})
)
}
}
const instance = IndexedDbService.getInstance()
export default instance

View File

@@ -0,0 +1,215 @@
import { StorageKey } from '@/constants'
import { isSameAccount } from '@/lib/account'
import { randomString } from '@/lib/random'
import {
TAccount,
TAccountPointer,
TFeedType,
TNoteListMode,
TRelaySet,
TThemeSetting
} from '@/types'
const DEFAULT_RELAY_SETS: TRelaySet[] = [
{
id: randomString(),
name: 'Global',
relayUrls: ['wss://relay.damus.io/', 'wss://nos.lol/']
},
{
id: randomString(),
name: 'Safer Global',
relayUrls: ['wss://nostr.wine/', 'wss://pyramid.fiatjaf.com/']
},
{
id: randomString(),
name: 'Short Notes',
relayUrls: ['wss://140.f7z.io/']
},
{
id: randomString(),
name: 'News',
relayUrls: ['wss://news.utxo.one/']
},
{
id: randomString(),
name: 'Algo',
relayUrls: ['wss://algo.utxo.one']
}
]
class LocalStorageService {
static instance: LocalStorageService
private relaySets: TRelaySet[] = []
private activeRelaySetId: string | null = null
private feedType: TFeedType = 'relays'
private themeSetting: TThemeSetting = 'system'
private accounts: TAccount[] = []
private currentAccount: TAccount | null = null
private noteListMode: TNoteListMode = 'posts'
constructor() {
if (!LocalStorageService.instance) {
this.init()
LocalStorageService.instance = this
}
return LocalStorageService.instance
}
init() {
this.themeSetting =
(window.localStorage.getItem(StorageKey.THEME_SETTING) as TThemeSetting) ?? 'system'
const accountsStr = window.localStorage.getItem(StorageKey.ACCOUNTS)
this.accounts = accountsStr ? JSON.parse(accountsStr) : []
const currentAccountStr = window.localStorage.getItem(StorageKey.CURRENT_ACCOUNT)
this.currentAccount = currentAccountStr ? JSON.parse(currentAccountStr) : null
const feedType = window.localStorage.getItem(StorageKey.FEED_TYPE)
if (feedType && ['following', 'relays'].includes(feedType)) {
this.feedType = feedType as 'following' | 'relays'
} else {
this.feedType = 'relays'
}
const noteListModeStr = window.localStorage.getItem(StorageKey.NOTE_LIST_MODE)
this.noteListMode =
noteListModeStr && ['posts', 'postsAndReplies', 'pictures'].includes(noteListModeStr)
? (noteListModeStr as TNoteListMode)
: 'posts'
const relaySetsStr = window.localStorage.getItem(StorageKey.RELAY_SETS)
if (!relaySetsStr) {
let relaySets: TRelaySet[] = []
const legacyRelayGroupsStr = window.localStorage.getItem('relayGroups')
if (legacyRelayGroupsStr) {
const legacyRelayGroups = JSON.parse(legacyRelayGroupsStr)
relaySets = legacyRelayGroups.map((group: any) => {
return {
id: randomString(),
name: group.groupName,
relayUrls: group.relayUrls
}
})
}
if (!relaySets.length) {
relaySets = DEFAULT_RELAY_SETS
}
const activeRelaySetId = relaySets[0].id
window.localStorage.setItem(StorageKey.RELAY_SETS, JSON.stringify(relaySets))
window.localStorage.setItem(StorageKey.ACTIVE_RELAY_SET_ID, activeRelaySetId)
this.relaySets = relaySets
this.activeRelaySetId = activeRelaySetId
} else {
this.relaySets = JSON.parse(relaySetsStr)
this.activeRelaySetId = window.localStorage.getItem(StorageKey.ACTIVE_RELAY_SET_ID) ?? null
}
// Clean up deprecated data
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)
window.localStorage.removeItem(StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP)
window.localStorage.removeItem(StorageKey.ACCOUNT_RELAY_LIST_EVENT_MAP)
window.localStorage.removeItem(StorageKey.ACCOUNT_MUTE_LIST_EVENT_MAP)
window.localStorage.removeItem(StorageKey.ACCOUNT_MUTE_DECRYPTED_TAGS_MAP)
}
getRelaySets() {
return this.relaySets
}
setRelaySets(relaySets: TRelaySet[]) {
this.relaySets = relaySets
window.localStorage.setItem(StorageKey.RELAY_SETS, JSON.stringify(this.relaySets))
}
getActiveRelaySetId() {
return this.activeRelaySetId
}
setActiveRelaySetId(id: string | null) {
this.activeRelaySetId = id
if (id) {
window.localStorage.setItem(StorageKey.ACTIVE_RELAY_SET_ID, id)
} else {
window.localStorage.removeItem(StorageKey.ACTIVE_RELAY_SET_ID)
}
}
getFeedType() {
return this.feedType
}
setFeedType(feedType: TFeedType) {
this.feedType = feedType
window.localStorage.setItem(StorageKey.FEED_TYPE, this.feedType)
}
getThemeSetting() {
return this.themeSetting
}
setThemeSetting(themeSetting: TThemeSetting) {
window.localStorage.setItem(StorageKey.THEME_SETTING, themeSetting)
this.themeSetting = themeSetting
}
getNoteListMode() {
return this.noteListMode
}
setNoteListMode(mode: TNoteListMode) {
window.localStorage.setItem(StorageKey.NOTE_LIST_MODE, mode)
this.noteListMode = mode
}
getAccounts() {
return this.accounts
}
findAccount(account: TAccountPointer) {
return this.accounts.find((act) => isSameAccount(act, account))
}
getCurrentAccount() {
return this.currentAccount
}
getAccountNsec(pubkey: string) {
const account = this.accounts.find((act) => act.pubkey === pubkey && act.signerType === 'nsec')
return account?.nsec
}
getAccountNcryptsec(pubkey: string) {
const account = this.accounts.find(
(act) => act.pubkey === pubkey && act.signerType === 'ncryptsec'
)
return account?.ncryptsec
}
addAccount(account: TAccount) {
if (this.accounts.find((act) => isSameAccount(act, account))) {
return
}
this.accounts.push(account)
window.localStorage.setItem(StorageKey.ACCOUNTS, JSON.stringify(this.accounts))
return account
}
removeAccount(account: TAccount) {
this.accounts = this.accounts.filter((act) => !isSameAccount(act, account))
window.localStorage.setItem(StorageKey.ACCOUNTS, JSON.stringify(this.accounts))
}
switchAccount(account: TAccount | null) {
if (isSameAccount(this.currentAccount, account)) {
return
}
const act = this.accounts.find((act) => isSameAccount(act, account))
if (!act) {
return
}
this.currentAccount = act
window.localStorage.setItem(StorageKey.CURRENT_ACCOUNT, JSON.stringify(act))
}
}
const instance = new LocalStorageService()
export default instance

View File

@@ -6,6 +6,7 @@ import DataLoader from 'dataloader'
import FlexSearch from 'flexsearch'
import { Event } from 'nostr-tools'
import client from './client.service'
import indexedDb from './indexed-db.service'
class RelayInfoService {
static instance: RelayInfoService
@@ -52,7 +53,12 @@ class RelayInfoService {
}
if (!query) {
return Array.from(this.relayInfoMap.values())
const arr = Array.from(this.relayInfoMap.values())
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[arr[i], arr[j]] = [arr[j], arr[i]]
}
return arr
}
const result = await this.relayInfoIndex.searchAsync(query)
@@ -128,28 +134,39 @@ class RelayInfoService {
}
private async loadRelayInfos() {
let until: number = Math.round(Date.now() / 1000)
const since = until - 60 * 60 * 48
while (until) {
const relayInfoEvents = await client.fetchEvents(MONITOR_RELAYS, {
authors: [MONITOR],
kinds: [30166],
since,
until,
limit: 1000
})
const events = relayInfoEvents.sort((a, b) => b.created_at - a.created_at)
if (events.length === 0) {
break
}
until = events[events.length - 1].created_at - 1
const relayInfos = formatRelayInfoEvents(events)
for (const relayInfo of relayInfos) {
await this.addRelayInfo(relayInfo)
}
}
const localRelayInfos = await indexedDb.getAllRelayInfoEvents()
const relayInfos = formatRelayInfoEvents(localRelayInfos)
relayInfos.forEach((relayInfo) => this.addRelayInfo(relayInfo))
this.relayUrlsForRandom = Array.from(this.relayInfoMap.keys())
const loadFromInternet = async () => {
let until: number = Math.round(Date.now() / 1000)
const since = until - 60 * 60 * 48
while (until) {
const relayInfoEvents = await client.fetchEvents(MONITOR_RELAYS, {
authors: [MONITOR],
kinds: [30166],
since,
until,
limit: 1000
})
const events = relayInfoEvents.sort((a, b) => b.created_at - a.created_at)
if (events.length === 0) {
break
}
await Promise.allSettled(events.map((event) => indexedDb.putRelayInfoEvent(event)))
until = events[events.length - 1].created_at - 1
const relayInfos = formatRelayInfoEvents(events)
relayInfos.forEach((relayInfo) => this.addRelayInfo(relayInfo))
}
this.relayUrlsForRandom = Array.from(this.relayInfoMap.keys())
}
if (localRelayInfos.length === 0) {
await loadFromInternet()
} else {
loadFromInternet()
}
}
private async addRelayInfo(relayInfo: TNip66RelayInfo) {

View File

@@ -1,370 +0,0 @@
import { StorageKey } from '@/constants'
import { isSameAccount } from '@/lib/account'
import { randomString } from '@/lib/random'
import {
TAccount,
TAccountPointer,
TFeedType,
TNoteListMode,
TRelaySet,
TThemeSetting
} from '@/types'
import { Event } from 'nostr-tools'
const DEFAULT_RELAY_SETS: TRelaySet[] = [
{
id: randomString(),
name: 'Global',
relayUrls: ['wss://relay.damus.io/', 'wss://nos.lol/']
},
{
id: randomString(),
name: 'Safer Global',
relayUrls: ['wss://nostr.wine/', 'wss://pyramid.fiatjaf.com/']
},
{
id: randomString(),
name: 'Short Notes',
relayUrls: ['wss://140.f7z.io/']
},
{
id: randomString(),
name: 'News',
relayUrls: ['wss://news.utxo.one/']
},
{
id: randomString(),
name: 'Algo',
relayUrls: ['wss://algo.utxo.one']
}
]
class StorageService {
static instance: StorageService
private relaySets: TRelaySet[] = []
private activeRelaySetId: string | null = null
private feedType: TFeedType = 'relays'
private themeSetting: TThemeSetting = 'system'
private accounts: TAccount[] = []
private currentAccount: TAccount | null = null
private noteListMode: TNoteListMode = 'posts'
private accountRelayListEventMap: Record<string, Event | undefined> = {} // pubkey -> relayListEvent
private accountFollowListEventMap: Record<string, Event | undefined> = {} // pubkey -> followListEvent
private accountMuteListEventMap: Record<string, Event | undefined> = {} // pubkey -> muteListEvent
private accountMuteDecryptedTagsMap: Record<
string,
{ id: string; tags: string[][] } | undefined
> = {} // pubkey -> { id, tags }
private accountProfileEventMap: Record<string, Event | undefined> = {} // pubkey -> profileEvent
constructor() {
if (!StorageService.instance) {
this.init()
StorageService.instance = this
}
return StorageService.instance
}
init() {
this.themeSetting =
(window.localStorage.getItem(StorageKey.THEME_SETTING) as TThemeSetting) ?? 'system'
const accountsStr = window.localStorage.getItem(StorageKey.ACCOUNTS)
this.accounts = accountsStr ? JSON.parse(accountsStr) : []
const currentAccountStr = window.localStorage.getItem(StorageKey.CURRENT_ACCOUNT)
this.currentAccount = currentAccountStr ? JSON.parse(currentAccountStr) : null
const feedType = window.localStorage.getItem(StorageKey.FEED_TYPE)
if (feedType && ['following', 'relays'].includes(feedType)) {
this.feedType = feedType as 'following' | 'relays'
} else {
this.feedType = 'relays'
}
const noteListModeStr = window.localStorage.getItem(StorageKey.NOTE_LIST_MODE)
this.noteListMode =
noteListModeStr && ['posts', 'postsAndReplies', 'pictures'].includes(noteListModeStr)
? (noteListModeStr as TNoteListMode)
: 'posts'
const accountRelayListEventMapStr = window.localStorage.getItem(
StorageKey.ACCOUNT_RELAY_LIST_EVENT_MAP
)
this.accountRelayListEventMap = accountRelayListEventMapStr
? JSON.parse(accountRelayListEventMapStr)
: {}
const accountFollowListEventMapStr = window.localStorage.getItem(
StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP
)
this.accountFollowListEventMap = accountFollowListEventMapStr
? JSON.parse(accountFollowListEventMapStr)
: {}
const accountMuteListEventMapStr = window.localStorage.getItem(
StorageKey.ACCOUNT_MUTE_LIST_EVENT_MAP
)
this.accountMuteListEventMap = accountMuteListEventMapStr
? JSON.parse(accountMuteListEventMapStr)
: {}
const accountMuteDecryptedTagsMapStr = window.localStorage.getItem(
StorageKey.ACCOUNT_MUTE_DECRYPTED_TAGS_MAP
)
this.accountMuteDecryptedTagsMap = accountMuteDecryptedTagsMapStr
? JSON.parse(accountMuteDecryptedTagsMapStr)
: {}
const accountProfileEventMapStr = window.localStorage.getItem(
StorageKey.ACCOUNT_PROFILE_EVENT_MAP
)
this.accountProfileEventMap = accountProfileEventMapStr
? JSON.parse(accountProfileEventMapStr)
: {}
const relaySetsStr = window.localStorage.getItem(StorageKey.RELAY_SETS)
if (!relaySetsStr) {
let relaySets: TRelaySet[] = []
const legacyRelayGroupsStr = window.localStorage.getItem('relayGroups')
if (legacyRelayGroupsStr) {
const legacyRelayGroups = JSON.parse(legacyRelayGroupsStr)
relaySets = legacyRelayGroups.map((group: any) => {
return {
id: randomString(),
name: group.groupName,
relayUrls: group.relayUrls
}
})
}
if (!relaySets.length) {
relaySets = DEFAULT_RELAY_SETS
}
const activeRelaySetId = relaySets[0].id
window.localStorage.setItem(StorageKey.RELAY_SETS, JSON.stringify(relaySets))
window.localStorage.setItem(StorageKey.ACTIVE_RELAY_SET_ID, activeRelaySetId)
this.relaySets = relaySets
this.activeRelaySetId = activeRelaySetId
} else {
this.relaySets = JSON.parse(relaySetsStr)
this.activeRelaySetId = window.localStorage.getItem(StorageKey.ACTIVE_RELAY_SET_ID) ?? null
}
}
getRelaySets() {
return this.relaySets
}
setRelaySets(relaySets: TRelaySet[]) {
this.relaySets = relaySets
window.localStorage.setItem(StorageKey.RELAY_SETS, JSON.stringify(this.relaySets))
}
getActiveRelaySetId() {
return this.activeRelaySetId
}
setActiveRelaySetId(id: string | null) {
this.activeRelaySetId = id
if (id) {
window.localStorage.setItem(StorageKey.ACTIVE_RELAY_SET_ID, id)
} else {
window.localStorage.removeItem(StorageKey.ACTIVE_RELAY_SET_ID)
}
}
getFeedType() {
return this.feedType
}
setFeedType(feedType: TFeedType) {
this.feedType = feedType
window.localStorage.setItem(StorageKey.FEED_TYPE, this.feedType)
}
getThemeSetting() {
return this.themeSetting
}
setThemeSetting(themeSetting: TThemeSetting) {
window.localStorage.setItem(StorageKey.THEME_SETTING, themeSetting)
this.themeSetting = themeSetting
}
getNoteListMode() {
return this.noteListMode
}
setNoteListMode(mode: TNoteListMode) {
window.localStorage.setItem(StorageKey.NOTE_LIST_MODE, mode)
this.noteListMode = mode
}
getAccounts() {
return this.accounts
}
findAccount(account: TAccountPointer) {
return this.accounts.find((act) => isSameAccount(act, account))
}
getCurrentAccount() {
return this.currentAccount
}
getAccountNsec(pubkey: string) {
const account = this.accounts.find((act) => act.pubkey === pubkey && act.signerType === 'nsec')
return account?.nsec
}
getAccountNcryptsec(pubkey: string) {
const account = this.accounts.find(
(act) => act.pubkey === pubkey && act.signerType === 'ncryptsec'
)
return account?.ncryptsec
}
addAccount(account: TAccount) {
if (this.accounts.find((act) => isSameAccount(act, account))) {
return
}
this.accounts.push(account)
window.localStorage.setItem(StorageKey.ACCOUNTS, JSON.stringify(this.accounts))
return account
}
removeAccount(account: TAccount) {
this.accounts = this.accounts.filter((act) => !isSameAccount(act, account))
delete this.accountFollowListEventMap[account.pubkey]
delete this.accountRelayListEventMap[account.pubkey]
delete this.accountMuteListEventMap[account.pubkey]
delete this.accountMuteDecryptedTagsMap[account.pubkey]
delete this.accountProfileEventMap[account.pubkey]
window.localStorage.setItem(StorageKey.ACCOUNTS, JSON.stringify(this.accounts))
window.localStorage.setItem(
StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP,
JSON.stringify(this.accountFollowListEventMap)
)
window.localStorage.setItem(
StorageKey.ACCOUNT_MUTE_LIST_EVENT_MAP,
JSON.stringify(this.accountMuteListEventMap)
)
window.localStorage.setItem(
StorageKey.ACCOUNT_MUTE_DECRYPTED_TAGS_MAP,
JSON.stringify(this.accountMuteDecryptedTagsMap)
)
window.localStorage.setItem(
StorageKey.ACCOUNT_RELAY_LIST_EVENT_MAP,
JSON.stringify(this.accountRelayListEventMap)
)
window.localStorage.setItem(
StorageKey.ACCOUNT_PROFILE_EVENT_MAP,
JSON.stringify(this.accountProfileEventMap)
)
}
switchAccount(account: TAccount | null) {
if (isSameAccount(this.currentAccount, account)) {
return
}
const act = this.accounts.find((act) => isSameAccount(act, account))
if (!act) {
return
}
this.currentAccount = act
window.localStorage.setItem(StorageKey.CURRENT_ACCOUNT, JSON.stringify(act))
}
getAccountRelayListEvent(pubkey: string) {
return this.accountRelayListEventMap[pubkey]
}
setAccountRelayListEvent(relayListEvent: Event) {
const pubkey = relayListEvent.pubkey
if (
this.accountRelayListEventMap[pubkey] &&
this.accountRelayListEventMap[pubkey].created_at > relayListEvent.created_at
) {
return false
}
this.accountRelayListEventMap[pubkey] = relayListEvent
window.localStorage.setItem(
StorageKey.ACCOUNT_RELAY_LIST_EVENT_MAP,
JSON.stringify(this.accountRelayListEventMap)
)
return true
}
getAccountFollowListEvent(pubkey: string) {
return this.accountFollowListEventMap[pubkey]
}
setAccountFollowListEvent(followListEvent: Event) {
const pubkey = followListEvent.pubkey
if (
this.accountFollowListEventMap[pubkey] &&
this.accountFollowListEventMap[pubkey].created_at > followListEvent.created_at
) {
return false
}
this.accountFollowListEventMap[pubkey] = followListEvent
window.localStorage.setItem(
StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP,
JSON.stringify(this.accountFollowListEventMap)
)
return true
}
getAccountMuteListEvent(pubkey: string) {
return this.accountMuteListEventMap[pubkey]
}
setAccountMuteListEvent(muteListEvent: Event) {
const pubkey = muteListEvent.pubkey
if (
this.accountMuteListEventMap[pubkey] &&
this.accountMuteListEventMap[pubkey].created_at > muteListEvent.created_at
) {
return false
}
this.accountMuteListEventMap[pubkey] = muteListEvent
window.localStorage.setItem(
StorageKey.ACCOUNT_MUTE_LIST_EVENT_MAP,
JSON.stringify(this.accountMuteListEventMap)
)
return true
}
getAccountMuteDecryptedTags(muteListEvent: Event) {
const stored = this.accountMuteDecryptedTagsMap[muteListEvent.pubkey]
if (stored && stored.id === muteListEvent.id) {
return stored.tags
}
return null
}
setAccountMuteDecryptedTags(muteListEvent: Event, tags: string[][]) {
this.accountMuteDecryptedTagsMap[muteListEvent.pubkey] = { id: muteListEvent.id, tags }
window.localStorage.setItem(
StorageKey.ACCOUNT_MUTE_DECRYPTED_TAGS_MAP,
JSON.stringify(this.accountMuteDecryptedTagsMap)
)
}
getAccountProfileEvent(pubkey: string) {
return this.accountProfileEventMap[pubkey]
}
setAccountProfileEvent(profileEvent: Event) {
const pubkey = profileEvent.pubkey
if (
this.accountProfileEventMap[pubkey] &&
this.accountProfileEventMap[pubkey].created_at > profileEvent.created_at
) {
return false
}
this.accountProfileEventMap[pubkey] = profileEvent
window.localStorage.setItem(
StorageKey.ACCOUNT_PROFILE_EVENT_MAP,
JSON.stringify(this.accountProfileEventMap)
)
return true
}
}
const instance = new StorageService()
export default instance