feat: favorite relays (#250)

This commit is contained in:
Cody Tseng 2025-04-05 15:31:34 +08:00 committed by GitHub
parent fab9ff88b5
commit c739d9d28c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
63 changed files with 1081 additions and 982 deletions

View file

@ -90,10 +90,6 @@ class ClientService extends EventTarget {
await indexedDb.iterateProfileEvents((profileEvent) => this.addUsernameToIndex(profileEvent))
}
listConnectionStatus() {
return this.pool.listConnectionStatus()
}
setCurrentRelayUrls(urls: string[]) {
this.currentRelayUrls = urls
}

View file

@ -1,3 +1,4 @@
import { ExtendedKind } from '@/constants'
import { tagNameEquals } from '@/lib/tag'
import { Event, kinds } from 'nostr-tools'
@ -13,7 +14,9 @@ const StoreNames = {
FOLLOW_LIST_EVENTS: 'followListEvents',
MUTE_LIST_EVENTS: 'muteListEvents',
MUTE_DECRYPTED_TAGS: 'muteDecryptedTags',
RELAY_INFO_EVENTS: 'relayInfoEvents'
RELAY_INFO_EVENTS: 'relayInfoEvents',
FAVORITE_RELAYS: 'favoriteRelays',
RELAY_SETS: 'relaySets'
}
class IndexedDbService {
@ -32,7 +35,7 @@ class IndexedDbService {
init(): Promise<void> {
if (!this.initPromise) {
this.initPromise = new Promise((resolve, reject) => {
const request = window.indexedDB.open('jumble', 2)
const request = window.indexedDB.open('jumble', 3)
request.onerror = (event) => {
reject(event)
@ -63,6 +66,12 @@ class IndexedDbService {
if (!db.objectStoreNames.contains(StoreNames.RELAY_INFO_EVENTS)) {
db.createObjectStore(StoreNames.RELAY_INFO_EVENTS, { keyPath: 'key' })
}
if (!db.objectStoreNames.contains(StoreNames.FAVORITE_RELAYS)) {
db.createObjectStore(StoreNames.FAVORITE_RELAYS, { keyPath: 'key' })
}
if (!db.objectStoreNames.contains(StoreNames.RELAY_SETS)) {
db.createObjectStore(StoreNames.RELAY_SETS, { keyPath: 'key' })
}
this.db = db
}
})
@ -84,14 +93,15 @@ class IndexedDbService {
const transaction = this.db.transaction(storeName, 'readwrite')
const store = transaction.objectStore(storeName)
const getRequest = store.get(event.pubkey)
const key = this.getReplaceableEventKey(event)
const getRequest = store.get(key)
getRequest.onsuccess = () => {
const oldValue = getRequest.result as TValue<Event> | undefined
if (oldValue && oldValue.value.created_at >= event.created_at) {
transaction.commit()
return resolve(oldValue.value)
}
const putRequest = store.put(this.formatValue(event.pubkey, event))
const putRequest = store.put(this.formatValue(key, event))
putRequest.onsuccess = () => {
transaction.commit()
resolve(event)
@ -110,7 +120,7 @@ class IndexedDbService {
})
}
async getReplaceableEvent(pubkey: string, kind: number): Promise<Event | undefined> {
async getReplaceableEvent(pubkey: string, kind: number, d?: string): Promise<Event | undefined> {
const storeName = this.getStoreNameByKind(kind)
if (!storeName) {
return Promise.reject('store name not found')
@ -122,7 +132,8 @@ class IndexedDbService {
}
const transaction = this.db.transaction(storeName, 'readonly')
const store = transaction.objectStore(storeName)
const request = store.get(pubkey)
const key = d === undefined ? pubkey : `${pubkey}:${d}`
const request = store.get(key)
request.onsuccess = () => {
transaction.commit()
@ -298,6 +309,18 @@ class IndexedDbService {
})
}
private getReplaceableEventKey(event: Event): string {
if (
[kinds.Metadata, kinds.Contacts].includes(event.kind) ||
(event.kind >= 10000 && event.kind < 20000)
) {
return event.pubkey
}
const [, d] = event.tags.find(tagNameEquals('d')) ?? []
return `${event.pubkey}:${d ?? ''}`
}
private getStoreNameByKind(kind: number): string | undefined {
switch (kind) {
case kinds.Metadata:
@ -308,6 +331,10 @@ class IndexedDbService {
return StoreNames.FOLLOW_LIST_EVENTS
case kinds.Mutelist:
return StoreNames.MUTE_LIST_EVENTS
case kinds.Relaysets:
return StoreNames.RELAY_SETS
case ExtendedKind.FAVORITE_RELAYS:
return StoreNames.FAVORITE_RELAYS
default:
return undefined
}

View file

@ -4,41 +4,16 @@ import { randomString } from '@/lib/random'
import {
TAccount,
TAccountPointer,
TFeedType,
TFeedInfo,
TNoteListMode,
TRelaySet,
TThemeSetting
} from '@/types'
const DEFAULT_RELAY_SETS: TRelaySet[] = [
{
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
@ -47,6 +22,7 @@ class LocalStorageService {
private defaultZapSats: number = 21
private defaultZapComment: string = 'Zap!'
private quickZap: boolean = false
private accountFeedInfoMap: Record<string, TFeedInfo | undefined> = {}
constructor() {
if (!LocalStorageService.instance) {
@ -63,12 +39,6 @@ class LocalStorageService {
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)
@ -93,16 +63,12 @@ class LocalStorageService {
})
}
if (!relaySets.length) {
relaySets = DEFAULT_RELAY_SETS
relaySets = []
}
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
}
const defaultZapSatsStr = window.localStorage.getItem(StorageKey.DEFAULT_ZAP_SATS)
@ -115,12 +81,18 @@ class LocalStorageService {
this.defaultZapComment = window.localStorage.getItem(StorageKey.DEFAULT_ZAP_COMMENT) ?? 'Zap!'
this.quickZap = window.localStorage.getItem(StorageKey.QUICK_ZAP) === 'true'
const accountFeedInfoMapStr =
window.localStorage.getItem(StorageKey.ACCOUNT_FEED_INFO_MAP) ?? '{}'
this.accountFeedInfoMap = JSON.parse(accountFeedInfoMapStr)
// 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)
window.localStorage.removeItem(StorageKey.ACTIVE_RELAY_SET_ID)
window.localStorage.removeItem(StorageKey.FEED_TYPE)
}
getRelaySets() {
@ -132,28 +104,6 @@ class LocalStorageService {
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
}
@ -260,6 +210,18 @@ class LocalStorageService {
JSON.stringify(this.lastReadNotificationTimeMap)
)
}
getFeedInfo(pubkey: string) {
return this.accountFeedInfoMap[pubkey]
}
setFeedInfo(info: TFeedInfo, pubkey?: string | null) {
this.accountFeedInfoMap[pubkey ?? 'default'] = info
window.localStorage.setItem(
StorageKey.ACCOUNT_FEED_INFO_MAP,
JSON.stringify(this.accountFeedInfoMap)
)
}
}
const instance = new LocalStorageService()