diff --git a/src/providers/NostrProvider/index.tsx b/src/providers/NostrProvider/index.tsx
index 43980157..d2c5e8fe 100644
--- a/src/providers/NostrProvider/index.tsx
+++ b/src/providers/NostrProvider/index.tsx
@@ -1,5 +1,5 @@
import LoginDialog from '@/components/LoginDialog'
-import { ApplicationDataKey, BIG_RELAY_URLS, ExtendedKind, FAST_WRITE_RELAY_URLS, PROFILE_FETCH_RELAY_URLS, PROFILE_RELAY_URLS, StorageKey } from '@/constants'
+import { ApplicationDataKey, BIG_RELAY_URLS, ExtendedKind, FAST_WRITE_RELAY_URLS, PROFILE_FETCH_RELAY_URLS, PROFILE_RELAY_URLS } from '@/constants'
import {
buildAltTag,
buildClientTag,
@@ -875,7 +875,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
const addClientTag =
typeof options.addClientTag === 'boolean'
? options.addClientTag
- : (typeof window !== 'undefined' && window.localStorage.getItem(StorageKey.ADD_CLIENT_TAG) !== 'false')
+ : (typeof window !== 'undefined' && storage.getAddClientTag())
if (addClientTag) {
draft.tags = draft.tags ?? []
draft.tags.push(buildClientTag(), buildAltTag())
@@ -919,6 +919,12 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
// This metadata is only for logging/feedback, not part of the actual event
const relayStatuses = publishResult.relayStatuses.length > 0 ? publishResult.relayStatuses : undefined
+ // If at least one relay accepted, cache and emit immediately so UI shows the event without waiting
+ if (publishResult.successCount >= 1) {
+ client.addEventToCache(event)
+ client.emitNewEvent(event)
+ }
+
// If publishing failed completely, throw an error so the form doesn't close
if (!publishResult.success) {
logger.error('[Publish] Publishing failed to all relays!', {
@@ -934,26 +940,19 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
'Failed to publish to any relay'
)
;(error as any).relayStatuses = publishResult.relayStatuses
+ if (publishResult.successCount >= 1) (error as any).event = event
throw error
}
-
+
logger.debug('[Publish] Publishing successful, attaching relayStatuses to event')
// Attach relayStatuses only temporarily for UI feedback, then remove it
- // This prevents it from being included in the event when serialized
- // Use a longer delay to ensure UI components can read it before deletion
if (relayStatuses) {
(event as any).relayStatuses = relayStatuses
- // Remove it after a delay to allow UI components to read it
- // Components should read it immediately after publish() returns
setTimeout(() => {
delete (event as any).relayStatuses
}, 100)
}
-
- // Emit newEvent immediately after publishing so UI components can react
- // This ensures replies appear immediately in the note view
- client.emitNewEvent(event)
-
+ // Cache and emit already done above when successCount >= 1
logger.debug('[Publish] Returning event', { eventId: event.id?.substring(0, 8), hasRelayStatuses: !!relayStatuses })
return event
} catch (error) {
diff --git a/src/providers/ThemeProvider.tsx b/src/providers/ThemeProvider.tsx
index f9faa581..2039268b 100644
--- a/src/providers/ThemeProvider.tsx
+++ b/src/providers/ThemeProvider.tsx
@@ -21,9 +21,9 @@ const ThemeProviderContext = createContext
(undef
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
const [themeSetting, setThemeSetting] = useState(
- (localStorage.getItem('themeSetting') as TThemeSetting | null) ?? 'system'
+ () => storage.getThemeSetting()
)
- const [theme, setTheme] = useState('light')
+ const [theme, setTheme] = useState(() => storage.getTheme())
useEffect(() => {
const init = async () => {
@@ -54,13 +54,10 @@ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
}, [themeSetting])
useEffect(() => {
- const updateTheme = async () => {
- const root = window.document.documentElement
- root.classList.remove('light', 'dark')
- root.classList.add(theme)
- localStorage.setItem('theme', theme)
- }
- updateTheme()
+ const root = window.document.documentElement
+ root.classList.remove('light', 'dark')
+ root.classList.add(theme)
+ storage.setTheme(theme)
}, [theme])
return (
diff --git a/src/routes.tsx b/src/routes.tsx
index 6a9aad44..78460f7d 100644
--- a/src/routes.tsx
+++ b/src/routes.tsx
@@ -13,6 +13,7 @@ import ProfilePage from './pages/secondary/ProfilePage'
import RelayPage from './pages/secondary/RelayPage'
import RelayReviewsPage from './pages/secondary/RelayReviewsPage'
import RelaySettingsPage from './pages/secondary/RelaySettingsPage'
+import CacheSettingsPage from './pages/secondary/CacheSettingsPage'
import RssFeedSettingsPage from './pages/secondary/RssFeedSettingsPage'
import SearchPage from './pages/secondary/SearchPage'
import SettingsPage from './pages/secondary/SettingsPage'
@@ -40,6 +41,7 @@ const ROUTES = [
{ path: '/search', element: },
{ path: '/settings', element: },
{ path: '/settings/relays', element: },
+ { path: '/settings/cache', element: },
{ path: '/settings/wallet', element: },
{ path: '/settings/posts', element: },
{ path: '/settings/general', element: },
diff --git a/src/services/client.service.ts b/src/services/client.service.ts
index 94bc2d38..87b44cb5 100644
--- a/src/services/client.service.ts
+++ b/src/services/client.service.ts
@@ -1,4 +1,4 @@
-import { BIG_RELAY_URLS, BOOKSTR_RELAY_URLS, ExtendedKind, FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS, NIP66_DISCOVERY_RELAY_URLS, PROFILE_FETCH_RELAY_URLS, PROFILE_RELAY_URLS, SEARCHABLE_RELAY_URLS } from '@/constants'
+import { BIG_RELAY_URLS, BOOKSTR_RELAY_URLS, ExtendedKind, FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS, KIND_1_BLOCKED_RELAY_URLS, NIP66_DISCOVERY_RELAY_URLS, PROFILE_FETCH_RELAY_URLS, PROFILE_RELAY_URLS, READ_ONLY_RELAY_URLS, SEARCHABLE_RELAY_URLS } from '@/constants'
/** NIP-01 filter keys only; NIP-50 adds `search` which non-searchable relays reject. */
function filterForRelay(f: Filter, relaySupportsSearch: boolean): Filter {
@@ -54,6 +54,8 @@ class ClientService extends EventTarget {
| undefined
> = {}
private eventCacheMap = new Map>()
+ /** Session-only: recently seen events (e.g. from feed) so back-navigation doesn't re-query. Bounded size, keyed by hex id. */
+ private sessionEventCache = new LRUCache({ max: 500, ttl: 1000 * 60 * 30 })
private relayListRequestCache = new Map>() // Cache in-flight relay list requests
private eventDataLoader = new DataLoader(
(ids) => Promise.all(ids.map((id) => this._fetchEvent(id))),
@@ -72,6 +74,13 @@ class ClientService extends EventTarget {
private activeSubCountByRelay = new Map()
private subSlotWaitQueueByRelay = new Map void>>()
+ /** Session-only: relay URL -> publish failure count; after 3 strikes we skip that relay for the rest of the session. */
+ private publishStrikeCount = new Map()
+ private static readonly PUBLISH_STRIKES_THRESHOLD = 3
+
+ /** Session-only: relay URL -> { successCount, sumLatencyMs } for preferring faster, proven relays when picking "random" relays. */
+ private sessionRelayPublishStats = new Map()
+
constructor() {
super()
this.pool = new SimplePool()
@@ -359,17 +368,131 @@ class ClientService extends EventTarget {
})
}
+ const readOnlySet = new Set(READ_ONLY_RELAY_URLS.map((u) => normalizeUrl(u) || u))
+ const kind1BlockedSet = new Set(KIND_1_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u))
+ relays = relays.filter((url) => {
+ const n = normalizeUrl(url) || url
+ if (readOnlySet.has(n)) return false
+ if (event.kind === kinds.ShortTextNote && kind1BlockedSet.has(n)) return false
+ return true
+ })
+
return relays
}
+ /** Record publish failures for 3-strikes session policy (skip relay for rest of session after 3 rejections). */
+ private recordPublishFailures(relayStatuses: { url: string; success: boolean; error?: string }[]) {
+ relayStatuses.filter((s) => !s.success).forEach((s) => {
+ const n = normalizeUrl(s.url) || s.url
+ const count = (this.publishStrikeCount.get(n) ?? 0) + 1
+ this.publishStrikeCount.set(n, count)
+ if (count >= ClientService.PUBLISH_STRIKES_THRESHOLD) {
+ logger.debug('[PublishEvent] Relay reached 3 strikes, skipping for session', { url: n })
+ }
+ })
+ }
+
+ /** Record a successful publish and its latency for session-based preference when selecting random relays. */
+ recordPublishSuccess(url: string, latencyMs: number) {
+ const n = normalizeUrl(url) || url
+ const cur = this.sessionRelayPublishStats.get(n)
+ if (cur) {
+ cur.successCount += 1
+ cur.sumLatencyMs += latencyMs
+ } else {
+ this.sessionRelayPublishStats.set(n, { successCount: 1, sumLatencyMs: latencyMs })
+ }
+ }
+
+ /**
+ * Session-only debug info for the Session Relays settings tab: working/striked preset relays and scored random relays.
+ */
+ getSessionRelayDebug(): {
+ strikedUrls: string[]
+ scoredRelays: { url: string; successCount: number; avgLatencyMs: number }[]
+ presetWorking: string[]
+ presetStriked: string[]
+ } {
+ const presetSet = new Set()
+ for (const u of [...FAST_WRITE_RELAY_URLS, ...BIG_RELAY_URLS]) {
+ const n = normalizeUrl(u) || u
+ if (n) presetSet.add(n)
+ }
+ const preset = Array.from(presetSet)
+ const strikedUrls = Array.from(this.publishStrikeCount.entries())
+ .filter(([, count]) => count >= ClientService.PUBLISH_STRIKES_THRESHOLD)
+ .map(([url]) => url)
+ const presetStriked = preset.filter((url) => (this.publishStrikeCount.get(url) ?? 0) >= ClientService.PUBLISH_STRIKES_THRESHOLD)
+ const presetWorking = preset.filter((url) => (this.publishStrikeCount.get(url) ?? 0) < ClientService.PUBLISH_STRIKES_THRESHOLD)
+ const scoredRelays = Array.from(this.sessionRelayPublishStats.entries()).map(([url, s]) => ({
+ url,
+ successCount: s.successCount,
+ avgLatencyMs: Math.round(s.sumLatencyMs / s.successCount)
+ }))
+ scoredRelays.sort((a, b) => a.avgLatencyMs - b.avgLatencyMs)
+ return { strikedUrls, scoredRelays, presetWorking, presetStriked }
+ }
+
+ /**
+ * From a list of candidate relay URLs (e.g. public lively), return up to `count` relays,
+ * preferring those that have succeeded and been fast this session. Excludes 3-strike and read-only relays.
+ */
+ getPreferredRelaysForRandom(candidateUrls: string[], count: number): string[] {
+ const readOnlySet = new Set(READ_ONLY_RELAY_URLS.map((u) => normalizeUrl(u) || u))
+ const normalizedCandidates = candidateUrls
+ .map((u) => normalizeUrl(u) || u)
+ .filter((n) => n && !readOnlySet.has(n))
+ const unique = Array.from(new Set(normalizedCandidates))
+ const notStruckOut = unique.filter((n) => (this.publishStrikeCount.get(n) ?? 0) < ClientService.PUBLISH_STRIKES_THRESHOLD)
+ const preferred: string[] = []
+ const rest: string[] = []
+ for (const url of notStruckOut) {
+ const stats = this.sessionRelayPublishStats.get(url)
+ if (stats && stats.successCount >= 1) preferred.push(url)
+ else rest.push(url)
+ }
+ preferred.sort((a, b) => {
+ const sa = this.sessionRelayPublishStats.get(a)!
+ const sb = this.sessionRelayPublishStats.get(b)!
+ const avgA = sa.sumLatencyMs / sa.successCount
+ const avgB = sb.sumLatencyMs / sb.successCount
+ return avgA - avgB
+ })
+ const result: string[] = []
+ let pi = 0
+ let ri = 0
+ const shuffledRest = rest.slice().sort(() => Math.random() - 0.5)
+ while (result.length < count && (pi < preferred.length || ri < shuffledRest.length)) {
+ if (pi < preferred.length) {
+ result.push(preferred[pi++])
+ } else if (ri < shuffledRest.length) {
+ result.push(shuffledRest[ri++])
+ }
+ }
+ return result.slice(0, count)
+ }
+
async publishEvent(relayUrls: string[], event: NEvent) {
+ const readOnlySet = new Set(READ_ONLY_RELAY_URLS.map((u) => normalizeUrl(u) || u))
+ const kind1BlockedSet = new Set(KIND_1_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u))
+ let filtered = relayUrls.filter((url) => {
+ const n = normalizeUrl(url) || url
+ if (readOnlySet.has(n)) return false
+ if (event.kind === kinds.ShortTextNote && kind1BlockedSet.has(n)) return false
+ const strikes = this.publishStrikeCount.get(n) ?? 0
+ if (strikes >= ClientService.PUBLISH_STRIKES_THRESHOLD) return false
+ return true
+ })
+ filtered = Array.from(new Set(filtered))
+
logger.debug('[PublishEvent] Starting publishEvent', {
eventId: event.id?.substring(0, 8),
kind: event.kind,
- relayCount: relayUrls.length
+ relayCount: filtered.length,
+ skippedStrikes: relayUrls.length - filtered.length
})
-
- const uniqueRelayUrls = Array.from(new Set(relayUrls))
+
+ const uniqueRelayUrls = filtered
if (event.kind === kinds.RelayList || event.kind === ExtendedKind.FAVORITE_RELAYS) {
logger.info('[PublishEvent] Publishing event to relays', {
eventId: event.id?.substring(0, 8),
@@ -383,6 +506,8 @@ class ClientService extends EventTarget {
const relayStatuses: { url: string; success: boolean; error?: string }[] = []
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
+ const client = this
return new Promise<{ success: boolean; relayStatuses: typeof relayStatuses; successCount: number; totalCount: number }>((resolve) => {
let successCount = 0
let finishedCount = 0
@@ -418,6 +543,7 @@ class ClientService extends EventTarget {
// Ensure we resolve even if not all relays finished
if (!hasResolved) {
hasResolved = true
+ client.recordPublishFailures(relayStatuses)
logger.debug('[PublishEvent] Resolving due to timeout', {
success: successCount >= uniqueRelayUrls.length / 3,
successCount,
@@ -436,6 +562,7 @@ class ClientService extends EventTarget {
logger.debug('[PublishEvent] Starting Promise.allSettled for all relays')
Promise.allSettled(
uniqueRelayUrls.map(async (url, index) => {
+ const startMs = Date.now()
logger.debug(`[PublishEvent] Starting relay ${index + 1}/${uniqueRelayUrls.length}`, { url })
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this
@@ -480,6 +607,7 @@ class ClientService extends EventTarget {
.publish(event)
.then(() => {
logger.debug(`[PublishEvent] Successfully published to relay`, { url })
+ that.recordPublishSuccess(url, Date.now() - startMs)
this.trackEventSeenOn(event.id, relay)
successCount++
relayStatuses.push({ url, success: true })
@@ -500,6 +628,7 @@ class ClientService extends EventTarget {
})
.then(() => {
logger.debug(`[PublishEvent] Successfully published after auth`, { url })
+ that.recordPublishSuccess(url, Date.now() - startMs)
this.trackEventSeenOn(event.id, relay)
successCount++
relayStatuses.push({ url, success: true })
@@ -548,6 +677,7 @@ class ClientService extends EventTarget {
}
if (currentFinished >= uniqueRelayUrls.length && !hasResolved) {
hasResolved = true
+ client.recordPublishFailures(relayStatuses)
logger.debug('[PublishEvent] All relays finished, resolving', {
success: successCount >= uniqueRelayUrls.length / 3,
successCount,
@@ -570,6 +700,7 @@ class ClientService extends EventTarget {
setTimeout(() => {
if (!hasResolved) {
hasResolved = true
+ client.recordPublishFailures(relayStatuses)
logger.debug('[PublishEvent] Resolving early with enough successes', {
success: true,
successCount,
@@ -765,9 +896,15 @@ class ClientService extends EventTarget {
onAllClose?: (reasons: string[]) => void
}
) {
- const relays = Array.from(new Set(urls))
+ let relays = Array.from(new Set(urls))
const filters = Array.isArray(filter) ? filter : [filter]
+ const hasKind1 = filters.some((f) => f.kinds && (Array.isArray(f.kinds) ? f.kinds.includes(1) : f.kinds === 1))
+ if (hasKind1 && KIND_1_BLOCKED_RELAY_URLS.length > 0) {
+ const kind1BlockedSet = new Set(KIND_1_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u))
+ relays = relays.filter((url) => !kind1BlockedSet.has(normalizeUrl(url) || url))
+ }
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this
const _knownIds = new Set()
@@ -1318,9 +1455,16 @@ class ClientService extends EventTarget {
globalTimeout?: number
} = {}
) {
- const relays = Array.from(new Set(urls))
+ let relays = Array.from(new Set(urls))
+ if (relays.length === 0) relays = [...BIG_RELAY_URLS]
+ const filters = Array.isArray(filter) ? filter : [filter]
+ const hasKind1 = filters.some((f) => f.kinds && (Array.isArray(f.kinds) ? f.kinds.includes(1) : f.kinds === 1))
+ if (hasKind1 && KIND_1_BLOCKED_RELAY_URLS.length > 0) {
+ const kind1BlockedSet = new Set(KIND_1_BLOCKED_RELAY_URLS.map((u) => normalizeUrl(u) || u))
+ relays = relays.filter((url) => !kind1BlockedSet.has(normalizeUrl(url) || url))
+ }
const events = await this.query(
- relays.length > 0 ? relays : BIG_RELAY_URLS,
+ relays,
filter,
onevent,
{ eoseTimeout, globalTimeout }
@@ -1334,27 +1478,29 @@ class ClientService extends EventTarget {
}
async fetchEvent(id: string): Promise {
- if (!/^[0-9a-f]{64}$/.test(id)) {
- let eventId: string | undefined
+ let hexId: string | undefined
+ if (/^[0-9a-f]{64}$/.test(id)) {
+ hexId = id
+ } else {
const { type, data } = nip19.decode(id)
switch (type) {
case 'note':
- eventId = data
+ hexId = data
break
case 'nevent':
- eventId = data.id
+ hexId = data.id
break
case 'naddr':
break
}
- if (eventId) {
- const cache = this.eventCacheMap.get(eventId)
- if (cache) {
- return cache
- }
- }
}
- return this.eventDataLoader.load(id)
+ if (hexId) {
+ const fromSession = this.sessionEventCache.get(hexId)
+ if (fromSession) return fromSession
+ const cachedPromise = this.eventCacheMap.get(hexId)
+ if (cachedPromise) return cachedPromise
+ }
+ return this.eventDataLoader.load(hexId ?? id)
}
addEventToCache(event: NEvent) {
@@ -1362,6 +1508,7 @@ class ClientService extends EventTarget {
const cleanEvent = { ...event } as NEvent
delete (cleanEvent as any).relayStatuses
+ this.sessionEventCache.set(cleanEvent.id, cleanEvent)
this.eventDataLoader.prime(cleanEvent.id, Promise.resolve(cleanEvent))
// Replaceable events are not stored in memory; they go to IndexedDB via putReplaceableEvent elsewhere
}
@@ -1815,6 +1962,7 @@ class ClientService extends EventTarget {
clearInMemoryCaches(): void {
this.relayListRequestCache.clear()
this.eventDataLoader.clearAll()
+ this.sessionEventCache.clear()
this.replaceableEventFromBigRelaysDataloader.clearAll()
this.followingFavoriteRelaysCache?.clear()
logger.info('[ClientService] In-memory caches cleared')
@@ -2057,7 +2205,26 @@ class ClientService extends EventTarget {
const eventsMap = new Map()
await Promise.allSettled(
Array.from(groups.entries()).map(async ([kind, pubkeys]) => {
- const events = await this.query(BIG_RELAY_URLS, {
+ // Profiles (kind 0) and relay lists (10002): use broader relay set + current user's inboxes if logged in
+ let relayUrls: string[]
+ if (kind === kinds.Metadata || kind === kinds.RelayList) {
+ const base = Array.from(new Set([...BIG_RELAY_URLS, ...PROFILE_FETCH_RELAY_URLS]))
+ if (this.pubkey) {
+ const userRelayEvent = await indexedDb.getReplaceableEvent(this.pubkey, kinds.RelayList)
+ if (userRelayEvent) {
+ const list = getRelayListFromEvent(userRelayEvent)
+ const read = (list?.read ?? []).map((u) => normalizeUrl(u)).filter(Boolean) as string[]
+ relayUrls = Array.from(new Set([...base, ...read]))
+ } else {
+ relayUrls = base
+ }
+ } else {
+ relayUrls = base
+ }
+ } else {
+ relayUrls = BIG_RELAY_URLS
+ }
+ const events = await this.query(relayUrls, {
authors: pubkeys,
kinds: [kind]
})
diff --git a/src/services/local-storage.service.ts b/src/services/local-storage.service.ts
index 69a0840f..a60970b6 100644
--- a/src/services/local-storage.service.ts
+++ b/src/services/local-storage.service.ts
@@ -19,6 +19,7 @@ import {
TNoteListMode,
TNotificationStyle,
TRelaySet,
+ TTheme,
TThemeSetting,
} from '@/types'
import indexedDb from './indexed-db.service'
@@ -27,6 +28,8 @@ import indexedDb from './indexed-db.service'
const SETTINGS_KEYS = [
StorageKey.RELAY_SETS,
StorageKey.THEME_SETTING,
+ StorageKey.THEME,
+ StorageKey.ADD_CLIENT_TAG,
StorageKey.FONT_SIZE,
StorageKey.NOTE_LIST_MODE,
StorageKey.ACCOUNTS,
@@ -70,6 +73,8 @@ class LocalStorageService {
private relaySets: TRelaySet[] = []
private themeSetting: TThemeSetting = 'system'
+ private theme: TTheme = 'light'
+ private addClientTag: boolean = true
private fontSize: TFontSize = 'medium'
private accounts: TAccount[] = []
private currentAccount: TAccount | null = null
@@ -118,6 +123,10 @@ class LocalStorageService {
init() {
this.themeSetting =
(window.localStorage.getItem(StorageKey.THEME_SETTING) as TThemeSetting) ?? 'system'
+ const themeStr = window.localStorage.getItem(StorageKey.THEME) as TTheme | null
+ this.theme = themeStr === 'dark' || themeStr === 'light' ? themeStr : 'light'
+ const addClientTagStr = window.localStorage.getItem(StorageKey.ADD_CLIENT_TAG)
+ this.addClientTag = addClientTagStr === null ? true : addClientTagStr === 'true'
this.fontSize =
(window.localStorage.getItem(StorageKey.FONT_SIZE) as TFontSize) ?? 'medium'
const accountsStr = window.localStorage.getItem(StorageKey.ACCOUNTS)
@@ -389,10 +398,13 @@ class LocalStorageService {
window.localStorage.removeItem(StorageKey.FEED_TYPE)
}
- /** Persist a setting to both localStorage and IndexedDB (source of truth is IndexedDB). */
+ /** Persist a setting. Keys in SETTINGS_KEYS go only to IndexedDB; others use localStorage. */
private persistSetting(key: string, value: string): void {
+ if ((SETTINGS_KEYS as readonly string[]).includes(key)) {
+ indexedDb.setSetting(key, value).catch(() => {})
+ return
+ }
window.localStorage.setItem(key, value)
- indexedDb.setSetting(key, value).catch(() => {})
}
private initPromise: Promise | null = null
@@ -411,10 +423,18 @@ class LocalStorageService {
} else {
await this.migrateToIdb()
}
+ this.clearSettingsFromLocalStorage()
})()
return this.initPromise
}
+ /** Remove SETTINGS_KEYS from localStorage so we don't duplicate; source of truth is IndexedDB. */
+ private clearSettingsFromLocalStorage(): void {
+ for (const key of SETTINGS_KEYS) {
+ window.localStorage.removeItem(key)
+ }
+ }
+
private async migrateToIdb(): Promise {
for (const key of SETTINGS_KEYS) {
const value = window.localStorage.getItem(key)
@@ -427,6 +447,10 @@ class LocalStorageService {
if (get(StorageKey.THEME_SETTING) != null) {
this.themeSetting = (get(StorageKey.THEME_SETTING) as TThemeSetting) ?? this.themeSetting
}
+ const themeStr = get(StorageKey.THEME)
+ if (themeStr === 'dark' || themeStr === 'light') this.theme = themeStr
+ const addClientTagStr = get(StorageKey.ADD_CLIENT_TAG)
+ if (addClientTagStr != null) this.addClientTag = addClientTagStr === 'true'
if (get(StorageKey.FONT_SIZE) != null) {
this.fontSize = (get(StorageKey.FONT_SIZE) as TFontSize) ?? this.fontSize
}
@@ -527,6 +551,24 @@ class LocalStorageService {
this.themeSetting = themeSetting
}
+ getTheme(): TTheme {
+ return this.theme
+ }
+
+ setTheme(theme: TTheme) {
+ this.theme = theme
+ this.persistSetting(StorageKey.THEME, theme)
+ }
+
+ getAddClientTag(): boolean {
+ return this.addClientTag
+ }
+
+ setAddClientTag(value: boolean) {
+ this.addClientTag = value
+ this.persistSetting(StorageKey.ADD_CLIENT_TAG, value.toString())
+ }
+
getFontSize() {
return this.fontSize
}
diff --git a/src/services/navigation.service.ts b/src/services/navigation.service.ts
index e574205c..7a747529 100644
--- a/src/services/navigation.service.ts
+++ b/src/services/navigation.service.ts
@@ -220,6 +220,7 @@ export class NavigationService {
if (viewType === 'settings-sub') {
if (pathname.includes('/general')) return 'General Settings'
if (pathname.includes('/relays')) return 'Relays and Storage Settings'
+ if (pathname.includes('/cache')) return 'Cache & offline storage'
if (pathname.includes('/wallet')) return 'Wallet Settings'
if (pathname.includes('/posts')) return 'Post Settings'
if (pathname.includes('/translation')) return 'Translation Settings'
diff --git a/src/services/relay-selection.service.ts b/src/services/relay-selection.service.ts
index b332951a..106d28e4 100644
--- a/src/services/relay-selection.service.ts
+++ b/src/services/relay-selection.service.ts
@@ -140,7 +140,7 @@ class RelaySelectionService {
openFrom.forEach((url) => addRelay(url, 'open_from'))
}
- // Random relays: always add 3 random public lively relays to the list; selected by default only when setting is ON
+ // Random relays: prefer session-proven fast relays, then fill with random from rest (selection only random between sessions)
const randomRelayUrls: string[] = []
if (typeof window !== 'undefined') {
try {
@@ -150,8 +150,8 @@ class RelaySelectionService {
const n = normalizeUrl(u) || u
return !existing.has(n)
})
- const shuffled = candidates.slice().sort(() => Math.random() - 0.5)
- shuffled.slice(0, 3).forEach((url) => {
+ const preferred = client.getPreferredRelaysForRandom(candidates, 3)
+ preferred.forEach((url) => {
const normalized = normalizeUrl(url) || url
addRelay(normalized, 'randomly_selected')
randomRelayUrls.push(normalized)