feat: zap (#107)

This commit is contained in:
Cody Tseng
2025-03-01 23:52:05 +08:00
committed by GitHub
parent 407a6fb802
commit 249593d547
72 changed files with 2582 additions and 818 deletions

View File

@@ -17,6 +17,7 @@ import {
SimplePool,
VerifiedEvent
} from 'nostr-tools'
import { SubscribeManyParams } from 'nostr-tools/abstract-pool'
import { AbstractRelay } from 'nostr-tools/abstract-relay'
import indexedDb from './indexed-db.service'
@@ -44,28 +45,23 @@ class ClientService extends EventTarget {
{ cacheMap: this.eventCache }
)
private fetchEventFromBigRelaysDataloader = new DataLoader<string, NEvent | undefined>(
this.eventBatchLoadFn.bind(this),
{ cache: false }
)
private profileEventDataloader = new DataLoader<string, NEvent | undefined>(
(ids) => Promise.all(ids.map((id) => this._fetchProfileEvent(id))),
{
cache: false,
maxBatchSize: 50
}
this.fetchEventsFromBigRelays.bind(this),
{ cache: false, batchScheduleFn: (callback) => setTimeout(callback, 200) }
)
private fetchProfileEventFromBigRelaysDataloader = new DataLoader<string, NEvent | undefined>(
this.profileEventBatchLoadFn.bind(this),
{
batchScheduleFn: (callback) => setTimeout(callback, 200),
cacheMap: new LRUCache<string, Promise<NEvent | undefined>>({ max: 1000 }),
maxBatchSize: 50
maxBatchSize: 20
}
)
private relayListEventDataLoader = new DataLoader<string, NEvent | undefined>(
this.relayListEventBatchLoadFn.bind(this),
{
batchScheduleFn: (callback) => setTimeout(callback, 200),
cacheMap: new LRUCache<string, Promise<NEvent | undefined>>({ max: 1000 }),
maxBatchSize: 50
maxBatchSize: 20
}
)
private followListCache = new LRUCache<string, Promise<NEvent | undefined>>({
@@ -166,7 +162,8 @@ class ClientService extends EventTarget {
needSort?: boolean
} = {}
) {
const key = this.generateTimelineKey(urls, filter)
const relays = Array.from(new Set(urls))
const key = this.generateTimelineKey(relays, filter)
const timeline = this.timelines[key]
let cachedEvents: NEvent[] = []
let since: number | undefined
@@ -183,7 +180,7 @@ class ClientService extends EventTarget {
}
if (!timeline && needSort) {
this.timelines[key] = { refs: [], filter, urls }
this.timelines[key] = { refs: [], filter, urls: relays }
}
// eslint-disable-next-line @typescript-eslint/no-this-alias
@@ -193,7 +190,7 @@ class ClientService extends EventTarget {
let startedCount = 0
let eosedCount = 0
let eosed = false
const subPromises = urls.map(async (url) => {
const subPromises = relays.map(async (url) => {
const relay = await this.pool.ensureRelay(url)
let hasAuthed = false
@@ -345,11 +342,19 @@ class ClientService extends EventTarget {
}
}
async query(urls: string[], filter: Filter) {
subscribe(urls: string[], filter: Filter | Filter[], params: SubscribeManyParams) {
const relays = Array.from(new Set(urls))
const filters = Array.isArray(filter) ? filter : [filter]
return this.pool.subscribeMany(relays, filters, params)
}
private async query(urls: string[], filter: Filter | Filter[], onevent?: (evt: NEvent) => void) {
const relays = Array.from(new Set(urls))
const filters = Array.isArray(filter) ? filter : [filter]
const _knownIds = new Set<string>()
const events: NEvent[] = []
await Promise.allSettled(
urls.map(async (url) => {
relays.map(async (url) => {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this
const relay = await this.pool.ensureRelay(url)
@@ -357,7 +362,7 @@ class ClientService extends EventTarget {
return new Promise<void>((resolve, reject) => {
const startQuery = () => {
relay.subscribe([filter], {
relay.subscribe(filters, {
receivedEvent(relay, id) {
that.trackEventSeenOn(id, relay)
},
@@ -384,6 +389,7 @@ class ClientService extends EventTarget {
if (_knownIds.has(evt.id)) return
_knownIds.add(evt.id)
events.push(evt)
onevent?.(evt)
}
})
}
@@ -421,10 +427,22 @@ class ClientService extends EventTarget {
return events
}
async fetchEvents(relayUrls: string[], filter: Filter, cache = false) {
async fetchEvents(
urls: string[],
filter: Filter | Filter[],
{
onevent,
cache = false
}: {
onevent?: (evt: NEvent) => void
cache?: boolean
} = {}
) {
const relays = Array.from(new Set(urls))
const events = await this.query(
relayUrls.length > 0 ? relayUrls : this.currentRelayUrls.concat(BIG_RELAY_URLS),
filter
relays.length > 0 ? relays : this.currentRelayUrls.concat(BIG_RELAY_URLS),
filter,
onevent
)
if (cache) {
events.forEach((evt) => {
@@ -460,12 +478,70 @@ class ClientService extends EventTarget {
this.eventDataLoader.prime(event.id, Promise.resolve(event))
}
async fetchProfileEvent(id: string): Promise<NEvent | undefined> {
return await this.profileEventDataloader.load(id)
async fetchProfileEvent(id: string, skipCache: boolean = false): Promise<NEvent | undefined> {
let pubkey: string | undefined
let relays: string[] = []
if (/^[0-9a-f]{64}$/.test(id)) {
pubkey = id
} else {
const { data, type } = nip19.decode(id)
switch (type) {
case 'npub':
pubkey = data
break
case 'nprofile':
pubkey = data.pubkey
if (data.relays) relays = data.relays
break
}
}
if (!pubkey) {
throw new Error('Invalid id')
}
if (!skipCache) {
const localProfile = await indexedDb.getReplaceableEvent(pubkey, kinds.Metadata)
if (localProfile) {
this.addUsernameToIndex(localProfile)
return localProfile
}
}
const profileFromBigRelays = await this.fetchProfileEventFromBigRelaysDataloader.load(pubkey)
if (profileFromBigRelays) {
this.addUsernameToIndex(profileFromBigRelays)
await indexedDb.putReplaceableEvent(profileFromBigRelays)
return profileFromBigRelays
}
if (!relays.length) {
return undefined
}
const profileEvent = await this.tryHarderToFetchEvent(
relays,
{
authors: [pubkey],
kinds: [kinds.Metadata],
limit: 1
},
true
)
if (profileEvent) {
this.addUsernameToIndex(profileEvent)
indexedDb.putReplaceableEvent(profileEvent)
}
return profileEvent
}
async fetchProfile(id: string): Promise<TProfile | undefined> {
const profileEvent = await this.fetchProfileEvent(id)
async fetchProfile(id: string, skipCache: boolean = false): Promise<TProfile | undefined> {
let profileEvent: NEvent | undefined
if (skipCache) {
profileEvent = await this.fetchProfileEvent(id, skipCache)
} else {
profileEvent = await this.fetchProfileEvent(id)
}
if (profileEvent) {
return getProfileFromProfileEvent(profileEvent)
}
@@ -478,11 +554,6 @@ class ClientService extends EventTarget {
}
}
updateProfileCache(event: NEvent) {
this.profileEventDataloader.clear(event.pubkey)
this.profileEventDataloader.prime(event.pubkey, Promise.resolve(event))
}
async fetchProfiles(relayUrls: string[], filter: Filter): Promise<TProfile[]> {
const events = await this.query(relayUrls, {
...filter,
@@ -490,7 +561,6 @@ class ClientService extends EventTarget {
})
const profileEvents = events.sort((a, b) => b.created_at - a.created_at)
profileEvents.forEach((profile) => this.profileEventDataloader.prime(profile.pubkey, profile))
await Promise.all(profileEvents.map((profile) => this.addUsernameToIndex(profile)))
return profileEvents.map((profileEvent) => getProfileFromProfileEvent(profileEvent))
}
@@ -519,17 +589,22 @@ class ClientService extends EventTarget {
return event
}
async fetchFollowings(pubkey: string) {
const followListEvent = await this.fetchFollowListEvent(pubkey)
async fetchFollowings(pubkey: string, storeToIndexedDb = false) {
const followListEvent = await this.fetchFollowListEvent(pubkey, storeToIndexedDb)
return followListEvent ? extractPubkeysFromEventTags(followListEvent.tags) : []
}
updateFollowListCache(pubkey: string, event: NEvent) {
this.followListCache.set(pubkey, Promise.resolve(event))
updateFollowListCache(event: NEvent) {
this.followListCache.set(event.pubkey, Promise.resolve(event))
}
updateRelayListCache(event: NEvent) {
this.relayListEventDataLoader.clear(event.pubkey)
this.relayListEventDataLoader.prime(event.pubkey, Promise.resolve(event))
}
async calculateOptimalReadRelays(pubkey: string) {
const followings = await this.fetchFollowings(pubkey)
const followings = await this.fetchFollowings(pubkey, true)
const [selfRelayListEvent, ...relayListEvents] = await this.relayListEventDataLoader.loadMany([
pubkey,
...followings
@@ -544,7 +619,6 @@ class ClientService extends EventTarget {
pubkeyRelayListMap.set(evt.pubkey, getRelayListFromRelayListEvent(evt).write)
}
})
let uncoveredPubkeys = [...followings]
const readRelays: { url: string; pubkeys: string[] }[] = []
while (uncoveredPubkeys.length) {
@@ -571,7 +645,6 @@ class ClientService extends EventTarget {
}
}
if (!maxCoveredRelay) break
readRelays.push(maxCoveredRelay)
uncoveredPubkeys = uncoveredPubkeys.filter(
(pubkey) => !maxCoveredRelay!.pubkeys.includes(pubkey)
@@ -588,12 +661,13 @@ class ClientService extends EventTarget {
}
async initUserIndexFromFollowings(pubkey: string, signal: AbortSignal) {
const followings = await this.fetchFollowings(pubkey)
for (let i = 0; i * 50 < followings.length; i++) {
const followings = await this.fetchFollowings(pubkey, true)
for (let i = 0; i * 20 < 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))
await Promise.all(
followings.slice(i * 20, (i + 1) * 20).map((pubkey) => this.fetchProfileEvent(pubkey))
)
await new Promise((resolve) => setTimeout(resolve, 1000))
}
}
@@ -665,9 +739,7 @@ class ClientService extends EventTarget {
let event: NEvent | undefined
if (filter.ids) {
event = await this.fetchEventById(relays, filter.ids[0])
}
if (!event) {
} else {
event = await this.tryHarderToFetchEvent(relays, filter)
}
@@ -678,62 +750,6 @@ class ClientService extends EventTarget {
return event
}
private async _fetchProfileEvent(id: string): Promise<NEvent | undefined> {
let pubkey: string | undefined
let relays: string[] = []
if (/^[0-9a-f]{64}$/.test(id)) {
pubkey = id
} else {
const { data, type } = nip19.decode(id)
switch (type) {
case 'npub':
pubkey = data
break
case 'nprofile':
pubkey = data.pubkey
if (data.relays) relays = data.relays
break
}
}
if (!pubkey) {
throw new Error('Invalid id')
}
const localProfile = await indexedDb.getReplaceableEvent(pubkey, kinds.Metadata)
if (localProfile) {
this.addUsernameToIndex(localProfile)
return localProfile
}
const profileFromBigRelays = await this.fetchProfileEventFromBigRelaysDataloader.load(pubkey)
if (profileFromBigRelays) {
this.addUsernameToIndex(profileFromBigRelays)
await indexedDb.putReplaceableEvent(profileFromBigRelays)
return profileFromBigRelays
}
const profileEvent = await this.tryHarderToFetchEvent(
relays,
{
authors: [pubkey],
kinds: [kinds.Metadata],
limit: 1
},
true
)
if (pubkey !== id) {
this.profileEventDataloader.prime(pubkey, Promise.resolve(profileEvent))
}
if (profileEvent) {
await Promise.allSettled([
this.addUsernameToIndex(profileEvent),
indexedDb.putReplaceableEvent(profileEvent)
])
}
return profileEvent
}
private async addUsernameToIndex(profileEvent: NEvent) {
try {
const profileObj = JSON.parse(profileEvent.content)
@@ -772,7 +788,7 @@ class ClientService extends EventTarget {
return events.sort((a, b) => b.created_at - a.created_at)[0]
}
private async eventBatchLoadFn(ids: readonly string[]) {
private async fetchEventsFromBigRelays(ids: readonly string[]) {
const events = await this.query(BIG_RELAY_URLS, {
ids: Array.from(new Set(ids)),
limit: ids.length
@@ -803,10 +819,8 @@ class ClientService extends EventTarget {
return eventsMap.get(pubkey)
})
await Promise.allSettled(
profileEvents.map(
(profileEvent) => profileEvent && indexedDb.putReplaceableEvent(profileEvent)
)
profileEvents.forEach(
(profileEvent) => profileEvent && indexedDb.putReplaceableEvent(profileEvent)
)
return profileEvents
}
@@ -830,9 +844,7 @@ class ClientService extends EventTarget {
eventsMap.set(pubkey, event)
}
}
await Promise.allSettled(
Array.from(eventsMap.values()).map((evt) => indexedDb.putReplaceableEvent(evt))
)
Array.from(eventsMap.values()).forEach((evt) => indexedDb.putReplaceableEvent(evt))
nonExistingPubkeys.forEach((pubkey) => {
const event = eventsMap.get(pubkey)
if (event) {
@@ -846,6 +858,11 @@ class ClientService extends EventTarget {
}
private async _fetchFollowListEvent(pubkey: string) {
const storedFollowListEvent = await indexedDb.getReplaceableEvent(pubkey, kinds.Contacts)
if (storedFollowListEvent) {
return storedFollowListEvent
}
const relayList = await this.fetchRelayList(pubkey)
const followListEvents = await this.query(relayList.write.concat(BIG_RELAY_URLS), {
authors: [pubkey],

View File

@@ -44,25 +44,26 @@ class IndexedDbService {
}
request.onupgradeneeded = () => {
this.db = request.result
if (!this.db.objectStoreNames.contains(StoreNames.PROFILE_EVENTS)) {
this.db.createObjectStore(StoreNames.PROFILE_EVENTS, { keyPath: 'key' })
const db = request.result
if (!db.objectStoreNames.contains(StoreNames.PROFILE_EVENTS)) {
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 (!db.objectStoreNames.contains(StoreNames.RELAY_LIST_EVENTS)) {
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 (!db.objectStoreNames.contains(StoreNames.FOLLOW_LIST_EVENTS)) {
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 (!db.objectStoreNames.contains(StoreNames.MUTE_LIST_EVENTS)) {
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 (!db.objectStoreNames.contains(StoreNames.MUTE_DECRYPTED_TAGS)) {
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' })
if (!db.objectStoreNames.contains(StoreNames.RELAY_INFO_EVENTS)) {
db.createObjectStore(StoreNames.RELAY_INFO_EVENTS, { keyPath: 'key' })
}
this.db = db
}
})
setTimeout(() => this.cleanUp(), 1000 * 60) // 1 minute
@@ -98,6 +99,10 @@ class IndexedDbService {
reject(event)
}
}
getRequest.onerror = (event) => {
reject(event)
}
})
}
@@ -264,12 +269,28 @@ class IndexedDbService {
return
}
const expirationTimestamp = Date.now() - 1000 * 60 * 60 * 24 // 1 day
const transaction = this.db!.transaction(Object.values(StoreNames), 'readwrite')
const stores = [
{ name: StoreNames.PROFILE_EVENTS, expirationTimestamp: Date.now() - 1000 * 60 * 60 * 24 }, // 1 day
{ name: StoreNames.RELAY_LIST_EVENTS, expirationTimestamp: Date.now() - 1000 * 60 * 60 * 24 }, // 1 day
{
name: StoreNames.FOLLOW_LIST_EVENTS,
expirationTimestamp: Date.now() - 1000 * 60 * 60 * 24
}, // 1 day
{ name: StoreNames.RELAY_INFO_EVENTS, expirationTimestamp: -1 },
{ name: StoreNames.MUTE_LIST_EVENTS, expirationTimestamp: -1 },
{ name: StoreNames.MUTE_DECRYPTED_TAGS, expirationTimestamp: -1 }
]
const transaction = this.db!.transaction(
stores.map((store) => store.name),
'readwrite'
)
await Promise.allSettled(
Object.values(StoreNames).map((storeName) => {
stores.map(({ name, expirationTimestamp }) => {
if (expirationTimestamp < 0) {
return Promise.resolve()
}
return new Promise<void>((resolve, reject) => {
const store = transaction.objectStore(storeName)
const store = transaction.objectStore(name)
const request = store.openCursor()
request.onsuccess = (event) => {
const cursor = (event.target as IDBRequest).result

View File

@@ -0,0 +1,192 @@
import { BIG_RELAY_URLS } from '@/constants'
import { extractZapInfoFromReceipt } from '@/lib/event'
import { TProfile } from '@/types'
import {
init,
launchPaymentModal,
onConnected,
onDisconnected
} from '@getalby/bitcoin-connect-react'
import { Invoice } from '@getalby/lightning-tools'
import { bech32 } from '@scure/base'
import { WebLNProvider } from '@webbtc/webln-types'
import dayjs from 'dayjs'
import { Filter, kinds } from 'nostr-tools'
import { SubCloser } from 'nostr-tools/abstract-pool'
import { makeZapRequest } from 'nostr-tools/nip57'
import { utf8Decoder } from 'nostr-tools/utils'
import client from './client.service'
class LightningService {
static instance: LightningService
private provider: WebLNProvider | null = null
constructor() {
if (!LightningService.instance) {
LightningService.instance = this
init({
appName: 'Jumble',
showBalance: false
})
onConnected((provider) => {
this.provider = provider
})
onDisconnected(() => {
this.provider = null
})
}
return LightningService.instance
}
async zap(
sender: string,
recipient: string,
sats: number,
comment: string,
eventId?: string,
closeOuterModel?: () => void
): Promise<{ preimage: string; invoice: string }> {
if (!client.signer) {
throw new Error('You need to be logged in to zap')
}
const [profile, receiptRelayList, senderRelayList] = await Promise.all([
client.fetchProfile(recipient, true),
client.fetchRelayList(recipient),
sender
? client.fetchRelayList(sender)
: Promise.resolve({ read: BIG_RELAY_URLS, write: BIG_RELAY_URLS })
])
if (!profile) {
throw new Error('Recipient not found')
}
const zapEndpoint = await this.getZapEndpoint(profile)
if (!zapEndpoint) {
throw new Error("Recipient's lightning address is invalid")
}
const { callback, lnurl } = zapEndpoint
const amount = sats * 1000
const zapRequestDraft = makeZapRequest({
profile: recipient,
event: eventId ?? null,
amount,
relays: receiptRelayList.read
.slice(0, 4)
.concat(senderRelayList.write.slice(0, 3))
.concat(BIG_RELAY_URLS),
comment
})
const zapRequest = await client.signer(zapRequestDraft)
const zapRequestRes = await fetch(
`${callback}?amount=${amount}&nostr=${encodeURI(JSON.stringify(zapRequest))}&lnurl=${lnurl}`
)
const zapRequestResBody = await zapRequestRes.json()
if (zapRequestResBody.error) {
throw new Error(zapRequestResBody.error)
}
const { pr, verify } = zapRequestResBody
if (!pr) {
throw new Error('Failed to create invoice')
}
if (this.provider) {
const { preimage } = await this.provider.sendPayment(pr)
closeOuterModel?.()
return { preimage, invoice: pr }
}
return new Promise((resolve) => {
closeOuterModel?.()
let checkPaymentInterval: ReturnType<typeof setInterval> | undefined
let subCloser: SubCloser | undefined
const { setPaid } = launchPaymentModal({
invoice: pr,
onPaid: (response) => {
clearInterval(checkPaymentInterval)
subCloser?.close()
resolve({ preimage: response.preimage, invoice: pr })
},
onCancelled: () => {
clearInterval(checkPaymentInterval)
subCloser?.close()
}
})
if (verify) {
checkPaymentInterval = setInterval(async () => {
const invoice = new Invoice({ pr, verify })
const paid = await invoice.verifyPayment()
if (paid && invoice.preimage) {
setPaid({
preimage: invoice.preimage
})
}
}, 1000)
} else {
const filter: Filter = {
kinds: [kinds.Zap],
'#p': [recipient],
since: dayjs().subtract(1, 'minute').unix()
}
if (eventId) {
filter['#e'] = [eventId]
}
subCloser = client.subscribe(
senderRelayList.write.concat(BIG_RELAY_URLS).slice(0, 4),
filter,
{
onevent: (evt) => {
const info = extractZapInfoFromReceipt(evt)
if (!info) return
if (info.invoice === pr) {
setPaid({ preimage: info.preimage ?? '' })
}
}
}
)
}
})
}
private async getZapEndpoint(profile: TProfile): Promise<null | {
callback: string
lnurl: string
}> {
try {
let lnurl: string = ''
// Some clients have incorrectly filled in the positions for lud06 and lud16
if (!profile.lightningAddress) {
return null
}
if (profile.lightningAddress.includes('@')) {
const [name, domain] = profile.lightningAddress.split('@')
lnurl = new URL(`/.well-known/lnurlp/${name}`, `https://${domain}`).toString()
} else {
const { words } = bech32.decode(profile.lightningAddress, 1000)
const data = bech32.fromWords(words)
lnurl = utf8Decoder.decode(data)
}
const res = await fetch(lnurl)
const body = await res.json()
if (body.allowsNostr && body.nostrPubkey) {
return {
callback: body.callback,
lnurl
}
}
} catch (err) {
console.error(err)
}
return null
}
}
const instance = new LightningService()
export default instance

View File

@@ -48,6 +48,10 @@ class LocalStorageService {
private accounts: TAccount[] = []
private currentAccount: TAccount | null = null
private noteListMode: TNoteListMode = 'posts'
private lastReadNotificationTimeMap: Record<string, number> = {}
private defaultZapSats: number = 21
private defaultZapComment: string = 'Zap!'
private quickZap: boolean = false
constructor() {
if (!LocalStorageService.instance) {
@@ -75,6 +79,9 @@ class LocalStorageService {
noteListModeStr && ['posts', 'postsAndReplies', 'pictures'].includes(noteListModeStr)
? (noteListModeStr as TNoteListMode)
: 'posts'
const lastReadNotificationTimeMapStr =
window.localStorage.getItem(StorageKey.LAST_READ_NOTIFICATION_TIME_MAP) ?? '{}'
this.lastReadNotificationTimeMap = JSON.parse(lastReadNotificationTimeMapStr)
const relaySetsStr = window.localStorage.getItem(StorageKey.RELAY_SETS)
if (!relaySetsStr) {
@@ -103,6 +110,16 @@ class LocalStorageService {
this.activeRelaySetId = window.localStorage.getItem(StorageKey.ACTIVE_RELAY_SET_ID) ?? null
}
const defaultZapSatsStr = window.localStorage.getItem(StorageKey.DEFAULT_ZAP_SATS)
if (defaultZapSatsStr) {
const num = parseInt(defaultZapSatsStr)
if (!isNaN(num)) {
this.defaultZapSats = num
}
}
this.defaultZapComment = window.localStorage.getItem(StorageKey.DEFAULT_ZAP_COMMENT) ?? 'Zap!'
this.quickZap = window.localStorage.getItem(StorageKey.QUICK_ZAP) === 'true'
// Clean up deprecated data
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)
window.localStorage.removeItem(StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP)
@@ -209,6 +226,45 @@ class LocalStorageService {
this.currentAccount = act
window.localStorage.setItem(StorageKey.CURRENT_ACCOUNT, JSON.stringify(act))
}
getDefaultZapSats() {
return this.defaultZapSats
}
setDefaultZapSats(sats: number) {
this.defaultZapSats = sats
window.localStorage.setItem(StorageKey.DEFAULT_ZAP_SATS, sats.toString())
}
getDefaultZapComment() {
return this.defaultZapComment
}
setDefaultZapComment(comment: string) {
this.defaultZapComment = comment
window.localStorage.setItem(StorageKey.DEFAULT_ZAP_COMMENT, comment)
}
getQuickZap() {
return this.quickZap
}
setQuickZap(quickZap: boolean) {
this.quickZap = quickZap
window.localStorage.setItem(StorageKey.QUICK_ZAP, quickZap.toString())
}
getLastReadNotificationTime(pubkey: string) {
return this.lastReadNotificationTimeMap[pubkey] ?? 0
}
setLastReadNotificationTime(pubkey: string, time: number) {
this.lastReadNotificationTimeMap[pubkey] = time
window.localStorage.setItem(
StorageKey.LAST_READ_NOTIFICATION_TIME_MAP,
JSON.stringify(this.lastReadNotificationTimeMap)
)
}
}
const instance = new LocalStorageService()

View File

@@ -139,7 +139,7 @@ class RelayInfoService {
relayInfos.forEach((relayInfo) => this.addRelayInfo(relayInfo))
this.relayUrlsForRandom = Array.from(this.relayInfoMap.keys())
const loadFromInternet = async () => {
const loadFromInternet = async (slowFetch: boolean = true) => {
let until: number = Math.round(Date.now() / 1000)
const since = until - 60 * 60 * 48
@@ -149,23 +149,28 @@ class RelayInfoService {
kinds: [30166],
since,
until,
limit: 1000
limit: slowFetch ? 100 : 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)))
for (const event of events) {
await indexedDb.putRelayInfoEvent(event)
const relayInfo = formatRelayInfoEvents([event])[0]
await this.addRelayInfo(relayInfo)
}
until = events[events.length - 1].created_at - 1
const relayInfos = formatRelayInfoEvents(events)
relayInfos.forEach((relayInfo) => this.addRelayInfo(relayInfo))
if (slowFetch) {
await new Promise((resolve) => setTimeout(resolve, 1000))
}
}
this.relayUrlsForRandom = Array.from(this.relayInfoMap.keys())
}
if (localRelayInfos.length === 0) {
await loadFromInternet()
await loadFromInternet(false)
} else {
loadFromInternet()
setTimeout(loadFromInternet, 1000 * 20) // 20 seconds
}
}