Browse Source

bug-fixes

imwald
Silberengel 1 month ago
parent
commit
8d6fe75069
  1. 52
      src/PageManager.tsx
  2. 38
      src/components/AdvancedEventLab/AdvancedEventLabDialog.tsx
  3. 48
      src/components/SearchResult/FullTextSearchByRelay.tsx
  4. 4
      src/components/SearchResult/index.tsx
  5. 14
      src/lib/languagetool-cm-linter.ts
  6. 35
      src/services/client.service.ts

52
src/PageManager.tsx

@ -70,7 +70,6 @@ import { SecondaryPageContext, useSecondaryPage, useSecondaryPageOptional } from @@ -70,7 +70,6 @@ import { SecondaryPageContext, useSecondaryPage, useSecondaryPageOptional } from
const SpellsPageLazy = lazy(() => import('./pages/primary/SpellsPage'))
/** Lazy NoteList pages break: PageManager → … → NoteList → NoteCard → useSmartNoteNavigation → PageManager */
const NoteListPageLazy = lazy(() => import('@/pages/primary/NoteListPage'))
const SecondaryNoteListPageLazy = lazy(() => import('@/pages/secondary/NoteListPage'))
const primaryPageLazyFallback = (
<div className="flex flex-1 items-center justify-center p-8 text-sm text-muted-foreground">
@ -725,34 +724,13 @@ export function useSmartProfileNavigationOptional() { @@ -725,34 +724,13 @@ export function useSmartProfileNavigationOptional() {
return { navigateToProfile }
}
// Fixed: Hashtag navigation now uses primary note view since secondary panel is disabled
// Hashtag / d-tag note list opens on the secondary stack (right panel or single-pane sheet), same as other search routes.
export function useSmartHashtagNavigation() {
const { setPrimaryNoteView, getNavigationCounter } = usePrimaryNoteView()
const { push: pushSecondaryPage } = useSecondaryPage()
const navigateToHashtag = (url: string) => {
// Use primary note view to show hashtag feed since secondary panel is disabled
// Update URL first - do this synchronously before setting the view
const parsedUrl = url.startsWith('/') ? url : `/${url}`
window.history.pushState(null, '', parsedUrl)
// Extract hashtag from URL for the key to ensure unique keys for different hashtags
const searchParams = new URLSearchParams(parsedUrl.includes('?') ? parsedUrl.split('?')[1] : '')
const hashtag = searchParams.get('t') || ''
// Get the current navigation counter and use next value for the key
// This ensures unique keys that force remounting - setPrimaryNoteView will increment it
const counter = getNavigationCounter()
const key = `hashtag-${hashtag}-${counter + 1}`
// Use a key based on the hashtag and navigation counter to force remounting when hashtag changes
// This ensures the component reads the new URL parameters when it mounts
// setPrimaryNoteView will increment the counter, so we use counter + 1 for the key
setPrimaryNoteView(
<Suspense fallback={primaryPageLazyFallback}>
<SecondaryNoteListPageLazy key={key} hideTitlebar={true} />
</Suspense>,
'hashtag'
)
// Dispatch custom event as a fallback for components that might be reused
pushSecondaryPage(parsedUrl)
window.dispatchEvent(new CustomEvent('hashtag-navigation', { detail: { url: parsedUrl } }))
}
@ -761,24 +739,18 @@ export function useSmartHashtagNavigation() { @@ -761,24 +739,18 @@ export function useSmartHashtagNavigation() {
/** Safe variant for createRoot trees. Returns fallback navigation when outside providers. */
export function useSmartHashtagNavigationOptional() {
const primaryNoteView = usePrimaryNoteViewOptional()
if (!primaryNoteView) {
return { navigateToHashtag: (url: string) => { window.location.href = url.startsWith('/') ? url : `/${url}` } }
const secondaryPage = useSecondaryPageOptional()
if (!secondaryPage) {
return {
navigateToHashtag: (url: string) => {
window.location.href = url.startsWith('/') ? url : `/${url}`
}
}
}
const { setPrimaryNoteView, getNavigationCounter } = primaryNoteView
const { push } = secondaryPage
const navigateToHashtag = (url: string) => {
const parsedUrl = url.startsWith('/') ? url : `/${url}`
window.history.pushState(null, '', parsedUrl)
const searchParams = new URLSearchParams(parsedUrl.includes('?') ? parsedUrl.split('?')[1] : '')
const hashtag = searchParams.get('t') || ''
const counter = getNavigationCounter()
const key = `hashtag-${hashtag}-${counter + 1}`
setPrimaryNoteView(
<Suspense fallback={primaryPageLazyFallback}>
<SecondaryNoteListPageLazy key={key} hideTitlebar={true} />
</Suspense>,
'hashtag'
)
push(parsedUrl)
window.dispatchEvent(new CustomEvent('hashtag-navigation', { detail: { url: parsedUrl } }))
}
return { navigateToHashtag }

38
src/components/AdvancedEventLab/AdvancedEventLabDialog.tsx

@ -18,7 +18,7 @@ import { @@ -18,7 +18,7 @@ import {
} from '@/components/ui/select'
import logger from '@/lib/logger'
import { isLanguageToolConfigured } from '@/lib/languagetool-client'
import { languageToolLintExtension, requestAdvancedLabGrammarLint } from '@/lib/languagetool-cm-linter'
import { languageToolLintExtension, LT_GRAMMAR_MARK_CLASS, requestAdvancedLabGrammarLint } from '@/lib/languagetool-cm-linter'
import { pickLanguageToolCodeForTranslateTarget } from '@/lib/languagetool-language-order'
import {
buildResolvedTranslateMenuLanguageOptions,
@ -629,6 +629,7 @@ export default function AdvancedEventLabDialog({ @@ -629,6 +629,7 @@ export default function AdvancedEventLabDialog({
const mkExtensions: Extension[] = [
history(),
keymap.of([...defaultKeymap, ...historyKeymap]),
EditorView.lineWrapping,
lineNumbers(),
cmPlaceholder(
labTRef.current(
@ -645,6 +646,20 @@ export default function AdvancedEventLabDialog({ @@ -645,6 +646,20 @@ export default function AdvancedEventLabDialog({
'.cm-content': {
minHeight: '11rem',
fontFamily: 'var(--font-mono, ui-monospace, monospace)'
},
// LanguageTool hits: drop default thin SVG underline, use thick wavy line (see `LT_GRAMMAR_MARK_CLASS`).
[`.cm-lintRange.${LT_GRAMMAR_MARK_CLASS}`]: {
backgroundImage: 'none',
paddingBottom: '2px',
textDecoration: 'underline',
textDecorationSkipInk: 'none',
textDecorationStyle: 'wavy',
textDecorationColor: '#ea580c',
textDecorationThickness: '3px',
textUnderlineOffset: '3px'
},
[`.cm-lintRange-active.${LT_GRAMMAR_MARK_CLASS}`]: {
backgroundColor: 'rgba(234, 88, 12, 0.22)'
}
}),
EditorView.updateListener.of((update) => {
@ -662,7 +677,26 @@ export default function AdvancedEventLabDialog({ @@ -662,7 +677,26 @@ export default function AdvancedEventLabDialog({
languageToolLintExtension(() => ltLangRef.current, 650, () => markupMode)
)
}
if (dark) mkExtensions.push(oneDark)
if (dark) {
mkExtensions.push(oneDark)
mkExtensions.push(
EditorView.theme({
[`.cm-lintRange.${LT_GRAMMAR_MARK_CLASS}`]: {
backgroundImage: 'none',
paddingBottom: '2px',
textDecoration: 'underline',
textDecorationSkipInk: 'none',
textDecorationStyle: 'wavy',
textDecorationColor: '#fdba74',
textDecorationThickness: '3px',
textUnderlineOffset: '3px'
},
[`.cm-lintRange-active.${LT_GRAMMAR_MARK_CLASS}`]: {
backgroundColor: 'rgba(251, 146, 60, 0.28)'
}
})
)
}
const mkState = EditorState.create({
doc: baseSlice.content,

48
src/components/SearchResult/FullTextSearchByRelay.tsx

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
import NoteCard from '@/components/NoteCard'
import RelayIcon from '@/components/RelayIcon'
import { Skeleton } from '@/components/ui/skeleton'
import { toRelay } from '@/lib/link'
import { compareEventsForDTagQuery } from '@/lib/dtag-search'
import { formatPubkey, pubkeyToNpub } from '@/lib/pubkey'
import { normalizeUrl } from '@/lib/url'
@ -12,16 +13,19 @@ import type { Event, Filter } from 'nostr-tools' @@ -12,16 +13,19 @@ import type { Event, Filter } from 'nostr-tools'
import { Loader2 } from 'lucide-react'
import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { useSmartRelayNavigationOptional } from '@/PageManager'
type MergedHit = {
event: Event
relayUrls: string[]
}
/** Hard cap for the whole merged search wave (from effect start). */
/** Hard cap for the merged search wave, counted from the first relay query start (not from React effect mount). */
const SEARCH_TOTAL_WALL_MS = 10_000
/** After the first relay reaches a terminal state, end the wave this many ms later (capped by {@link SEARCH_TOTAL_WALL_MS}). */
const SEARCH_AFTER_FIRST_RELAY_MS = 2_000
/** After the first results arrive from any relay, end the wave this many ms later (capped by {@link SEARCH_TOTAL_WALL_MS}). */
const SEARCH_AFTER_FIRST_RELAY_MS = 3_000
/** Per-relay {@link QueryService.query} budget from when that relay’s fetch starts (capped by remaining wave wall). */
const SEARCH_PER_RELAY_QUERY_MS = 10_000
/** Avoid opening every index relay at once (pool + main thread). */
const FULL_TEXT_SEARCH_RELAY_CONCURRENCY = 3
const FULL_TEXT_SEARCH_PER_RELAY_LIMIT = 80
@ -224,6 +228,11 @@ export default function FullTextSearchByRelay({ @@ -224,6 +228,11 @@ export default function FullTextSearchByRelay({
kinds: readonly number[]
}) {
const { t } = useTranslation()
const { navigateToRelay } = useSmartRelayNavigationOptional() ?? {
navigateToRelay: (url: string) => {
window.location.href = url
}
}
const runGeneration = useRef(0)
const [relayRows, setRelayRows] = useState<RelayFetchRow[]>([])
const [mergedHits, setMergedHits] = useState<MergedHit[]>([])
@ -281,9 +290,10 @@ export default function FullTextSearchByRelay({ @@ -281,9 +290,10 @@ export default function FullTextSearchByRelay({
)
setMergedHits([])
const waveT0 = Date.now()
let waveEndAt = waveT0 + SEARCH_TOTAL_WALL_MS
/** Only after ≥1 event from a relay: apply "first results + 2s" (empty EOSE must not shorten the wave). */
/** Set when the first {@link runOneRelay} begins (first real NIP-50 query); master wall clock starts then. */
let waveT0: number | null = null
let waveEndAt = 0
/** Only after ≥1 event from a relay: apply "first results + …ms" (empty EOSE must not shorten the wave). */
let appliedRelativeWaveCutoff = false
const scheduleMasterAbort = () => {
@ -298,8 +308,15 @@ export default function FullTextSearchByRelay({ @@ -298,8 +308,15 @@ export default function FullTextSearchByRelay({
}, ms)
}
const beginWaveIfNeeded = () => {
if (waveT0 !== null) return
waveT0 = Date.now()
waveEndAt = waveT0 + SEARCH_TOTAL_WALL_MS
scheduleMasterAbort()
}
const onFirstSearchHits = () => {
if (appliedRelativeWaveCutoff) return
if (appliedRelativeWaveCutoff || waveT0 === null) return
appliedRelativeWaveCutoff = true
const now = Date.now()
waveEndAt = Math.min(waveT0 + SEARCH_TOTAL_WALL_MS, now + SEARCH_AFTER_FIRST_RELAY_MS)
@ -320,8 +337,6 @@ export default function FullTextSearchByRelay({ @@ -320,8 +337,6 @@ export default function FullTextSearchByRelay({
{ once: true }
)
scheduleMasterAbort()
let relayCursor = 0
const nextRelayUrl = (): string | undefined => {
if (relayCursor >= normalizedRelays.length) return undefined
@ -359,8 +374,10 @@ export default function FullTextSearchByRelay({ @@ -359,8 +374,10 @@ export default function FullTextSearchByRelay({
const runOneRelay = async (relayUrl: string) => {
if (myRun !== runGeneration.current || abort.signal.aborted) return
beginWaveIfNeeded()
const t0 = performance.now()
const perRelayBudget = Math.max(1000, waveEndAt - Date.now())
const remainingWaveMs = Math.max(500, waveEndAt - Date.now())
const perRelayBudget = Math.min(SEARCH_PER_RELAY_QUERY_MS, remainingWaveMs)
try {
const { events: raw, connectionError } = await client.fetchEventsFromSingleRelay(
relayUrl,
@ -488,13 +505,18 @@ export default function FullTextSearchByRelay({ @@ -488,13 +505,18 @@ export default function FullTextSearchByRelay({
</span>
<div className="flex flex-wrap items-center gap-0.5">
{hit.relayUrls.map((url) => (
<span
<button
key={`${hit.event.id}-${relayKey(url)}`}
type="button"
title={relayHostForSubscribeLog(url)}
className="inline-flex shrink-0 opacity-90"
className="inline-flex shrink-0 rounded-sm opacity-90 ring-offset-background hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1"
onClick={(e) => {
e.stopPropagation()
navigateToRelay(toRelay(url))
}}
>
<RelayIcon url={url} skipRelayInfoFetch className="h-5 w-5 rounded-sm" iconSize={12} />
</span>
</button>
))}
</div>
</div>

4
src/components/SearchResult/index.tsx

@ -19,11 +19,11 @@ export default function SearchResult({ searchParams }: { searchParams: TSearchPa @@ -19,11 +19,11 @@ export default function SearchResult({ searchParams }: { searchParams: TSearchPa
const { pubkey, relayList } = useNostr()
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
/** Before child effects (e.g. NIP-50) open REQs, tear down idle feed / prefetch queries so search gets the pool. */
/** Before child effects (e.g. NIP-50) open REQs, abort background queries and drop pooled relay sockets so search gets the pool. */
useLayoutEffect(() => {
if (!searchParams) return
if (searchParams.type === 'relay') return
client.interruptBackgroundQueries()
client.interruptBackgroundQueries({ closePooledRelayConnections: true })
}, [searchParams?.type, searchParams?.search, searchParams?.input])
/** NIP-50 / index relays — always queried first on their own shard so dead personal relays cannot zero out search. */

14
src/lib/languagetool-cm-linter.ts

@ -22,6 +22,9 @@ export function requestAdvancedLabGrammarLint(view: EditorView): void { @@ -22,6 +22,9 @@ export function requestAdvancedLabGrammarLint(view: EditorView): void {
view.dispatch({ annotations: advancedLabGrammarLangRerun.of(true) })
}
/** Extra editor mark class so the lab can style LT hits more prominently than default `info` underlines. */
export const LT_GRAMMAR_MARK_CLASS = 'cm-ltGrammarIssue'
function matchToDiagnostic(docLen: number, m: LanguageToolMatch): Diagnostic | null {
const from = Math.max(0, Math.min(m.offset, docLen))
const to = Math.max(from, Math.min(m.offset + m.length, docLen))
@ -30,7 +33,13 @@ function matchToDiagnostic(docLen: number, m: LanguageToolMatch): Diagnostic | n @@ -30,7 +33,13 @@ function matchToDiagnostic(docLen: number, m: LanguageToolMatch): Diagnostic | n
const message = m.message + (m.rule?.id ? ` (${m.rule.id})` : '')
if (!fix) {
return { from, to, severity: 'info', message }
return {
from,
to,
severity: 'warning',
markClass: LT_GRAMMAR_MARK_CLASS,
message
}
}
/**
@ -42,7 +51,8 @@ function matchToDiagnostic(docLen: number, m: LanguageToolMatch): Diagnostic | n @@ -42,7 +51,8 @@ function matchToDiagnostic(docLen: number, m: LanguageToolMatch): Diagnostic | n
return {
from,
to,
severity: 'info',
severity: 'warning',
markClass: LT_GRAMMAR_MARK_CLASS,
message,
renderMessage(view: EditorView) {
const wrap = document.createElement('span')

35
src/services/client.service.ts

@ -3125,9 +3125,27 @@ class ClientService extends EventTarget { @@ -3125,9 +3125,27 @@ class ClientService extends EventTarget {
set.add(relay)
}
/** Yield relay pool / HTTP index capacity to search or publish by aborting default {@link QueryService.query} work. */
interruptBackgroundQueries(): void {
/**
* Yield capacity to foreground work: abort in-flight {@link QueryService.query} calls that did not pass
* `foreground: true` (feeds, prefetch, etc.).
* With {@link options.closePooledRelayConnections}, also closes every pooled relay socket so live timeline REQs
* release the pool (e.g. NIP-50 search); those subs reconnect when needed.
*/
interruptBackgroundQueries(options?: { closePooledRelayConnections?: boolean }): void {
this.queryService.interruptBackgroundQueries()
if (!options?.closePooledRelayConnections) return
let urls: string[] = []
try {
urls = [...this.pool.listConnectionStatus().keys()]
} catch {
/* ignore */
}
if (urls.length === 0) return
try {
this.pool.close(urls)
} catch {
/* ignore */
}
}
// Delegate to QueryService
@ -3237,6 +3255,19 @@ class ClientService extends EventTarget { @@ -3237,6 +3255,19 @@ class ClientService extends EventTarget {
...(options?.signal ? { signal: options.signal } : {})
}
if (import.meta.env.DEV) {
const f0 = Array.isArray(filter) ? filter[0] : filter
const search =
f0 && typeof f0 === 'object' && 'search' in f0 && typeof (f0 as { search?: unknown }).search === 'string'
? String((f0 as { search: string }).search).slice(0, 48)
: undefined
logger.info('[NIP-50] per-relay query', {
relay: normalized,
budgetMs: queryOpts.globalTimeout,
search
})
}
if (isHttpRelayUrl(normalized)) {
// HTTP index relay: use HTTP API instead of WebSocket pool
try {

Loading…
Cancel
Save