perf: improve loading speed (#116)
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
271
src/services/indexed-db.service.ts
Normal file
271
src/services/indexed-db.service.ts
Normal 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
|
||||
215
src/services/local-storage.service.ts
Normal file
215
src/services/local-storage.service.ts
Normal 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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user