Browse Source

bug-fixes

imwald
Silberengel 1 month ago
parent
commit
e8019fa14b
  1. 2
      src/PageManager.tsx
  2. 2
      src/components/CreateWalletGuideToast/index.tsx
  3. 3
      src/components/FormattedTimestamp/index.tsx
  4. 2
      src/components/LiveActivitiesStrip.tsx
  5. 2
      src/components/NormalFeed/index.tsx
  6. 2
      src/components/NoteOptions/EditOrCloneEventDialog.tsx
  7. 2
      src/components/NoteStats/LikeButton.tsx
  8. 2
      src/components/NoteStats/Likes.tsx
  9. 2
      src/components/NoteStats/RepostButton.tsx
  10. 2
      src/components/PostEditor/PostContent.tsx
  11. 2
      src/components/Sidebar/RssButton.tsx
  12. 2
      src/components/TooManyRelaysAlertDialog/index.tsx
  13. 4
      src/i18n/index.ts
  14. 35
      src/lib/calendar-event.ts
  15. 2
      src/lib/publishing-feedback.tsx
  16. 2
      src/main.tsx
  17. 2
      src/pages/primary/CalendarPrimaryPage.tsx
  18. 2
      src/pages/primary/SpellsPage/useSpellsPageFeed.ts
  19. 2
      src/pages/secondary/RssFeedSettingsPage/index.tsx
  20. 2
      src/providers/FavoriteRelaysActivityProvider.tsx
  21. 2
      src/providers/FavoriteRelaysProvider.tsx
  22. 2
      src/providers/LiveActivitiesProvider.tsx
  23. 2
      src/providers/NostrProvider/index.tsx
  24. 4
      src/providers/UserTrustProvider.tsx
  25. 160
      src/services/client-query.service.ts
  26. 21
      src/services/client.service.ts
  27. 5
      src/services/media-upload.service.ts
  28. 2
      src/services/post-editor-cache.service.ts
  29. 15
      src/services/relay-operation-log.service.ts
  30. 2
      src/services/relay-selection.service.ts

2
src/PageManager.tsx

@ -2,6 +2,7 @@ @@ -2,6 +2,7 @@
// making it incompatible with Vite's Fast Refresh auto-detection. Opting into explicit
// full-reload mode to suppress the "incompatible export" HMR warning.
// @refresh reset
import storage from '@/services/local-storage.service'
import { RefreshButton } from '@/components/RefreshButton'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
@ -13,7 +14,6 @@ import { NavigationService } from '@/services/navigation.service' @@ -13,7 +14,6 @@ import { NavigationService } from '@/services/navigation.service'
import { ImwaldBrandBar } from '@/assets/Logo'
import LiveActivitiesStrip from '@/components/LiveActivitiesStrip'
import NoteDrawer from '@/components/NoteDrawer'
import storage from '@/services/local-storage.service'
import client from '@/services/client.service'
import { navigationEventStore } from '@/services/navigation-event-store'
import type { Event } from 'nostr-tools'

2
src/components/CreateWalletGuideToast/index.tsx

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
import storage from '@/services/local-storage.service'
import { toWallet } from '@/lib/link'
import { useSecondaryPage } from '@/contexts/secondary-page-context'
import { useNostr } from '@/providers/NostrProvider'
import storage from '@/services/local-storage.service'
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'

3
src/components/FormattedTimestamp/index.tsx

