feat: allow changing default relays

This commit is contained in:
codytseng 2026-01-04 23:40:43 +08:00
parent 36959a1052
commit 53a67d8233
44 changed files with 356 additions and 92 deletions

View file

@ -1,4 +1,4 @@
import { BIG_RELAY_URLS, ExtendedKind, SEARCHABLE_RELAY_URLS } from '@/constants'
import { ExtendedKind, SEARCHABLE_RELAY_URLS } from '@/constants'
import {
compareEvents,
getReplaceableCoordinate,
@ -7,7 +7,7 @@ import {
} from '@/lib/event'
import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata'
import { formatPubkey, isValidPubkey, pubkeyToNpub, userIdToPubkey } from '@/lib/pubkey'
import { filterOutBigRelays } from '@/lib/relay'
import { filterOutBigRelays, getDefaultRelayUrls } from '@/lib/relay'
import { getPubkeysFromPTags, getServersFromServerTags, tagNameEquals } from '@/lib/tag'
import { mergeTimelines } from '@/lib/timeline'
import { isLocalNetworkUrl, isWebsocketUrl, normalizeUrl } from '@/lib/url'
@ -97,6 +97,7 @@ class ClientService extends EventTarget {
}
}
const defaultRelays = getDefaultRelayUrls()
const relaySet = new Set<string>()
if (specifiedRelayUrls?.length) {
specifiedRelayUrls.forEach((url) => relaySet.add(url))
@ -137,20 +138,20 @@ class ClientService extends EventTarget {
ExtendedKind.RELAY_REVIEW
].includes(event.kind)
) {
BIG_RELAY_URLS.forEach((url) => relaySet.add(url))
defaultRelays.forEach((url) => relaySet.add(url))
}
if (event.kind === ExtendedKind.COMMENT) {
const rootITag = event.tags.find(tagNameEquals('I'))
if (rootITag) {
// For external content comments, always publish to big relays
BIG_RELAY_URLS.forEach((url) => relaySet.add(url))
// For external content comments, always publish to default relays
defaultRelays.forEach((url) => relaySet.add(url))
}
}
}
if (!relaySet.size) {
BIG_RELAY_URLS.forEach((url) => relaySet.add(url))
defaultRelays.forEach((url) => relaySet.add(url))
}
return Array.from(relaySet)
@ -166,7 +167,7 @@ class ClientService extends EventTarget {
const relayLists = await this.fetchRelayLists(filter['#p'])
return Array.from(new Set(relayLists.flatMap((list) => list.read.slice(0, 5))))
}
return BIG_RELAY_URLS
return getDefaultRelayUrls()
}
async publishEvent(relayUrls: string[], event: NEvent) {
@ -807,7 +808,11 @@ class ClientService extends EventTarget {
} = {}
) {
const relays = Array.from(new Set(urls))
const events = await this.query(relays.length > 0 ? relays : BIG_RELAY_URLS, filter, onevent)
const events = await this.query(
relays.length > 0 ? relays : getDefaultRelayUrls(),
filter,
onevent
)
if (cache) {
events.forEach((evt) => {
this.addEventToCache(evt)
@ -935,7 +940,7 @@ class ClientService extends EventTarget {
}
private async fetchEventsFromBigRelays(ids: readonly string[]) {
const events = await this.query(BIG_RELAY_URLS, {
const events = await this.query(getDefaultRelayUrls(), {
ids: Array.from(new Set(ids)),
limit: ids.length
})
@ -961,7 +966,7 @@ class ClientService extends EventTarget {
private async _fetchFollowingFavoriteRelays(pubkey: string) {
const fetchNewData = async () => {
const followings = await this.fetchFollowings(pubkey)
const events = await this.fetchEvents(BIG_RELAY_URLS, {
const events = await this.fetchEvents(getDefaultRelayUrls(), {
authors: followings,
kinds: [ExtendedKind.FAVORITE_RELAYS, kinds.Relaysets],
limit: 1000
@ -1182,9 +1187,10 @@ class ClientService extends EventTarget {
if (event) {
return getRelayListFromEvent(event, storage.getFilterOutOnionRelays())
}
const defaultRelays = getDefaultRelayUrls()
return {
write: BIG_RELAY_URLS,
read: BIG_RELAY_URLS,
write: defaultRelays,
read: defaultRelays,
originalRelays: []
}
})
@ -1224,7 +1230,7 @@ class ClientService extends EventTarget {
const eventsMap = new Map<string, NEvent>()
await Promise.allSettled(
Array.from(groups.entries()).map(async ([kind, pubkeys]) => {
const events = await this.query(BIG_RELAY_URLS, {
const events = await this.query(getDefaultRelayUrls(), {
authors: pubkeys,
kinds: [kind]
})
@ -1340,7 +1346,7 @@ class ClientService extends EventTarget {
: { authors: [pubkey], kinds: [kind] }) as Filter
)
const relayList = await this.fetchRelayList(pubkey)
const relays = relayList.write.concat(BIG_RELAY_URLS).slice(0, 5)
const relays = relayList.write.concat(getDefaultRelayUrls()).slice(0, 5)
const events = await this.query(relays, filters)
for (const event of events) {
@ -1471,10 +1477,10 @@ class ClientService extends EventTarget {
// If many websocket connections are initiated simultaneously, it will be
// very slow on Safari (for unknown reason)
if (isSafari()) {
let urls = BIG_RELAY_URLS
let urls = getDefaultRelayUrls()
if (myPubkey) {
const relayList = await this.fetchRelayList(myPubkey)
urls = relayList.read.concat(BIG_RELAY_URLS).slice(0, 5)
urls = relayList.read.concat(getDefaultRelayUrls()).slice(0, 5)
}
return [{ urls, filter: { authors: pubkeys } }]
}

View file

@ -1,5 +1,6 @@
import { BIG_RELAY_URLS, CODY_PUBKEY, JUMBLE_PUBKEY } from '@/constants'
import { CODY_PUBKEY, JUMBLE_PUBKEY } from '@/constants'
import { getZapInfoFromEvent } from '@/lib/event-metadata'
import { getDefaultRelayUrls } from '@/lib/relay'
import { TProfile } from '@/types'
import { init, launchPaymentModal } from '@getalby/bitcoin-connect-react'
import { Invoice } from '@getalby/lightning-tools'
@ -52,7 +53,7 @@ class LightningService {
client.fetchRelayList(recipient),
sender
? client.fetchRelayList(sender)
: Promise.resolve({ read: BIG_RELAY_URLS, write: BIG_RELAY_URLS })
: Promise.resolve({ read: getDefaultRelayUrls(), write: getDefaultRelayUrls() })
])
if (!profile) {
throw new Error('Recipient not found')
@ -69,7 +70,7 @@ class LightningService {
relays: receiptRelayList.read
.slice(0, 4)
.concat(senderRelayList.write.slice(0, 3))
.concat(BIG_RELAY_URLS),
.concat(getDefaultRelayUrls()),
comment
})
const zapRequest = await client.signer.signEvent(zapRequestDraft)
@ -134,7 +135,7 @@ class LightningService {
filter['#e'] = [event.id]
}
subCloser = client.subscribe(
senderRelayList.write.concat(BIG_RELAY_URLS).slice(0, 4),
senderRelayList.write.concat(getDefaultRelayUrls()).slice(0, 4),
filter,
{
onevent: (evt) => {

View file

@ -1,5 +1,6 @@
import {
ALLOWED_FILTER_KINDS,
BIG_RELAY_URLS,
DEFAULT_FAVICON_URL_TEMPLATE,
DEFAULT_NIP_96_SERVICE,
ExtendedKind,
@ -21,9 +22,9 @@ import {
TMediaAutoLoadPolicy,
TMediaUploadServiceConfig,
TNoteListMode,
TProfilePictureAutoLoadPolicy,
TNsfwDisplayPolicy,
TNotificationStyle,
TNsfwDisplayPolicy,
TProfilePictureAutoLoadPolicy,
TRelaySet,
TThemeSetting,
TTranslationServiceConfig
@ -65,6 +66,7 @@ class LocalStorageService {
private nsfwDisplayPolicy: TNsfwDisplayPolicy = NSFW_DISPLAY_POLICY.HIDE_CONTENT
private minTrustScore: number = 40
private enableLiveFeed: boolean = false
private defaultRelayUrls: string[] = BIG_RELAY_URLS
constructor() {
if (!LocalStorageService.instance) {
@ -278,6 +280,22 @@ class LocalStorageService {
this.enableLiveFeed = window.localStorage.getItem(StorageKey.ENABLE_LIVE_FEED) === 'true'
const defaultRelayUrlsStr = window.localStorage.getItem(StorageKey.DEFAULT_RELAY_URLS)
if (defaultRelayUrlsStr) {
try {
const urls = JSON.parse(defaultRelayUrlsStr)
if (
Array.isArray(urls) &&
urls.length > 0 &&
urls.every((url) => typeof url === 'string')
) {
this.defaultRelayUrls = urls
}
} catch {
// Invalid JSON, use default
}
}
// Clean up deprecated data
window.localStorage.removeItem(StorageKey.PINNED_PUBKEYS)
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)
@ -624,6 +642,15 @@ class LocalStorageService {
this.enableLiveFeed = enable
window.localStorage.setItem(StorageKey.ENABLE_LIVE_FEED, enable.toString())
}
getDefaultRelayUrls() {
return this.defaultRelayUrls
}
setDefaultRelayUrls(urls: string[]) {
this.defaultRelayUrls = urls
window.localStorage.setItem(StorageKey.DEFAULT_RELAY_URLS, JSON.stringify(urls))
}
}
const instance = new LocalStorageService()

View file

@ -1,6 +1,7 @@
import { BIG_RELAY_URLS, ExtendedKind } from '@/constants'
import { ExtendedKind } from '@/constants'
import { getEventKey, getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event'
import { getZapInfoFromEvent } from '@/lib/event-metadata'
import { getDefaultRelayUrls } from '@/lib/relay'
import { getEmojiInfosFromEmojiTags, tagNameEquals } from '@/lib/tag'
import client from '@/services/client.service'
import { TEmoji } from '@/types'
@ -150,7 +151,9 @@ class StuffStatsService {
})
}
const relays = relayList ? relayList.read.concat(BIG_RELAY_URLS).slice(0, 5) : BIG_RELAY_URLS
const relays = relayList
? relayList.read.concat(getDefaultRelayUrls()).slice(0, 5)
: getDefaultRelayUrls()
const events: Event[] = []
await client.fetchEvents(relays, filters, {

View file

@ -1,4 +1,4 @@
import { BIG_RELAY_URLS, ExtendedKind } from '@/constants'
import { ExtendedKind } from '@/constants'
import {
getEventKey,
getKeyFromTag,
@ -9,6 +9,7 @@ import {
isReplaceableEvent,
isReplyNoteEvent
} from '@/lib/event'
import { getDefaultRelayUrls } from '@/lib/relay'
import { generateBech32IdFromETag } from '@/lib/tag'
import client from '@/services/client.service'
import dayjs from 'dayjs'
@ -69,7 +70,7 @@ class ThreadService {
const relayList = await client.fetchRelayList(rootPubkey)
relayUrls = relayList.read
}
relayUrls = relayUrls.concat(BIG_RELAY_URLS).slice(0, 4)
relayUrls = relayUrls.concat(getDefaultRelayUrls()).slice(0, 4)
// If current event is protected, we can assume its replies are also protected and stored on the same relays
if (event && isProtectedEvent(event)) {