Browse Source

bug-fixes

imwald
Silberengel 1 month ago
parent
commit
d0a4f4dba0
  1. 17
      src/components/Note/MarkdownArticle/MarkdownArticle.tsx
  2. 39
      src/components/NoteList/index.tsx
  3. 3
      src/constants.ts
  4. 10
      src/lib/event-ingest-filter.ts
  5. 24
      src/pages/secondary/RelayReviewsPage/index.tsx
  6. 25
      src/providers/MuteListProvider.tsx
  7. 8
      src/providers/NostrProvider/index.tsx
  8. 2
      src/routes.tsx
  9. 20
      src/services/indexed-db.service.ts

17
src/components/Note/MarkdownArticle/MarkdownArticle.tsx

@ -133,23 +133,30 @@ function CodeBlock({ id, code, language }: { id: string; code: string; language: @@ -133,23 +133,30 @@ function CodeBlock({ id, code, language }: { id: string; code: string; language:
const codeRef = useRef<HTMLDivElement>(null)
useEffect(() => {
let cancelled = false
const initHighlight = async () => {
if (typeof window !== 'undefined' && codeRef.current) {
if (typeof window === 'undefined') return
try {
const hljs = await import('highlight.js')
const codeElement = codeRef.current.querySelector('code')
if (cancelled) return
const root = codeRef.current
if (!root) return
const codeElement = root.querySelector('code')
if (codeElement) {
hljs.default.highlightElement(codeElement)
}
} catch (error) {
if (!cancelled) {
logger.error('Error loading highlight.js:', error)
}
}
}
// Small delay to ensure DOM is ready
const timeoutId = setTimeout(initHighlight, 0)
return () => clearTimeout(timeoutId)
const timeoutId = window.setTimeout(initHighlight, 0)
return () => {
cancelled = true
window.clearTimeout(timeoutId)
}
}, [code, language])
return (

39
src/components/NoteList/index.tsx

@ -93,6 +93,12 @@ if (import.meta.env.DEV && import.meta.hot) { @@ -93,6 +93,12 @@ if (import.meta.env.DEV && import.meta.hot) {
import.meta.hot.on('vite:beforeFullReload', bumpSuppressRelayEmptyFeedToast)
}
const SHOW_COUNT = 20 // Increased from 10 to show more events at once, reducing scroll load frequency
/**
* When building visible rows, scan this many merged-timeline events at most. Previously we only looked at the first
* {@link showCount} events then filtered with posts only, kind filters, and mutes, most of those could be hidden
* so the feed showed 24 notes while 100+ were already loaded (felt like a crawl).
*/
const MAX_TIMELINE_EVENTS_SCAN_FOR_VISIBLE = 2500
/** Hard cap after merging parallel one-shot fetches (e.g. interests = one REQ per topic). */
const ONE_SHOT_MERGED_CAP =100
/** Max events kept after merging parallel full-search REQ results across relays. */
@ -672,30 +678,33 @@ const NoteList = forwardRef( @@ -672,30 +678,33 @@ const NoteList = forwardRef(
const filteredEvents = useMemo(() => {
const idSet = new Set<string>()
const out: Event[] = []
const target = showCount
const maxScan = Math.min(
timelineEventsForFilter.length,
Math.min(MAX_TIMELINE_EVENTS_SCAN_FOR_VISIBLE, Math.max(target * 60, 400))
)
return timelineEventsForFilter.slice(0, showCount).filter((evt) => {
for (let i = 0; i < maxScan && out.length < target; i++) {
const evt = timelineEventsForFilter[i]
if (applyKindPickerInUi) {
if (!showKinds.includes(evt.kind)) return false
// Kind 1: show only OPs if showKind1OPs, only replies if showKind1Replies
if (!showKinds.includes(evt.kind)) continue
if (evt.kind === kinds.ShortTextNote) {
const isReply = isReplyNoteEvent(evt)
if (isReply && !showKind1Replies) return false
if (!isReply && !showKind1OPs) return false
if (isReply && !showKind1Replies) continue
if (!isReply && !showKind1OPs) continue
}
// Kind 1111 (comments): show only if showKind1111
if (evt.kind === ExtendedKind.COMMENT && !showKind1111) return false
// Git Republic releases: same visibility as kind-1 OPs
if (evt.kind === ExtendedKind.GIT_RELEASE && !showKind1OPs) return false
if (evt.kind === ExtendedKind.COMMENT && !showKind1111) continue
if (evt.kind === ExtendedKind.GIT_RELEASE && !showKind1OPs) continue
}
if (shouldHideEvent(evt)) return false
if (shouldHideEvent(evt)) continue
const id = isReplaceableEvent(evt.kind) ? getReplaceableCoordinateFromEvent(evt) : evt.id
if (idSet.has(id)) {
return false
}
if (idSet.has(id)) continue
idSet.add(id)
return true
})
out.push(evt)
}
return out
}, [
timelineEventsForFilter,
showCount,

3
src/constants.ts

@ -55,8 +55,9 @@ export const MAX_CONCURRENT_SUBS_PER_RELAY = 7 @@ -55,8 +55,9 @@ export const MAX_CONCURRENT_SUBS_PER_RELAY = 7
* How many timeline shards may open relay subscriptions at once. Each shard sends one REQ per relay
* in its list; with 6 shards in parallel a popular relay can see 6+ SUBs from this app alone, and a
* second feed wave (remount / strict mode) pushes past strict relay caps (e.g. nostr.sovbit.host 10).
* 3 is a modest bump for faster multi-shard home loads; lower to 2 if a relay complains about SUB count.
*/
export const TIMELINE_SHARD_SUBSCRIBE_CONCURRENCY = 2
export const TIMELINE_SHARD_SUBSCRIBE_CONCURRENCY = 3
/** Max relays to publish each event to (outboxes first, then targets' inboxes, then extras). */
export const MAX_PUBLISH_RELAYS = 20

10
src/lib/event-ingest-filter.ts

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
import { ExtendedKind } from '@/constants'
import { getRelayUrlFromRelayReviewEvent, getStarsFromRelayReviewEvent } from '@/lib/event-metadata'
import { getRelayUrlFromRelayReviewEvent } from '@/lib/event-metadata'
import type { Event as NEvent } from 'nostr-tools'
import { kinds } from 'nostr-tools'
@ -23,14 +23,12 @@ export function isStringifiedJsonObjectContentNostrEvent( @@ -23,14 +23,12 @@ export function isStringifiedJsonObjectContentNostrEvent(
}
/**
* Kind-31987 noise: missing `d` (relay URL) or a parseable `rating` tag (see {@link getStarsFromRelayReviewEvent}).
* Content may be JSON or prose; structure is validated on tags, not `content`.
* Kind-31987 noise: missing `d` (relay URL). Rating formats differ across clients; do not drop at ingest
* (feeds and cards already treat unknown ratings as zero stars).
*/
export function isIncompleteRelayReviewIngest(event: NEvent): boolean {
if (event.kind !== ExtendedKind.RELAY_REVIEW) return false
if (!getRelayUrlFromRelayReviewEvent(event)) return true
if (!getStarsFromRelayReviewEvent(event)) return true
return false
return !getRelayUrlFromRelayReviewEvent(event)
}
/** Single gate for subscribe/cache/IDB read paths: drop kind-1 JSON-object spam and malformed relay reviews. */

24
src/pages/secondary/RelayReviewsPage/index.tsx

@ -26,6 +26,19 @@ const RelayReviewsPage = forwardRef(({ url, index, hideTitlebar = false }: { url @@ -26,6 +26,19 @@ const RelayReviewsPage = forwardRef(({ url, index, hideTitlebar = false }: { url
}, [hideTitlebar, registerPrimaryPanelRefresh, bumpFeed])
const normalizedUrl = useMemo(() => (url ? normalizeUrl(url) : undefined), [url])
/** `d` tag values vary by client (raw vs normalized URL); REQ should OR-match like {@link RelayReviewsPreview}. */
const relayReviewDTags = useMemo(() => {
const raw = url?.trim()
const norm = normalizedUrl?.trim()
const uniq: string[] = []
const add = (s: string | undefined) => {
const t = s?.trim()
if (t && !uniq.includes(t)) uniq.push(t)
}
add(raw)
add(norm)
return uniq
}, [url, normalizedUrl])
/** Stable identity for session feed snapshot (decoupled from FAST_READ_RELAY_URLS JSON churn). */
const relayReviewsFeedSubscriptionKey = useMemo(
() =>
@ -33,14 +46,18 @@ const RelayReviewsPage = forwardRef(({ url, index, hideTitlebar = false }: { url @@ -33,14 +46,18 @@ const RelayReviewsPage = forwardRef(({ url, index, hideTitlebar = false }: { url
[normalizedUrl]
)
const reviewsSubRequests = useMemo<TFeedSubRequest[]>(() => {
if (!normalizedUrl) return []
if (!normalizedUrl || relayReviewDTags.length === 0) return []
return [
{
urls: [normalizedUrl, ...FAST_READ_RELAY_URLS],
filter: { '#d': [normalizedUrl] }
filter: {
kinds: [ExtendedKind.RELAY_REVIEW],
'#d': relayReviewDTags,
limit: 100
}
}
]
}, [normalizedUrl])
}, [normalizedUrl, relayReviewDTags])
const title = useMemo(
() => (url ? t('Reviews for {{relay}}', { relay: simplifyUrl(url) }) : undefined),
[url, t]
@ -63,6 +80,7 @@ const RelayReviewsPage = forwardRef(({ url, index, hideTitlebar = false }: { url @@ -63,6 +80,7 @@ const RelayReviewsPage = forwardRef(({ url, index, hideTitlebar = false }: { url
showKinds={[ExtendedKind.RELAY_REVIEW]}
subRequests={reviewsSubRequests}
feedSubscriptionKey={relayReviewsFeedSubscriptionKey}
useFilterAsIs
/>
</SecondaryPageLayout>
)

25
src/providers/MuteListProvider.tsx

@ -69,21 +69,17 @@ export function MuteListProvider({ children }: { children: ReactNode }) { @@ -69,21 +69,17 @@ export function MuteListProvider({ children }: { children: ReactNode }) {
const storedDecryptedTags = await indexedDb.getMuteDecryptedTags(muteListEvent.id)
if (storedDecryptedTags) {
return storedDecryptedTags
}
let plainText: string
const cached = z.array(z.array(z.string())).safeParse(storedDecryptedTags)
if (cached.success) return cached.data
try {
plainText = await nip04Decrypt(muteListEvent.pubkey, muteListEvent.content)
} catch (error) {
logMuteListPrivateIssueOnce(
muteListEvent.id,
'Mute list private section could not be decrypted (public mutes still apply). Use a signing-capable login for private mutes.',
{ cause: error instanceof Error ? error.message : String(error) }
)
return []
await indexedDb.deleteMuteDecryptedTags(muteListEvent.id)
} catch {
/* ignore */
}
}
const plainText = await nip04Decrypt(muteListEvent.pubkey, muteListEvent.content)
if (!plainText.trim()) {
logMuteListPrivateIssueOnce(
muteListEvent.id,
@ -98,6 +94,11 @@ export function MuteListProvider({ children }: { children: ReactNode }) { @@ -98,6 +94,11 @@ export function MuteListProvider({ children }: { children: ReactNode }) {
await indexedDb.putMuteDecryptedTags(muteListEvent.id, privateTags)
return privateTags
} catch (error) {
try {
await indexedDb.deleteMuteDecryptedTags(muteListEvent.id)
} catch {
/* ignore */
}
logMuteListPrivateIssueOnce(
muteListEvent.id,
'Mute list decrypted but private payload was not valid JSON (public mutes still apply).',

8
src/providers/NostrProvider/index.tsx

@ -1289,7 +1289,13 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -1289,7 +1289,13 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
}
const nip04Decrypt = async (pubkey: string, cipherText: string) => {
return signer?.nip04Decrypt(pubkey, cipherText) ?? ''
if (!signer) return ''
try {
return (await signer.nip04Decrypt(pubkey, cipherText)) ?? ''
} catch {
// Extensions often throw (padding / wrong key) while nsec path returns ''; keep call sites simple.
return ''
}
}
const checkLogin = async <T,>(cb?: () => T): Promise<T | void> => {

2
src/routes.tsx

@ -67,8 +67,8 @@ const ROUTES = [ @@ -67,8 +67,8 @@ const ROUTES = [
{ path: '/users/:id', element: SR(ProfilePageLazy) },
{ path: '/users/:id/following', element: SR(FollowingListPageLazy) },
{ path: '/users/:id/relays', element: SR(OthersRelaySettingsPageLazy) },
{ path: '/relays/:url', element: SR(RelayPageLazy) },
{ path: '/relays/:url/reviews', element: SR(RelayReviewsPageLazy) },
{ path: '/relays/:url', element: SR(RelayPageLazy) },
{ path: '/home/relays/:url', element: SR(RelayPageLazy) },
{ path: '/explore/relays/:url', element: SR(RelayPageLazy) },
{ path: '/search', element: SR(SearchPageLazy) },

20
src/services/indexed-db.service.ts

@ -656,6 +656,26 @@ class IndexedDbService { @@ -656,6 +656,26 @@ class IndexedDbService {
})
}
async deleteMuteDecryptedTags(id: string): Promise<void> {
await this.initPromise
return new Promise((resolve, reject) => {
if (!this.db) {
return resolve()
}
const transaction = this.db.transaction(StoreNames.MUTE_DECRYPTED_TAGS, 'readwrite')
const store = transaction.objectStore(StoreNames.MUTE_DECRYPTED_TAGS)
const req = store.delete(id)
req.onsuccess = () => {
transaction.commit()
resolve()
}
req.onerror = (event) => {
transaction.commit()
reject(event)
}
})
}
async iterateProfileEvents(callback: (event: Event) => Promise<void>): Promise<void> {
await this.initPromise
if (!this.db) {

Loading…
Cancel
Save