@ -25,6 +25,9 @@ function FormattedTimestampContent({ @@ -25,6 +25,9 @@ function FormattedTimestampContent({
short?: boolean
}) {
const { t } = useTranslation()
if (!Number.isFinite(timestamp)) {
return '\u2014'
}
const time = dayjs(timestamp * 1000)
const now = dayjs()

2
src/components/LiveActivitiesStrip.tsx

@ -1,10 +1,10 @@ @@ -1,10 +1,10 @@
import storage from '@/services/local-storage.service'
import { LIVE_ACTIVITIES_SLIDE_INTERVAL_MS } from '@/lib/live-activities'
import { toNote } from '@/lib/link'
import { cn } from '@/lib/utils'
import { useSmartNoteNavigation } from '@/PageManager'
import { useLiveActivitiesOptional } from '@/providers/useLiveActivities'
import { useUserPreferencesOptional } from '@/providers/UserPreferencesProvider'
import storage from '@/services/local-storage.service'
import { ExternalLink } from 'lucide-react'
import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'

2
src/components/NormalFeed/index.tsx

@ -1,9 +1,9 @@ @@ -1,9 +1,9 @@
import storage from '@/services/local-storage.service'
import NoteList, { TNoteListRef } from '@/components/NoteList'
import { RefreshButton } from '@/components/RefreshButton'
import Tabs, { TabDefinition } from '@/components/Tabs'
import { useKindFilterOrDefaults } from '@/providers/KindFilterProvider'
import { useUserTrust } from '@/contexts/user-trust-context'
import storage from '@/services/local-storage.service'
import { PROFILE_MEDIA_TAB_KINDS, FAST_READ_RELAY_URLS } from '@/constants'
import { isWispTrendingNotesRelayUrl } from '@/lib/wisp-trending-relay'
import type { TPrimaryPageName } from '@/PageManager'

2
src/components/NoteOptions/EditOrCloneEventDialog.tsx

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
import storage from '@/services/local-storage.service'
import { Card } from '@/components/ui/card'
import {
Dialog,
@ -39,7 +40,6 @@ import { @@ -39,7 +40,6 @@ import {
} from '@/lib/publishing-feedback'
import { cn } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider'
import storage from '@/services/local-storage.service'
import postEditorCache from '@/services/post-editor-cache.service'
import type { TDraftEvent } from '@/types'
import dayjs from 'dayjs'

2
src/components/NoteStats/LikeButton.tsx

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
import storage from '@/services/local-storage.service'
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '@/components/ui/drawer'
import {
DropdownMenu,
@ -26,7 +27,6 @@ import { useUserTrust } from '@/contexts/user-trust-context' @@ -26,7 +27,6 @@ import { useUserTrust } from '@/contexts/user-trust-context'
import { eventService } from '@/services/client.service'
import noteStatsService from '@/services/note-stats.service'
import type { TNoteStats } from '@/services/note-stats.service'
import storage from '@/services/local-storage.service'
import { TEmoji } from '@/types'
import { SmilePlus } from 'lucide-react'
import { Event } from 'nostr-tools'

2
src/components/NoteStats/Likes.tsx

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
import storage from '@/services/local-storage.service'
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'
import { Skeleton } from '@/components/ui/skeleton'
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
@ -9,7 +10,6 @@ import { useNostr } from '@/providers/NostrProvider' @@ -9,7 +10,6 @@ import { useNostr } from '@/providers/NostrProvider'
import { useUserTrust } from '@/contexts/user-trust-context'
import noteStatsService from '@/services/note-stats.service'
import type { TNoteStats } from '@/services/note-stats.service'
import storage from '@/services/local-storage.service'
import { TEmoji } from '@/types'
import { Event } from 'nostr-tools'
import { useMemo, useRef, useState } from 'react'

2
src/components/NoteStats/RepostButton.tsx

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
import storage from '@/services/local-storage.service'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerOverlay } from '@/components/ui/drawer'
@ -17,7 +18,6 @@ import { useScreenSize } from '@/providers/ScreenSizeProvider' @@ -17,7 +18,6 @@ import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { useUserTrust } from '@/contexts/user-trust-context'
import noteStatsService from '@/services/note-stats.service'
import type { TNoteStats } from '@/services/note-stats.service'
import storage from '@/services/local-storage.service'
import { PencilLine, Repeat } from 'lucide-react'
import { Event } from 'nostr-tools'
import { useMemo, useState } from 'react'

2
src/components/PostEditor/PostContent.tsx

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
import storage from '@/services/local-storage.service'
import Note from '@/components/Note'
import { Button } from '@/components/ui/button'
import { ScrollArea } from '@/components/ui/scroll-area'
@ -51,7 +52,6 @@ import { cleanUrl, rewritePlainTextHttpUrls } from '@/lib/url' @@ -51,7 +52,6 @@ import { cleanUrl, rewritePlainTextHttpUrls } from '@/lib/url'
import logger from '@/lib/logger'
import { LoginRequiredError } from '@/lib/nostr-errors'
import postEditorCache from '@/services/post-editor-cache.service'
import storage from '@/services/local-storage.service'
import { TPollCreateData } from '@/types'
import {
Book,

2
src/components/Sidebar/RssButton.tsx

@ -1,9 +1,9 @@ @@ -1,9 +1,9 @@
import storage from '@/services/local-storage.service'
import { usePrimaryPage } from '@/contexts/primary-page-context'
import { usePrimaryNoteView } from '@/contexts/primary-note-view-context'
import { Rss } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import SidebarItem from './SidebarItem'
import storage from '@/services/local-storage.service'
export default function RssButton() {
const { t } = useTranslation()

2
src/components/TooManyRelaysAlertDialog/index.tsx

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
import storage from '@/services/local-storage.service'
import {
AlertDialog,
AlertDialogContent,
@ -19,7 +20,6 @@ import { toRelaySettings } from '@/lib/link' @@ -19,7 +20,6 @@ import { toRelaySettings } from '@/lib/link'
import { useSecondaryPage } from '@/contexts/secondary-page-context'
import { useNostr } from '@/providers/NostrProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import storage from '@/services/local-storage.service'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'

4
src/i18n/index.ts

@ -65,6 +65,10 @@ export function initI18n(): Promise<void> { @@ -65,6 +65,10 @@ export function initI18n(): Promise<void> {
})
i18n.services.formatter?.add('date', (timestamp, lng) => {
const n = Number(timestamp)
if (!Number.isFinite(n)) {
return '\u2014'
}
switch (lng) {
case 'zh':
return dayjs(timestamp).format('YYYY年MM月DD日')

35
src/lib/calendar-event.ts

@ -240,10 +240,24 @@ export function stripCalendarEventRedundantTopicHashtagLines( @@ -240,10 +240,24 @@ export function stripCalendarEventRedundantTopicHashtagLines(
const CALENDAR_DISPLAY_LOCALE = 'en-US'
/** Safe fallback when `Date` is invalid — avoids `Intl.DateTimeFormat#formatToParts` throwing. */
const INVALID_DATE_PARTS: Record<Intl.DateTimeFormatPartTypes, string> = new Proxy(
{} as Record<Intl.DateTimeFormatPartTypes, string>,
{
get(_target, prop: string | symbol) {
if (typeof prop !== 'string') return '\u2014'
if (prop === 'literal') return ''
if (prop === 'dayPeriod' || prop === 'timeZoneName') return ''
return '\u2014'
}
}
)
function readFormatParts(
d: Date,
opts: Intl.DateTimeFormatOptions
): Record<Intl.DateTimeFormatPartTypes, string> {
if (!Number.isFinite(d.getTime())) return INVALID_DATE_PARTS
const out: Partial<Record<Intl.DateTimeFormatPartTypes, string>> = {}
for (const p of new Intl.DateTimeFormat(CALENDAR_DISPLAY_LOCALE, opts).formatToParts(d)) {
if (p.type !== 'literal') out[p.type] = p.value
@ -256,7 +270,11 @@ function readFormatParts( @@ -256,7 +270,11 @@ function readFormatParts(
* (e.g. `May 13, 2025 10:30 am EST`) in the viewer's local zone avoids DD/MM vs MM/DD ambiguity.
*/
export function formatCalendarTime(ts: number): string {
const d = new Date(ts * 1000)
if (!Number.isFinite(ts)) return '\u2014'
const ms = ts * 1000
if (!Number.isFinite(ms)) return '\u2014'
const d = new Date(ms)
if (!Number.isFinite(d.getTime())) return '\u2014'
const p = readFormatParts(d, {
month: 'long',
day: 'numeric',
@ -313,6 +331,7 @@ export function formatCalendarTimeRange(start: number, end: number | undefined): @@ -313,6 +331,7 @@ export function formatCalendarTimeRange(start: number, end: number | undefined):
export function formatCalendarDate(dateStr: string): string {
if (!dateStr) return ''
const d = new Date(dateStr + 'T12:00:00')
if (!Number.isFinite(d.getTime())) return ''
const p = readFormatParts(d, { month: 'long', day: 'numeric', year: 'numeric' })
return `${p.month} ${p.day}, ${p.year}`
}
@ -331,7 +350,13 @@ const NIP52_SECONDS_PER_DAY = 86400 @@ -331,7 +350,13 @@ const NIP52_SECONDS_PER_DAY = 86400
function nip52DayIndexToUtcCalendarParts(dayIndex: number): { month: string; day: string; year: string } {
const ms = dayIndex * NIP52_SECONDS_PER_DAY * 1000
if (!Number.isFinite(ms)) {
return { month: '\u2014', day: '\u2014', year: '\u2014' }
}
const d = new Date(ms)
if (!Number.isFinite(d.getTime())) {
return { month: '\u2014', day: '\u2014', year: '\u2014' }
}
const parts = new Intl.DateTimeFormat(CALENDAR_DISPLAY_LOCALE, {
month: 'long',
day: 'numeric',
@ -507,8 +532,10 @@ function toYmdLocal(d: Date): string { @@ -507,8 +532,10 @@ function toYmdLocal(d: Date): string {
/** Compact week banner for sidebar (en-US month names). */
export function formatSidebarWeekLabel(weekStartMs: number, weekEndExclusiveMs: number): string {
if (!Number.isFinite(weekStartMs) || !Number.isFinite(weekEndExclusiveMs)) return ''
const start = new Date(weekStartMs)
const last = new Date(weekEndExclusiveMs)
if (!Number.isFinite(start.getTime()) || !Number.isFinite(last.getTime())) return ''
last.setDate(last.getDate() - 1)
const y1 = start.getFullYear()
const y2 = last.getFullYear()
@ -542,8 +569,9 @@ export function formatCalendarSidebarRow(event: Event): string { @@ -542,8 +569,9 @@ export function formatCalendarSidebarRow(event: Event): string {
}
return a
}
if (m.start == null || Number.isNaN(m.start)) return ''
if (m.start == null || Number.isNaN(m.start) || !Number.isFinite(m.start)) return ''
const d = new Date(m.start * 1000)
if (!Number.isFinite(d.getTime())) return ''
const p = readFormatParts(d, {
month: 'short',
day: 'numeric',
@ -554,8 +582,9 @@ export function formatCalendarSidebarRow(event: Event): string { @@ -554,8 +582,9 @@ export function formatCalendarSidebarRow(event: Event): string {
})
const ap = (p.dayPeriod ?? '').toLowerCase()
const base = `${p.month} ${p.day} · ${p.hour}:${p.minute} ${ap} ${p.timeZoneName ?? ''}`.trim()
if (m.end != null && !Number.isNaN(m.end) && m.end > m.start) {
if (m.end != null && !Number.isNaN(m.end) && Number.isFinite(m.end) && m.end > m.start) {
const d2 = new Date(m.end * 1000)
if (!Number.isFinite(d2.getTime())) return base
const p2 = readFormatParts(d2, {
hour: 'numeric',
minute: '2-digit',

2
src/lib/publishing-feedback.tsx

@ -1,9 +1,9 @@ @@ -1,9 +1,9 @@
import storage from '@/services/local-storage.service'
import RelayStatusDisplay from '@/components/RelayStatusDisplay'
import { CheckCircle2 } from 'lucide-react'
import type { ReactNode } from 'react'
import { useContext } from 'react'
import { FavoriteRelaysContext } from '@/providers/favorite-relays-context'
import storage from '@/services/local-storage.service'
import { toast } from 'sonner'
export type PublishSuccessSubtleDetail = { message?: string }

2
src/main.tsx

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
import './index.css'
import './polyfill'
import './lib/error-suppression'
import storage from './services/local-storage.service'
import './services/lightning.service'
import './lib/debug-utils'
import { fetchWithTimeout } from './lib/fetch-with-timeout'
@ -10,7 +11,6 @@ import { createRoot } from 'react-dom/client' @@ -10,7 +11,6 @@ import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import { ErrorBoundary } from './components/ErrorBoundary.tsx'
import { initI18n } from './i18n'
import storage from './services/local-storage.service'
import { restoreSessionFeedSnapshotsAfterHardRefresh } from './services/session-feed-snapshot.service'
import { installStaleBuildChunkRecovery } from './lib/stale-chunk-recovery'

2
src/pages/primary/CalendarPrimaryPage.tsx

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
import storage from '@/services/local-storage.service'
import PrimaryPageLayout, { type TPrimaryPageLayoutRef } from '@/layouts/PrimaryPageLayout'
import {
calendarOccurrenceOverlapsRange,
@ -20,7 +21,6 @@ import { useNostr } from '@/providers/NostrProvider' @@ -20,7 +21,6 @@ import { useNostr } from '@/providers/NostrProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import client from '@/services/client.service'
import indexedDb from '@/services/indexed-db.service'
import storage from '@/services/local-storage.service'
import { CALENDAR_EVENT_KINDS, ExtendedKind } from '@/constants'
import { TPageRef } from '@/types'
import { CalendarEventCoverImage } from '@/components/CalendarEventCoverImage'

2
src/pages/primary/SpellsPage/useSpellsPageFeed.ts

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import type { Event } from 'nostr-tools'
import { kinds as nostrKinds } from 'nostr-tools'
import storage from '@/services/local-storage.service'
import { ExtendedKind, DEFAULT_FEED_SHOW_KINDS } from '@/constants'
import { getPubkeysFromPTags } from '@/lib/tag'
import { normalizeUrl } from '@/lib/url'
@ -36,7 +37,6 @@ import { @@ -36,7 +37,6 @@ import {
import { getRelaysForSpell, spellEventToFilter } from '@/services/spell.service'
import type { TFeedSubRequest } from '@/types'
import { isFollowFeedFauxSpellId } from './fauxSpellConfig'
import storage from '@/services/local-storage.service'
/** `fetchReplaceableEvent(kind 3)` / relay-list hydration can hang; never block the Following spell on it. */
const FOLLOWING_FETCH_FOLLOWINGS_TIMEOUT_MS = 10_000

2
src/pages/secondary/RssFeedSettingsPage/index.tsx

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
import storage from '@/services/local-storage.service'
import { RefreshButton } from '@/components/RefreshButton'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { usePrimaryNoteView } from '@/contexts/primary-note-view-context'
@ -11,7 +12,6 @@ import { Skeleton } from '@/components/ui/skeleton' @@ -11,7 +12,6 @@ import { Skeleton } from '@/components/ui/skeleton'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import storage from '@/services/local-storage.service'
import { createRssFeedListDraftEvent } from '@/lib/draft-event'
import { showPublishingFeedback, showSimplePublishSuccess, showPublishingError } from '@/lib/publishing-feedback'
import { CloudUpload, Trash2, Plus, Download, Upload } from 'lucide-react'

2
src/providers/FavoriteRelaysActivityProvider.tsx

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
import storage from '@/services/local-storage.service'
import logger from '@/lib/logger'
import { ExtendedKind, NIP71_VIDEO_KINDS } from '@/constants'
import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
@ -12,7 +13,6 @@ import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' @@ -12,7 +13,6 @@ import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useNostr } from '@/providers/NostrProvider'
import { queryService, replaceableEventService } from '@/services/client.service'
import indexedDb from '@/services/indexed-db.service'
import storage from '@/services/local-storage.service'
import type { Event } from 'nostr-tools'
import { kinds } from 'nostr-tools'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'

2
src/providers/FavoriteRelaysProvider.tsx

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
import { FAST_READ_RELAY_URLS, DEFAULT_FAVORITE_RELAYS } from '@/constants'
import storage from '@/services/local-storage.service'
import { createFavoriteRelaysDraftEvent, createBlockedRelaysDraftEvent, createRelaySetDraftEvent } from '@/lib/draft-event'
import { getReplaceableEventIdentifier } from '@/lib/event'
import { getRelaySetFromEvent } from '@/lib/event-metadata'
@ -6,7 +7,6 @@ import { randomString } from '@/lib/random' @@ -6,7 +7,6 @@ import { randomString } from '@/lib/random'
import { isHttpRelayUrl, isWebsocketUrl, normalizeAnyRelayUrl, normalizeUrl } from '@/lib/url'
import { queryService } from '@/services/client.service'
import indexedDb from '@/services/indexed-db.service'
import storage from '@/services/local-storage.service'
import { TRelaySet } from '@/types'
import { Event, kinds } from 'nostr-tools'
import { useCallback, useEffect, useMemo, useState } from 'react'

2
src/providers/LiveActivitiesProvider.tsx

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
import storage from '@/services/local-storage.service'
import {
buildLiveActivitiesRelayUrls,
filterLiveActivityItemsByReachableMedia,
@ -11,7 +12,6 @@ import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays' @@ -11,7 +12,6 @@ import { userReadRelaysWithHttp } from '@/lib/favorites-feed-relays'
import logger from '@/lib/logger'
import client from '@/services/client.service'
import indexedDb from '@/services/indexed-db.service'
import storage from '@/services/local-storage.service'
import { registerLiveActivitiesPrewarmCallback } from '@/services/live-activities-prewarm-bridge'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { LiveActivitiesContext } from './live-activities-context'

2
src/providers/NostrProvider/index.tsx

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
import storage from '@/services/local-storage.service'
import LoginDialog from '@/components/LoginDialog'
import NcryptsecPasswordPrompt from '@/components/NcryptsecPasswordPrompt'
import {
@ -34,7 +35,6 @@ import { queryService, replaceableEventService } from '@/services/client.service @@ -34,7 +35,6 @@ import { queryService, replaceableEventService } from '@/services/client.service
import customEmojiService from '@/services/custom-emoji.service'
import indexedDb from '@/services/indexed-db.service'
import postEditorCache from '@/services/post-editor-cache.service'
import storage from '@/services/local-storage.service'
import noteStatsService from '@/services/note-stats.service'
import {
ISigner,

4
src/providers/UserTrustProvider.tsx

@ -1,7 +1,5 @@ @@ -1,7 +1,5 @@
import { replaceableEventService } from '@/services/client.service'
import { getPubkeysFromPTags } from '@/lib/tag'
import { kinds } from 'nostr-tools'
import storage from '@/services/local-storage.service'
import { replaceableEventService } from '@/services/client.service'
import { UserTrustContext } from '@/contexts/user-trust-context'
import { type ReactNode, useCallback, useEffect, useState } from 'react'
import { useNostr } from './NostrProvider'

160
src/services/client-query.service.ts

@ -29,7 +29,13 @@ import { applyRelayNip42AckTimeout } from '@/lib/relay-nip42-tuning' @@ -29,7 +29,13 @@ import { applyRelayNip42AckTimeout } from '@/lib/relay-nip42-tuning'
import { isIndexRelayTransportFailure, queryIndexRelay } from '@/lib/index-relay-http'
import logger from '@/lib/logger'
import { isHttpRelayUrl, normalizeHttpRelayUrl, normalizeUrl } from '@/lib/url'
import { RelaySubscribeOpBatch } from '@/services/relay-operation-log.service'
import {
RelaySubscribeOpBatch,
compactFilterForRelayLog,
humanizeSubscribeTerminalDetail,
relayHostForSubscribeLog,
type RelayOpTerminalRow
} from '@/services/relay-operation-log.service'
import { patchRelayNoticeForFetchFailures } from '@/services/relay-notice-fetch-failure'
import type { Filter, Event as NEvent } from 'nostr-tools'
import { SimplePool, EventTemplate, VerifiedEvent, nip19 } from 'nostr-tools'
@ -47,6 +53,74 @@ function filterForRelay(f: Filter, relaySupportsSearch: boolean): Filter { @@ -47,6 +53,74 @@ function filterForRelay(f: Filter, relaySupportsSearch: boolean): Filter {
const HEX_EVENT_ID_RE = /^[0-9a-f]{64}$/i
let queryReqSeq = 0
function logQueryReqConsolidatedEnd(
reqId: number,
source: string,
inputRelays: string[],
httpBases: string[],
events: NEvent[],
terminals: RelayOpTerminalRow[],
getSeenForEvent: (eventId: string) => string[]
): void {
const kindHistogram: Record<string, number> = {}
for (const e of events) {
const k = String(e.kind)
kindHistogram[k] = (kindHistogram[k] ?? 0) + 1
}
const norm = (u: string) => normalizeUrl(u) || u
type Row = {
url: string
host: string
terminal?: RelayOpTerminalRow['outcome']
detail?: string
eventsReturned: number
}
const byKey = new Map<string, Row>()
const rowFor = (url: string): Row => {
const key = norm(url)
let r = byKey.get(key)
if (!r) {
r = { url: key, host: relayHostForSubscribeLog(key), eventsReturned: 0 }
byKey.set(key, r)
}
return r
}
for (const t of terminals) {
const r = rowFor(t.relayUrl)
r.terminal = t.outcome
r.detail = humanizeSubscribeTerminalDetail(t.outcome, t.detail)
}
for (const e of events) {
const seen = getSeenForEvent(e.id)
for (const u of seen) {
rowFor(u).eventsReturned += 1
}
}
for (const u of inputRelays) {
rowFor(u)
}
for (const b of httpBases) {
rowFor(b)
}
const perRelay = [...byKey.values()].sort((a, b) => a.host.localeCompare(b.host))
logger.debug('[QueryService] req_end', {
reqId,
source,
eventCount: events.length,
kindHistogram,
perRelay
})
}
function decodeEventRefForETagFilter(raw: string): string | null {
const trimmed = raw.trim()
if (!trimmed) return null
@ -314,19 +388,6 @@ export class QueryService { @@ -314,19 +388,6 @@ export class QueryService {
const replaceableRace = options?.replaceableRace ?? false
const replaceableRaceWaitMs = options?.replaceableRaceWaitMs ?? FIRST_RELAY_RESULT_GRACE_MS
const immediateReturn = options?.immediateReturn ?? false
const isExternalSearch = eoseTimeout > 1000
if (isExternalSearch) {
logger.debug('query: Starting external relay search', {
relayCount: urls.length,
relays: urls,
eoseTimeout,
globalTimeout,
replaceableRace,
immediateReturn,
filter: sanitizedFilters
})
}
const filtersForGrace = sanitizedFilters
const maxLimitForGrace = Math.max(...filtersForGrace.map((f) => (f.limit ?? 0) as number), 0)
@ -352,6 +413,22 @@ export class QueryService { @@ -352,6 +413,22 @@ export class QueryService {
)
const wsQueryUrls = urls.filter((u) => !isHttpRelayUrl(u))
const reqId = ++queryReqSeq
const source = options?.relayOpSource ?? 'QueryService.query'
const inputRelaysOrdered = Array.from(new Set(urls.map((u) => normalizeUrl(u) || u).filter(Boolean)))
const wsRelayCandidates = Array.from(new Set(wsQueryUrls.map((u) => normalizeUrl(u) || u).filter(Boolean)))
logger.debug('[QueryService] req_begin', {
reqId,
source,
relays: inputRelaysOrdered,
httpRelayBases,
wsRelayCandidates,
filters: sanitizedFilters.map(compactFilterForRelayLog),
eoseTimeout,
globalTimeout
})
return await new Promise<NEvent[]>((resolve) => {
const events: NEvent[] = []
const abortHttp = new AbortController()
@ -364,6 +441,27 @@ export class QueryService { @@ -364,6 +441,27 @@ export class QueryService {
let firstResultTime: number | null = null
let globalTimeoutId: ReturnType<typeof setTimeout> | null = null
let queryFinalizing = false
let resolvedSnapshot: NEvent[] = []
let reqEndLogged = false
let fallbackReqEndTimer: ReturnType<typeof setTimeout> | null = null
const emitReqEnd = (terminals: RelayOpTerminalRow[], snapshot: NEvent[]) => {
if (reqEndLogged) return
reqEndLogged = true
if (fallbackReqEndTimer != null) {
clearTimeout(fallbackReqEndTimer)
fallbackReqEndTimer = null
}
logQueryReqConsolidatedEnd(
reqId,
source,
inputRelaysOrdered,
httpRelayBases,
snapshot,
terminals,
(id) => this.getSeenEventRelayUrls(id)
)
}
const httpInflight =
httpRelayBases.length === 0
@ -388,9 +486,7 @@ export class QueryService { @@ -388,9 +486,7 @@ export class QueryService {
} catch (e) {
if ((e as Error).name === 'AbortError') return
relaySessionStrikes.recordReadFailure(base, 'http')
if (isIndexRelayTransportFailure(e)) {
logger.debug('[QueryService] HTTP index relay unreachable', { base, error: e })
} else {
if (!isIndexRelayTransportFailure(e)) {
logger.warn('[QueryService] HTTP index relay query failed', { base, error: e })
}
}
@ -443,10 +539,20 @@ export class QueryService { @@ -443,10 +539,20 @@ export class QueryService {
if (replaceableRaceTimeoutId) clearTimeout(replaceableRaceTimeoutId)
if (globalTimeoutId) clearTimeout(globalTimeoutId)
sub.close()
const resolvedList =
replaceableRace && events.length > 0 ? resolveReplaceableRaceEvents() : events
resolvedSnapshot = resolvedList
if (wsQueryUrls.length === 0) {
emitReqEnd([], resolvedList)
} else {
fallbackReqEndTimer = setTimeout(() => {
emitReqEnd([], resolvedList)
}, Math.min(globalTimeout + 2500, 45_000))
}
sub.close()
resolve(resolvedList)
}
@ -557,7 +663,7 @@ export class QueryService { @@ -557,7 +663,7 @@ export class QueryService {
}
}
},
{ source: options?.relayOpSource ?? 'QueryService.query', logLevel: 'debug' }
{ source: options?.relayOpSource ?? 'QueryService.query', logLevel: 'debug', quiet: true, onBatchEnd: (rows) => emitReqEnd(rows, resolvedSnapshot) }
)
const sub = {
@ -578,7 +684,13 @@ export class QueryService { @@ -578,7 +684,13 @@ export class QueryService {
urls: string[],
filter: Filter | Filter[],
callbacks: SubscribeCallbacks,
relayOpMeta?: { source: string; logLevel?: 'info' | 'debug' }
relayOpMeta?: {
source: string
logLevel?: 'info' | 'debug'
/** When true, suppress `[RelayOp] batch_begin` / `batch_end` (used by {@link QueryService.query}). */
quiet?: boolean
onBatchEnd?: (rows: RelayOpTerminalRow[]) => void
}
): { close: () => void } {
const filters = sanitizeFiltersBeforeReq(filter)
if (filters.length === 0) {
@ -645,7 +757,11 @@ export class QueryService { @@ -645,7 +757,11 @@ export class QueryService {
const opSource = relayOpMeta?.source ?? 'QueryService.subscribe'
const opBatch =
groupedRequests.length > 0
? new RelaySubscribeOpBatch(opSource, groupedRequests, { logLevel: relayOpMeta?.logLevel })
? new RelaySubscribeOpBatch(opSource, groupedRequests, {
logLevel: relayOpMeta?.logLevel,
quiet: relayOpMeta?.quiet,
onBatchEnd: relayOpMeta?.onBatchEnd
})
: null
opBatch?.logBegin()

21
src/services/client.service.ts

@ -3398,27 +3398,6 @@ class ClientService extends EventTarget { @@ -3398,27 +3398,6 @@ class ClientService extends EventTarget {
relayOpSource: 'ClientService.searchProfiles'
})
/** Which relays actually delivered each kind-0 id (for tuning SEARCHABLE_RELAY_URLS). DEBUG only. */
if (searchStr.length > 0) {
const relayHitCounts = new Map<string, number>()
for (const e of events) {
if (e.kind !== kinds.Metadata) continue
for (const u of this.queryService.getSeenEventRelayUrls(e.id)) {
const n = (normalizeUrl(u) || u).trim()
if (!n) continue
relayHitCounts.set(n, (relayHitCounts.get(n) ?? 0) + 1)
}
}
if (relayHitCounts.size > 0) {
const relayHits = [...relayHitCounts.entries()].sort((a, b) => b[1] - a[1])
logger.debug('[ClientService.searchProfiles] kind=0 deliveries by relay URL (count = events relay sent for this query)', {
searchPreview: searchStr.slice(0, 80),
totalKind0Events: events.filter((e) => e.kind === kinds.Metadata).length,
relayHits: Object.fromEntries(relayHits)
})
}
}
const byPk = new Map<string, NEvent>()
for (const e of events) {
if (e.kind !== kinds.Metadata) continue

5
src/services/media-upload.service.ts

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
/** Compression runs entirely in-app before upload (`compress-upload-media`). */
/** Compression runs entirely in-app before upload (`compress-upload-media`). Load `local-storage` before `./client.service` (that graph can re-enter here; constructor reads storage). */
import storage from './local-storage.service'
import { compressMediaForUpload } from '@/lib/compress-upload-media'
import { fetchWithTimeout } from '@/lib/fetch-with-timeout'
import logger from '@/lib/logger'
@ -12,8 +13,6 @@ import { simplifyUrl } from '@/lib/url' @@ -12,8 +13,6 @@ import { simplifyUrl } from '@/lib/url'
import { TDraftEvent, TMediaUploadServiceConfig } from '@/types'
import { BlossomClient } from 'blossom-client-sdk'
import { z } from 'zod'
/** Must run before `./client.service` — that graph can synchronously re-enter this module; `storage` must be bound first (constructor reads it at module bottom). */
import storage from './local-storage.service'
import client from './client.service'
type UploadOptions = {

2
src/services/post-editor-cache.service.ts

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
import storage from '@/services/local-storage.service'
import { StorageKey } from '@/constants'
import type { AdvancedEventLabSlice } from '@/lib/advanced-event-lab-slice'
import { parseEditorJsonToText } from '@/lib/tiptap'
import storage from '@/services/local-storage.service'
import { TPollCreateData } from '@/types'
import { Content } from '@tiptap/react'
import { Event } from 'nostr-tools'

15
src/services/relay-operation-log.service.ts

@ -31,7 +31,11 @@ export function compactFilterForRelayLog(f: Filter): Record<string, unknown> { @@ -31,7 +31,11 @@ export function compactFilterForRelayLog(f: Filter): Record<string, unknown> {
if (f['#p']?.length) out.pTagCount = f['#p'].length
if (f['#e']?.length) out.eTagCount = f['#e'].length
if (f['#t']?.length) out.tTagCount = f['#t'].length
if (f.search) out.search = true
if (typeof f.search === 'string' && f.search.length > 0) {
out.searchPreview = f.search.length > 120 ? `${f.search.slice(0, 117)}` : f.search
} else if (f.search) {
out.search = true
}
return out
}
@ -49,7 +53,7 @@ export interface RelayOpTerminalRow { @@ -49,7 +53,7 @@ export interface RelayOpTerminalRow {
type GroupedRelayRow = { url: string; filters: Filter[] }
/** Short host label for subscribe REQ logs (same as publish). */
function relayHostForSubscribeLog(url: string): string {
export function relayHostForSubscribeLog(url: string): string {
return relayHostForPublishLog(url)
}
@ -137,6 +141,8 @@ function groupTerminalsByOutcome(rows: RelayOpTerminalRow[]): Record<string, { c @@ -137,6 +141,8 @@ function groupTerminalsByOutcome(rows: RelayOpTerminalRow[]): Record<string, { c
export type RelaySubscribeOpBatchOptions = {
/** `info` logs every REQ wave at INFO; default `debug` keeps subscribe noise behind jumble-debug / VITE_DEBUG. */
logLevel?: 'info' | 'debug'
/** When true, skip `[RelayOp] batch_begin` / `batch_end` lines (e.g. when {@link QueryService.query} logs `req_begin`/`req_end`). */
quiet?: boolean
/** Invoked once when this REQ wave finishes (same `rows` as `batch_end` / `terminals`). */
onBatchEnd?: (rows: RelayOpTerminalRow[]) => void
}
@ -181,6 +187,7 @@ export class RelaySubscribeOpBatch { @@ -181,6 +187,7 @@ export class RelaySubscribeOpBatch {
private readonly source: string
private readonly grouped: GroupedRelayRow[]
private readonly logLevel: 'info' | 'debug'
private readonly quiet: boolean
private readonly onBatchEnd?: (rows: RelayOpTerminalRow[]) => void
private readonly terminal = new Map<number, RelayOpTerminalRow>()
private endLogged = false
@ -191,6 +198,7 @@ export class RelaySubscribeOpBatch { @@ -191,6 +198,7 @@ export class RelaySubscribeOpBatch {
this.source = source
this.grouped = grouped
this.logLevel = options?.logLevel ?? 'debug'
this.quiet = options?.quiet ?? false
this.onBatchEnd = options?.onBatchEnd
}
@ -203,6 +211,7 @@ export class RelaySubscribeOpBatch { @@ -203,6 +211,7 @@ export class RelaySubscribeOpBatch {
}
logBegin(): void {
if (this.quiet) return
const uniqueRelays = [...new Set(this.grouped.map((g) => g.url))]
this.logLine('[RelayOp] batch_begin', {
batchId: this.batchId,
@ -285,6 +294,7 @@ export class RelaySubscribeOpBatch { @@ -285,6 +294,7 @@ export class RelaySubscribeOpBatch {
timeoutCount: nTimeout
}
if (!this.quiet) {
if (this.logLevel === 'debug') {
this.logLine('[RelayOp] batch_end', {
...compact,
@ -295,6 +305,7 @@ export class RelaySubscribeOpBatch { @@ -295,6 +305,7 @@ export class RelaySubscribeOpBatch {
} else {
logger.info(`[RelayOp] batch_end — ${headline}\n${readableSummary}`, compact)
}
}
this.onBatchEnd?.(rows)
}

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

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
import { Event, kinds } from 'nostr-tools'
import { ExtendedKind, FAST_WRITE_RELAY_URLS, RANDOM_PUBLISH_RELAY_COUNT, READ_ONLY_RELAY_URLS } from '@/constants'
import storage from '@/services/local-storage.service'
import { NOSTR_URI_FOR_REPLY_PUBKEYS_REGEX } from '@/lib/content-patterns'
import client from '@/services/client.service'
import { eventService } from '@/services/client.service'
@ -10,7 +11,6 @@ import indexedDb from '@/services/indexed-db.service' @@ -10,7 +11,6 @@ import indexedDb from '@/services/indexed-db.service'
import { getHttpRelayListFromEvent, getRelayListFromEvent } from '@/lib/event-metadata'
import { stripLocalNetworkRelaysFromRelayList } from '@/lib/relay-list-sanitize'
import nip66Service from '@/services/nip66.service'
import storage from '@/services/local-storage.service'
export interface RelaySelectionContext {
// User's own relays

Loading…
Cancel
Save