Browse Source

quiet the console

and fix relay list
imwald
Silberengel 3 weeks ago
parent
commit
ed2ca2a7e9
  1. 141
      src/components/PostEditor/PostRelaySelector.tsx
  2. 150
      src/lib/logger.ts
  3. 69
      src/services/relay-selection.service.ts

141
src/components/PostEditor/PostRelaySelector.tsx

@ -17,7 +17,7 @@ import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import indexedDb from '@/services/indexed-db.service' import indexedDb from '@/services/indexed-db.service'
import { Check, ChevronDown, Server } from 'lucide-react' import { Check, ChevronDown, Server } from 'lucide-react'
import { NostrEvent } from 'nostr-tools' import { NostrEvent } from 'nostr-tools'
import { Dispatch, SetStateAction, useCallback, useEffect, useState, useMemo } from 'react' import { Dispatch, SetStateAction, useCallback, useEffect, useState, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import RelayIcon from '../RelayIcon' import RelayIcon from '../RelayIcon'
import relaySelectionService, { type RelaySourceType } from '@/services/relay-selection.service' import relaySelectionService, { type RelaySourceType } from '@/services/relay-selection.service'
@ -69,12 +69,9 @@ export default function PostRelaySelector({
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [hasManualSelection, setHasManualSelection] = useState(false) const [hasManualSelection, setHasManualSelection] = useState(false)
const [previousSelectableCount, setPreviousSelectableCount] = useState(0) const [previousSelectableCount, setPreviousSelectableCount] = useState(0)
const [previousMentions, setPreviousMentions] = useState<string[]>([]) // Generation counter: incremented every time the effect fires; async callback checks whether
// it's still the latest invocation before committing state, preventing stale races.
// Initialize previousMentions with the initial mentions value const selectionGenRef = useRef(0)
useEffect(() => {
setPreviousMentions(mentions)
}, []) // Only run once on mount
// For discussion replies, content doesn't affect relay selection // For discussion replies, content doesn't affect relay selection
// Check if this is a reply to a discussion by looking for "K" tag with "11" // Check if this is a reply to a discussion by looking for "K" tag with "11"
@ -181,28 +178,26 @@ export default function PostRelaySelector({
const memoizedRelaySets = useMemo(() => relaySets, [relaySets]) const memoizedRelaySets = useMemo(() => relaySets, [relaySets])
const memoizedOpenFrom = useMemo(() => openFrom, [openFrom]) const memoizedOpenFrom = useMemo(() => openFrom, [openFrom])
// Use centralized relay selection service - only for non-content dependencies // Single relay-selection effect. The generation counter (selectionGenRef) guards against
// stale async completions: if a newer invocation has started, the older one discards its results.
useEffect(() => { useEffect(() => {
const gen = ++selectionGenRef.current
const updateRelaySelection = async () => { const updateRelaySelection = async () => {
setIsLoading(true) setIsLoading(true)
try { try {
// Ensure cache relays (kind 10432) are included in userWriteRelays even if relayList hasn't been updated yet
// Get cache relays directly from IndexedDB (don't fetch new every time)
let userWriteRelays = relayList?.write || [] let userWriteRelays = relayList?.write || []
if (pubkey) { if (pubkey) {
try { try {
const cacheRelayListEvent = await indexedDb.getReplaceableEvent(pubkey, ExtendedKind.CACHE_RELAYS) const cacheRelayListEvent = await indexedDb.getReplaceableEvent(pubkey, ExtendedKind.CACHE_RELAYS)
if (cacheRelayListEvent) { if (cacheRelayListEvent) {
const cacheRelayList = getRelayListFromEvent(cacheRelayListEvent) const cacheRelayList = getRelayListFromEvent(cacheRelayListEvent)
// Get all cache relays (they should all be local network URLs)
// Include both write and both-scoped relays (cache relays should be write-capable)
const cacheRelays = [ const cacheRelays = [
...cacheRelayList.write, ...cacheRelayList.write,
...cacheRelayList.originalRelays ...cacheRelayList.originalRelays
.filter(relay => (relay.scope === 'both' || relay.scope === 'write') && isLocalNetworkUrl(relay.url)) .filter(relay => (relay.scope === 'both' || relay.scope === 'write') && isLocalNetworkUrl(relay.url))
.map(relay => relay.url) .map(relay => relay.url)
].filter(url => { ].filter(url => {
// Filter out invalid/empty URLs
if (!url || typeof url !== 'string' || url.trim() === '' || url === 'ws://' || url === 'wss://') return false if (!url || typeof url !== 'string' || url.trim() === '' || url === 'ws://' || url === 'wss://') return false
return isLocalNetworkUrl(url) return isLocalNetworkUrl(url)
}) })
@ -228,12 +223,15 @@ export default function PostRelaySelector({
relaySets: memoizedRelaySets, relaySets: memoizedRelaySets,
parentEvent: _parentEvent, parentEvent: _parentEvent,
isPublicMessage, isPublicMessage,
content: isDiscussionReply ? '' : postContent, // Don't use content for discussion replies content: isDiscussionReply ? '' : postContent,
mentions: isPublicMessage ? mentions : undefined, // Pass mentions for PMs mentions: isPublicMessage ? mentions : undefined,
userPubkey: pubkey || undefined, userPubkey: pubkey || undefined,
openFrom: memoizedOpenFrom openFrom: memoizedOpenFrom
}) })
// Discard results from a superseded invocation
if (gen !== selectionGenRef.current) return
const newSelectableCount = result.selectableRelays.length const newSelectableCount = result.selectableRelays.length
const selectableRelaysChanged = newSelectableCount !== previousSelectableCount const selectableRelaysChanged = newSelectableCount !== previousSelectableCount
@ -241,22 +239,17 @@ export default function PostRelaySelector({
setRelayTypes(result.relayTypes ?? {}) setRelayTypes(result.relayTypes ?? {})
setPreviousSelectableCount(newSelectableCount) setPreviousSelectableCount(newSelectableCount)
// Only update selected relays if:
// 1. User hasn't manually modified them, OR
// 2. Selectable relays changed
if (!hasManualSelection || selectableRelaysChanged) { if (!hasManualSelection || selectableRelaysChanged) {
// Ensure cache relays are included by default (but user can uncheck them)
const cacheRelays = result.selectableRelays.filter(url => isLocalNetworkUrl(url)) const cacheRelays = result.selectableRelays.filter(url => isLocalNetworkUrl(url))
const selectedWithCache = Array.from(new Set([...result.selectedRelays, ...cacheRelays])) const selectedWithCache = Array.from(new Set([...result.selectedRelays, ...cacheRelays]))
setSelectedRelayUrls(selectedWithCache) setSelectedRelayUrls(selectedWithCache)
setDescription(describeRelaySelection(selectedWithCache)) setDescription(describeRelaySelection(selectedWithCache))
// Reset manual selection flag if relays changed
if (selectableRelaysChanged && hasManualSelection) { if (selectableRelaysChanged && hasManualSelection) {
setHasManualSelection(false) setHasManualSelection(false)
} }
} }
} catch (error) { } catch (error) {
if (gen !== selectionGenRef.current) return
logger.error('Failed to update relay selection', { error }) logger.error('Failed to update relay selection', { error })
setSelectableRelays([]) setSelectableRelays([])
if (!hasManualSelection) { if (!hasManualSelection) {
@ -264,7 +257,7 @@ export default function PostRelaySelector({
setDescription(t('No relays selected')) setDescription(t('No relays selected'))
} }
} finally { } finally {
setIsLoading(false) if (gen === selectionGenRef.current) setIsLoading(false)
} }
} }
@ -285,110 +278,6 @@ export default function PostRelaySelector({
t t
]) ])
// Separate effect for mention changes in non-discussion replies
useEffect(() => {
if (isDiscussionReply) return // Skip for discussion replies
const mentionsChanged = JSON.stringify(mentions) !== JSON.stringify(previousMentions)
if (mentionsChanged) {
setPreviousMentions(mentions)
// Update relay selection when mentions change
const updateRelaySelection = async () => {
setIsLoading(true)
try {
// Ensure cache relays (kind 10432) are included in userWriteRelays even if relayList hasn't been updated yet
// Get cache relays directly from IndexedDB (don't fetch new every time)
let userWriteRelays = relayList?.write || []
if (pubkey) {
try {
const cacheRelayListEvent = await indexedDb.getReplaceableEvent(pubkey, ExtendedKind.CACHE_RELAYS)
if (cacheRelayListEvent) {
const cacheRelayList = getRelayListFromEvent(cacheRelayListEvent)
// Get all cache relays (they should all be local network URLs)
// Include both write and both-scoped relays (cache relays should be write-capable)
const cacheRelays = [
...cacheRelayList.write,
...cacheRelayList.originalRelays
.filter(relay => (relay.scope === 'both' || relay.scope === 'write') && isLocalNetworkUrl(relay.url))
.map(relay => relay.url)
].filter(url => isLocalNetworkUrl(url))
const existingUrls = new Set(userWriteRelays.map(url => normalizeUrl(url) || url))
const newCacheRelays = cacheRelays
.map(url => normalizeUrl(url) || url)
.filter((url): url is string => !!url && !existingUrls.has(url))
if (newCacheRelays.length > 0) {
userWriteRelays = [...newCacheRelays, ...userWriteRelays]
}
}
} catch (error) {
logger.warn('Failed to get cache relays from IndexedDB', { error, pubkey })
}
}
const result = await relaySelectionService.selectRelays({
userWriteRelays,
userHttpWriteRelays: relayList?.httpWrite ?? [],
userReadRelays: userReadRelaysForSelection,
favoriteRelays: memoizedFavoriteRelays,
blockedRelays: memoizedBlockedRelays,
relaySets: memoizedRelaySets,
parentEvent: _parentEvent,
isPublicMessage,
content: isDiscussionReply ? '' : postContent, // Don't use content for discussion replies
mentions: isPublicMessage ? mentions : undefined, // Pass mentions for PMs
userPubkey: pubkey || undefined,
openFrom: memoizedOpenFrom
})
const newSelectableCount = result.selectableRelays.length
const selectableRelaysChanged = newSelectableCount !== previousSelectableCount
setSelectableRelays(result.selectableRelays)
setRelayTypes(result.relayTypes ?? {})
setPreviousSelectableCount(newSelectableCount)
// Only update selected relays if:
// 1. User hasn't manually modified them, OR
// 2. Selectable relays changed
if (!hasManualSelection || selectableRelaysChanged) {
// Ensure cache relays are included by default (but user can uncheck them)
const cacheRelays = result.selectableRelays.filter(url => isLocalNetworkUrl(url))
const selectedWithCache = Array.from(new Set([...result.selectedRelays, ...cacheRelays]))
setSelectedRelayUrls(selectedWithCache)
setDescription(describeRelaySelection(selectedWithCache))
// Reset manual selection flag if relays changed
if (selectableRelaysChanged && hasManualSelection) {
setHasManualSelection(false)
}
}
} catch (error) {
logger.error('Failed to update relay selection', { error })
} finally {
setIsLoading(false)
}
}
updateRelaySelection()
}
}, [
mentions,
isDiscussionReply,
memoizedFavoriteRelays,
memoizedBlockedRelays,
memoizedRelaySets,
_parentEvent,
isPublicMessage,
pubkey,
relayList,
memoizedOpenFrom,
previousSelectableCount,
hasManualSelection,
describeRelaySelection
])
// Update description when selected relays change due to manual selection // Update description when selected relays change due to manual selection
useEffect(() => { useEffect(() => {
if (hasManualSelection && !isLoading) { if (hasManualSelection && !isLoading) {

150
src/lib/logger.ts

@ -1,149 +1,127 @@
/** /**
* Centralized logging utility to reduce console noise and improve performance * Centralized logging utility.
* *
* Usage: * Level matrix:
* - Use logger.debug() for development debugging (only shows in dev mode) * dev + debug flag debug / info / warn / error (full formatted output)
* - Use logger.info() for important information (always shows) * dev (no flag) info / warn / error (formatted, no stack)
* - Use logger.warn() for warnings (always shows) * production warn / error only (bare console no timestamp string built)
* - Use logger.error() for errors (always shows)
* *
* In production builds, debug logs are completely removed to improve performance. * Enable debug in dev: localStorage.setItem('jumble-debug', 'true') then reload.
*/ */
type LogLevel = 'debug' | 'info' | 'warn' | 'error' type LogLevel = 'debug' | 'info' | 'warn' | 'error'
interface LoggerConfig { const LEVELS: LogLevel[] = ['debug', 'info', 'warn', 'error']
level: LogLevel
enableDebug: boolean
enablePerformance: boolean
}
class Logger { class Logger {
private config: LoggerConfig private readonly isDev = import.meta.env.DEV
private enableDebug: boolean
private minLevel: LogLevel
constructor() { constructor() {
// In production, disable debug logging for better performance this.enableDebug =
const isDev = import.meta.env.DEV this.isDev &&
const isDebugEnabled =
isDev &&
(localStorage.getItem('imwald-debug') === 'true' || (localStorage.getItem('imwald-debug') === 'true' ||
localStorage.getItem('jumble-debug') === 'true' || localStorage.getItem('jumble-debug') === 'true' ||
import.meta.env.VITE_DEBUG === 'true') import.meta.env.VITE_DEBUG === 'true')
this.config = { // In production only warn/error reach the console — info is noise for end-users.
level: isDebugEnabled ? 'debug' : 'info', this.minLevel = this.enableDebug ? 'debug' : this.isDev ? 'info' : 'warn'
enableDebug: isDebugEnabled,
enablePerformance: isDev
}
} }
private shouldLog(level: LogLevel): boolean { private shouldLog(level: LogLevel): boolean {
const levels = ['debug', 'info', 'warn', 'error'] return LEVELS.indexOf(level) >= LEVELS.indexOf(this.minLevel)
const currentLevelIndex = levels.indexOf(this.config.level)
const messageLevelIndex = levels.indexOf(level)
return messageLevelIndex >= currentLevelIndex
} }
private getCallerInfo(): string { private getCallerInfo(): string {
const stack = new Error().stack const stack = new Error().stack
if (!stack) return 'unknown' if (!stack) return 'unknown'
for (const line of stack.split('\n').slice(3)) {
const lines = stack.split('\n') const m = line.match(/at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/)
// Skip the first 3 lines (Error, getCallerInfo, formatMessage) if (m) {
// Look for the first line that contains a file path const fileName = m[2].split('/').pop()?.replace(/\.[tj]sx?$/, '') ?? 'unknown'
for (let i = 3; i < lines.length; i++) { return `${fileName}:${m[1]}`
const line = lines[i]
const match = line.match(/at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/)
if (match) {
const [, functionName, filePath] = match
const fileName = filePath.split('/').pop()?.replace('.tsx', '').replace('.ts', '') || 'unknown'
return `${fileName}:${functionName}`
} }
} }
return 'unknown' return 'unknown'
} }
private formatMessage(level: LogLevel, message: string, ...args: any[]): [string, ...any[]] { private prefix(level: LogLevel): string {
const timestamp = new Date().toISOString().substring(11, 23) // HH:mm:ss.SSS const ts = new Date().toISOString().substring(11, 23)
// Stack capture is expensive (main-thread jank, especially on mobile). Only when deep debug is on. const caller = this.enableDebug ? ` [${this.getCallerInfo()}]` : ''
const caller = this.config.enableDebug ? this.getCallerInfo() : '' return `[${ts}] [${level.toUpperCase()}]${caller}`
const prefix = caller
? `[${timestamp}] [${level.toUpperCase()}] [${caller}]`
: `[${timestamp}] [${level.toUpperCase()}]`
return [`${prefix} ${message}`, ...args]
} }
debug(message: string, ...args: any[]): void { debug(message: string, ...args: unknown[]): void {
if (!this.config.enableDebug || !this.shouldLog('debug')) return if (!this.enableDebug) return
console.log(...this.formatMessage('debug', message, ...args)) console.log(`${this.prefix('debug')} ${message}`, ...args)
} }
info(message: string, ...args: any[]): void { info(message: string, ...args: unknown[]): void {
if (!this.shouldLog('info')) return if (!this.shouldLog('info')) return
console.log(...this.formatMessage('info', message, ...args)) // In production this branch is never reached (minLevel === 'warn').
console.log(`${this.prefix('info')} ${message}`, ...args)
} }
warn(message: string, ...args: any[]): void { warn(message: string, ...args: unknown[]): void {
if (!this.shouldLog('warn')) return if (!this.shouldLog('warn')) return
console.warn(...this.formatMessage('warn', message, ...args)) if (this.isDev) {
console.warn(`${this.prefix('warn')} ${message}`, ...args)
} else {
// In production: no string-building overhead; browser devtools add their own timestamp.
console.warn(message, ...args)
}
} }
error(message: string, ...args: any[]): void { error(message: string, ...args: unknown[]): void {
if (!this.shouldLog('error')) return if (!this.shouldLog('error')) return
console.error(...this.formatMessage('error', message, ...args)) if (this.isDev) {
console.error(`${this.prefix('error')} ${message}`, ...args)
} else {
console.error(message, ...args)
}
} }
// Performance logging for development /** Dev-only performance marker. */
perf(message: string, ...args: any[]): void { perf(message: string, ...args: unknown[]): void {
if (!this.config.enablePerformance) return if (!this.isDev) return
console.log(`[PERF] ${message}`, ...args) console.log(`[PERF] ${message}`, ...args)
} }
// Group logging for related operations /** Run `fn` inside a console group (debug mode only). */
group(label: string, fn: () => void): void { group(label: string, fn: () => void): void {
if (!this.config.enableDebug) { if (!this.enableDebug) { fn(); return }
fn()
return
}
console.group(label) console.group(label)
fn() fn()
console.groupEnd() console.groupEnd()
} }
// Conditional logging based on environment /** Raw dev-only log — no formatting. */
dev(message: string, ...args: any[]): void { dev(message: string, ...args: unknown[]): void {
if (import.meta.env.DEV) { if (this.isDev) console.log(message, ...args)
console.log(message, ...args)
}
} }
// Enable/disable debug mode at runtime
setDebugMode(enabled: boolean): void { setDebugMode(enabled: boolean): void {
this.config.enableDebug = enabled this.enableDebug = enabled
this.config.level = enabled ? 'debug' : 'info' this.minLevel = enabled ? 'debug' : this.isDev ? 'info' : 'warn'
localStorage.setItem('imwald-debug', enabled.toString()) localStorage.setItem('imwald-debug', String(enabled))
localStorage.setItem('jumble-debug', enabled.toString()) localStorage.setItem('jumble-debug', String(enabled))
} }
// Check if debug mode is enabled
isDebugEnabled(): boolean { isDebugEnabled(): boolean {
return this.config.enableDebug return this.enableDebug
} }
// Context-aware logging for components /** Component-scoped debug log (debug mode only). */
component(componentName: string, message: string, ...args: any[]): void { component(componentName: string, message: string, ...args: unknown[]): void {
if (!this.config.enableDebug) return if (!this.enableDebug) return
const timestamp = new Date().toISOString().substring(11, 23) console.log(`${this.prefix('debug')} [${componentName}] ${message}`, ...args)
const caller = this.getCallerInfo()
console.log(`[${timestamp}] [COMPONENT] [${componentName}] [${caller}] ${message}`, ...args)
} }
// Performance logging with context /** Component-scoped perf log (dev only). */
perfComponent(componentName: string, operation: string, ...args: any[]): void { perfComponent(componentName: string, operation: string, ...args: unknown[]): void {
if (!this.config.enablePerformance) return if (!this.isDev) return
const timestamp = new Date().toISOString().substring(11, 23) console.log(`[PERF] [${componentName}] ${operation}`, ...args)
const caller = this.getCallerInfo()
console.log(`[${timestamp}] [PERF] [${componentName}] [${caller}] ${operation}`, ...args)
} }
} }

69
src/services/relay-selection.service.ts

@ -3,7 +3,7 @@ import { ExtendedKind, FAST_WRITE_RELAY_URLS, RANDOM_PUBLISH_RELAY_COUNT } from
import { NOSTR_URI_FOR_REPLY_PUBKEYS_REGEX } from '@/lib/content-patterns' import { NOSTR_URI_FOR_REPLY_PUBKEYS_REGEX } from '@/lib/content-patterns'
import client from '@/services/client.service' import client from '@/services/client.service'
import { eventService } from '@/services/client.service' import { eventService } from '@/services/client.service'
import { normalizeAnyRelayUrl, normalizeUrl, isLocalNetworkUrl } from '@/lib/url' import { normalizeAnyRelayUrl, isLocalNetworkUrl } from '@/lib/url'
import { TRelaySet, TRelayList } from '@/types' import { TRelaySet, TRelayList } from '@/types'
import logger from '@/lib/logger' import logger from '@/lib/logger'
import indexedDb from '@/services/indexed-db.service' import indexedDb from '@/services/indexed-db.service'
@ -158,14 +158,14 @@ class RelaySelectionService {
const seenCand = new Set<string>() const seenCand = new Set<string>()
const candidates: string[] = [] const candidates: string[] = []
for (const u of [...sessionBoost, ...publicLively]) { for (const u of [...sessionBoost, ...publicLively]) {
const n = normalizeUrl(u) || u const n = normalizeAnyRelayUrl(u) || u
if (!n || existing.has(n) || seenCand.has(n)) continue if (!n || existing.has(n) || seenCand.has(n)) continue
seenCand.add(n) seenCand.add(n)
candidates.push(n) candidates.push(n)
} }
const preferred = client.getPreferredRelaysForRandom(candidates, RANDOM_PUBLISH_RELAY_COUNT) const preferred = client.getPreferredRelaysForRandom(candidates, RANDOM_PUBLISH_RELAY_COUNT)
preferred.forEach((url) => { preferred.forEach((url) => {
const normalized = normalizeUrl(url) || url const normalized = normalizeAnyRelayUrl(url) || url
addRelay(normalized, 'randomly_selected') addRelay(normalized, 'randomly_selected')
randomRelayUrls.push(normalized) randomRelayUrls.push(normalized)
}) })
@ -365,11 +365,11 @@ class RelaySelectionService {
let selectedRelays: string[] = [] let selectedRelays: string[] = []
const norm = (url: string) => normalizeAnyRelayUrl(url) || url
// If called with specific relay URLs, use those // If called with specific relay URLs, use those
if (openFrom && openFrom.length > 0) { if (openFrom && openFrom.length > 0) {
selectedRelays = openFrom.map(url => normalizeUrl(url) || url).filter(Boolean) selectedRelays = Array.from(new Set(openFrom.map(norm).filter(Boolean)))
// Deduplicate the selected relays
selectedRelays = Array.from(new Set(selectedRelays))
} }
// For discussion replies, use relay hints from the kind 11 + user's outboxes + local relays + thecitadel // For discussion replies, use relay hints from the kind 11 + user's outboxes + local relays + thecitadel
else if (parentEvent && (parentEvent.kind === ExtendedKind.DISCUSSION || parentEvent.kind === ExtendedKind.COMMENT)) { else if (parentEvent && (parentEvent.kind === ExtendedKind.DISCUSSION || parentEvent.kind === ExtendedKind.COMMENT)) {
@ -381,67 +381,46 @@ class RelaySelectionService {
} }
// For regular replies, use user's write relays + mention relays // For regular replies, use user's write relays + mention relays
else if (parentEvent && this.isRegularReply(parentEvent)) { else if (parentEvent && this.isRegularReply(parentEvent)) {
// Get user's write relays
const userRelays = userWriteRelays.length > 0 ? userWriteRelays : FAST_WRITE_RELAY_URLS const userRelays = userWriteRelays.length > 0 ? userWriteRelays : FAST_WRITE_RELAY_URLS
selectedRelays = userRelays.map(url => normalizeUrl(url) || url).filter(Boolean) selectedRelays = Array.from(new Set(userRelays.map(norm).filter(Boolean)))
// Deduplicate the selected relays
selectedRelays = Array.from(new Set(selectedRelays))
// Add mention relays // Add mention relays
if (userPubkey) { if (userPubkey) {
let mentions: string[] = [] let mentions: string[] = []
if (parentEvent) mentions.push(parentEvent.pubkey)
// Always include parent event author for replies
if (parentEvent) {
mentions.push(parentEvent.pubkey)
}
// Extract additional mentions from content if available
if (content) { if (content) {
const contentMentions = await this.extractMentions(content, parentEvent) const contentMentions = await this.extractMentions(content, parentEvent)
mentions = [...new Set([...mentions, ...contentMentions])] // deduplicate mentions = [...new Set([...mentions, ...contentMentions])]
} }
const mentionedPubkeys = mentions.filter(p => p !== userPubkey) const mentionedPubkeys = mentions.filter(p => p !== userPubkey)
if (mentionedPubkeys.length > 0) { if (mentionedPubkeys.length > 0) {
const mentionRelayLists = await Promise.all( const mentionRelayLists = await Promise.all(
mentionedPubkeys.map(async (pubkey) => { mentionedPubkeys.map(async (pubkey) => {
try { try {
// Use cached version from IndexedDB instead of fetching from relays
const relayList = await this.getCachedRelayList(pubkey) const relayList = await this.getCachedRelayList(pubkey)
if (!relayList) return [] if (!relayList) return []
const userRelays = relayList.write || [] return this.filterLocalRelaysFromOthers(relayList.write || [])
// Filter out local relays from other users
return this.filterLocalRelaysFromOthers(userRelays)
} catch (error) { } catch (error) {
logger.warn('Failed to get cached relay list', { pubkey, error }) logger.warn('Failed to get cached relay list', { pubkey, error })
return [] return []
} }
}) })
) )
const mentionRelays = mentionRelayLists.flat().map(url => normalizeUrl(url) || url).filter(Boolean) const mentionRelays = mentionRelayLists.flat().map(norm).filter(Boolean)
selectedRelays = [...selectedRelays, ...mentionRelays] selectedRelays = Array.from(new Set([...selectedRelays, ...mentionRelays]))
// Deduplicate after adding mention relays
selectedRelays = Array.from(new Set(selectedRelays))
} }
} }
} }
// Default: user's write relays (or fallback to fast write relays if no user relays) // Default: user's write relays (or fallback to fast write relays if no user relays)
else { else {
const defaultRelays = userWriteRelays.length > 0 ? userWriteRelays : FAST_WRITE_RELAY_URLS const defaultRelays = userWriteRelays.length > 0 ? userWriteRelays : FAST_WRITE_RELAY_URLS
selectedRelays = defaultRelays.map(url => normalizeUrl(url) || url).filter(Boolean) selectedRelays = Array.from(new Set(defaultRelays.map(norm).filter(Boolean)))
// Deduplicate the selected relays
selectedRelays = Array.from(new Set(selectedRelays))
} }
// ALWAYS include cache relays (local network relays) in selected relays // ALWAYS include cache relays (local network relays) in selected relays
// Cache relays are important for offline functionality
const cacheRelays = userWriteRelays.filter(url => isLocalNetworkUrl(url)) const cacheRelays = userWriteRelays.filter(url => isLocalNetworkUrl(url))
if (cacheRelays.length > 0) { if (cacheRelays.length > 0) {
selectedRelays = [...selectedRelays, ...cacheRelays] selectedRelays = Array.from(new Set([...selectedRelays, ...cacheRelays.map(norm).filter(Boolean)]))
// Deduplicate after adding cache relays
selectedRelays = Array.from(new Set(selectedRelays))
} }
// When "add random relays" setting is ON, include random relays in selected by default; when OFF they are still in the list but unchecked // When "add random relays" setting is ON, include random relays in selected by default; when OFF they are still in the list but unchecked
@ -491,7 +470,7 @@ class RelaySelectionService {
} }
senderRelays.forEach(url => { senderRelays.forEach(url => {
const normalized = normalizeUrl(url) const normalized = normalizeAnyRelayUrl(url)
if (normalized) { if (normalized) {
if (!relayToMembers.has(normalized)) { if (!relayToMembers.has(normalized)) {
relayToMembers.set(normalized, new Set()) relayToMembers.set(normalized, new Set())
@ -549,7 +528,7 @@ class RelaySelectionService {
recipientRelayLists.forEach((relays, index) => { recipientRelayLists.forEach((relays, index) => {
const pubkey = recipientPubkeys[index] const pubkey = recipientPubkeys[index]
relays.forEach(url => { relays.forEach(url => {
const normalized = normalizeUrl(url) const normalized = normalizeAnyRelayUrl(url)
if (normalized) { if (normalized) {
if (!relayToMembers.has(normalized)) { if (!relayToMembers.has(normalized)) {
relayToMembers.set(normalized, new Set()) relayToMembers.set(normalized, new Set())
@ -615,7 +594,7 @@ class RelaySelectionService {
// Normalize and deduplicate final list // Normalize and deduplicate final list
const normalizedRelays = relays const normalizedRelays = relays
.map(url => normalizeUrl(url)) .map(url => normalizeAnyRelayUrl(url))
.filter((url): url is string => !!url) .filter((url): url is string => !!url)
return Array.from(new Set(normalizedRelays)) return Array.from(new Set(normalizedRelays))
@ -623,7 +602,7 @@ class RelaySelectionService {
logger.error('Failed to get public message relays', { error, parentEvent: context.parentEvent?.id }) logger.error('Failed to get public message relays', { error, parentEvent: context.parentEvent?.id })
// Fallback to sender's write relays // Fallback to sender's write relays
const senderRelays = userWriteRelays.length > 0 ? userWriteRelays : FAST_WRITE_RELAY_URLS const senderRelays = userWriteRelays.length > 0 ? userWriteRelays : FAST_WRITE_RELAY_URLS
return senderRelays.map(url => normalizeUrl(url) || url).filter(Boolean) return senderRelays.map(url => normalizeAnyRelayUrl(url) || url).filter(Boolean)
} }
} }
@ -642,7 +621,7 @@ class RelaySelectionService {
*/ */
private getDiscussionRelayHints(discussionEventId: string): string[] { private getDiscussionRelayHints(discussionEventId: string): string[] {
const eventHints = client.getEventHints(discussionEventId) const eventHints = client.getEventHints(discussionEventId)
return eventHints.map(url => normalizeUrl(url) || url).filter(Boolean) return eventHints.map(url => normalizeAnyRelayUrl(url) || url).filter(Boolean)
} }
/** /**
@ -682,7 +661,7 @@ class RelaySelectionService {
} }
// Step 2: Add wss://thecitadel.nostr1.com // Step 2: Add wss://thecitadel.nostr1.com
const thecitadelUrl = normalizeUrl('wss://thecitadel.nostr1.com') const thecitadelUrl = normalizeAnyRelayUrl('wss://thecitadel.nostr1.com')
if (thecitadelUrl) { if (thecitadelUrl) {
relayUrls.add(thecitadelUrl) relayUrls.add(thecitadelUrl)
} }
@ -690,7 +669,7 @@ class RelaySelectionService {
// Step 3: Add user's outboxes (write relays from kind 10002) // Step 3: Add user's outboxes (write relays from kind 10002)
if (userWriteRelays.length > 0) { if (userWriteRelays.length > 0) {
userWriteRelays.forEach(url => { userWriteRelays.forEach(url => {
const normalized = normalizeUrl(url) const normalized = normalizeAnyRelayUrl(url)
if (normalized) { if (normalized) {
relayUrls.add(normalized) relayUrls.add(normalized)
} }
@ -701,7 +680,7 @@ class RelaySelectionService {
const relayList = await this.getCachedRelayList(userPubkey) const relayList = await this.getCachedRelayList(userPubkey)
if (relayList?.write) { if (relayList?.write) {
relayList.write.forEach(url => { relayList.write.forEach(url => {
const normalized = normalizeUrl(url) const normalized = normalizeAnyRelayUrl(url)
if (normalized) { if (normalized) {
relayUrls.add(normalized) relayUrls.add(normalized)
} }
@ -719,7 +698,7 @@ class RelaySelectionService {
if (cacheRelayEvent) { if (cacheRelayEvent) {
cacheRelayEvent.tags.forEach(tag => { cacheRelayEvent.tags.forEach(tag => {
if (tag[0] === 'relay' && tag[1]) { if (tag[0] === 'relay' && tag[1]) {
const normalized = normalizeUrl(tag[1]) const normalized = normalizeAnyRelayUrl(tag[1])
if (normalized) { if (normalized) {
relayUrls.add(normalized) relayUrls.add(normalized)
} }
@ -733,7 +712,7 @@ class RelaySelectionService {
// Step 5: Convert to array, normalize, and deduplicate // Step 5: Convert to array, normalize, and deduplicate
const normalizedRelays = Array.from(relayUrls) const normalizedRelays = Array.from(relayUrls)
.map(url => normalizeUrl(url)) .map(url => normalizeAnyRelayUrl(url))
.filter((url): url is string => !!url) .filter((url): url is string => !!url)
const deduplicatedRelays = Array.from(new Set(normalizedRelays)) const deduplicatedRelays = Array.from(new Set(normalizedRelays))

Loading…
Cancel
Save