Browse Source

bug-fixes

imwald
Silberengel 4 weeks ago
parent
commit
c104dfd3f7
  1. 4
      package-lock.json
  2. 2
      package.json
  3. 27
      src/components/Explore/ExploreRelayReviews.tsx
  4. 18
      src/components/HelpAndAccountMenu.tsx
  5. 8
      src/components/ImageWithLightbox/index.tsx
  6. 182
      src/components/Note/PublicationIndex/PublicationIndex.tsx
  7. 8
      src/components/Profile/ProfilePublicationsFeed.tsx
  8. 57
      src/components/SessionRelaysTab/index.tsx
  9. 10
      src/hooks/usePublicationSectionLoader.ts
  10. 16
      src/lib/relay-list-builder.ts
  11. 4
      src/services/client.service.ts

4
package-lock.json generated

@ -1,12 +1,12 @@ @@ -1,12 +1,12 @@
{
"name": "jumble-imwald",
"version": "21.3.2",
"version": "21.4.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "jumble-imwald",
"version": "21.3.2",
"version": "21.4.0",
"license": "MIT",
"dependencies": {
"@asciidoctor/core": "^3.0.4",

2
package.json

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
{
"name": "jumble-imwald",
"version": "21.3.2",
"version": "21.4.0",
"description": "A user-friendly Nostr client focused on relay feed browsing and relay discovery, forked from Jumble",
"private": true,
"type": "module",

27
src/components/Explore/ExploreRelayReviews.tsx

@ -8,6 +8,7 @@ import { appendCuratedReadOnlyRelays } from '@/pages/primary/SpellsPage/fauxSpel @@ -8,6 +8,7 @@ import { appendCuratedReadOnlyRelays } from '@/pages/primary/SpellsPage/fauxSpel
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useNostr } from '@/providers/NostrProvider'
import client from '@/services/client.service'
import indexedDb, { StoreNames } from '@/services/indexed-db.service'
import type { Event } from 'nostr-tools'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -32,6 +33,28 @@ function dedupeRelayReviewsNewestFirst(events: Event[]): Event[] { @@ -32,6 +33,28 @@ function dedupeRelayReviewsNewestFirst(events: Event[]): Event[] {
return out
}
async function loadCachedRelayReviews(limit: number): Promise<Event[]> {
const fromSession = client
.getSessionEventsMatchingSearch('', Math.max(limit * 2, 200), [ExtendedKind.RELAY_REVIEW])
.filter((e) => e.kind === ExtendedKind.RELAY_REVIEW && !!getRelayUrlFromRelayReviewEvent(e))
if (fromSession.length >= limit) {
return dedupeRelayReviewsNewestFirst(fromSession).slice(0, limit)
}
try {
const archiveRows = await indexedDb.getStoreItems(StoreNames.EVENT_ARCHIVE)
const fromArchive = archiveRows
.map((row) => row?.value as Event | undefined)
.filter(
(e): e is Event =>
!!e && e.kind === ExtendedKind.RELAY_REVIEW && !!getRelayUrlFromRelayReviewEvent(e)
)
return dedupeRelayReviewsNewestFirst([...fromSession, ...fromArchive]).slice(0, limit)
} catch {
return dedupeRelayReviewsNewestFirst(fromSession).slice(0, limit)
}
}
export default function ExploreRelayReviews() {
const { t } = useTranslation()
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
@ -70,6 +93,10 @@ export default function ExploreRelayReviews() { @@ -70,6 +93,10 @@ export default function ExploreRelayReviews() {
setShowCount(SHOW_COUNT)
void (async () => {
const cached = await loadCachedRelayReviews(REVIEW_QUERY_LIMIT)
if (!cancelled && fetchGenRef.current === gen && cached.length > 0) {
setEvents(cached)
}
try {
const raw = await client.fetchEvents(
relayUrls,

18
src/components/HelpAndAccountMenu.tsx

@ -16,6 +16,7 @@ import { Skeleton } from '@/components/ui/skeleton' @@ -16,6 +16,7 @@ import { Skeleton } from '@/components/ui/skeleton'
import { formatPubkey, formatNpub, generateImageByPubkey, pubkeyToNpub } from '@/lib/pubkey'
import { cn } from '@/lib/utils'
import { usePrimaryPage } from '@/contexts/primary-page-context'
import { useFetchProfile } from '@/hooks/useFetchProfile'
import { useNostr } from '@/providers/NostrProvider'
import { ArrowDownUp, LogIn, LogOut, Settings, User, UserRound } from 'lucide-react'
import { useMemo, useState, type ReactNode } from 'react'
@ -67,6 +68,7 @@ function SidebarAccountMenu({ @@ -67,6 +68,7 @@ function SidebarAccountMenu({
const { account, profile } = useNostr()
const { current, display } = usePrimaryPage()
const pubkey = account?.pubkey
const { profile: fetchedProfile } = useFetchProfile(pubkey)
const active = useMemo(() => current === 'profile' && display, [display, current])
if (!pubkey) return null
@ -74,7 +76,8 @@ function SidebarAccountMenu({ @@ -74,7 +76,8 @@ function SidebarAccountMenu({
const defaultAvatar = generateImageByPubkey(pubkey)
const npub = pubkeyToNpub(pubkey)
const fallbackUsername = npub ? formatNpub(npub) : formatPubkey(pubkey)
const { username, avatar } = profile || { username: fallbackUsername, avatar: defaultAvatar }
const resolvedProfile = fetchedProfile ?? profile
const { username, avatar } = resolvedProfile || { username: fallbackUsername, avatar: defaultAvatar }
return (
<DropdownMenu>
@ -114,11 +117,14 @@ function TitlebarAccountMenu({ @@ -114,11 +117,14 @@ function TitlebarAccountMenu({
onLogoutClick: () => void
}) {
const { t } = useTranslation()
const { profile } = useNostr()
const { account, profile } = useNostr()
const pubkey = account?.pubkey
const { profile: fetchedProfile } = useFetchProfile(pubkey)
const resolvedProfile = fetchedProfile ?? profile
const { current, display } = usePrimaryPage()
const defaultAvatar = useMemo(
() => (profile?.pubkey ? generateImageByPubkey(profile.pubkey) : ''),
[profile]
() => (resolvedProfile?.pubkey ? generateImageByPubkey(resolvedProfile.pubkey) : ''),
[resolvedProfile]
)
const active = useMemo(() => current === 'profile' && display, [display, current])
@ -132,9 +138,9 @@ function TitlebarAccountMenu({ @@ -132,9 +138,9 @@ function TitlebarAccountMenu({
title={t('Account menu')}
aria-label={t('Account menu')}
>
{profile ? (
{resolvedProfile ? (
<Avatar className={cn('w-6 h-6', active ? 'ring-primary ring-1' : '')}>
<AvatarImage src={profile.avatar} className="object-cover object-center" />
<AvatarImage src={resolvedProfile.avatar} className="object-cover object-center" />
<AvatarFallback>
<img src={defaultAvatar} alt="" />
</AvatarFallback>

8
src/components/ImageWithLightbox/index.tsx

@ -72,7 +72,13 @@ export default function ImageWithLightbox({ @@ -72,7 +72,13 @@ export default function ImageWithLightbox({
/>
{index >= 0 &&
createPortal(
<div onClick={(e) => e.stopPropagation()}>
<div
data-lightbox-overlay
onClick={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
onTouchStart={(e) => e.stopPropagation()}
>
<Lightbox
index={index}
slides={[{

182
src/components/Note/PublicationIndex/PublicationIndex.tsx

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
import { ExtendedKind } from '@/constants'
import { Event, kinds, nip19 } from 'nostr-tools'
import { useEffect, useMemo, useState, useCallback, useSyncExternalStore } from 'react'
import { useEffect, useMemo, useState, useCallback, useSyncExternalStore, useRef } from 'react'
import { usePublicationSectionLoader } from '@/hooks/usePublicationSectionLoader'
import { parsePublicationATagCoordinate, publicationRefKey } from '@/lib/publication-section-fetch'
import { cn } from '@/lib/utils'
@ -15,7 +15,7 @@ import indexedDb from '@/services/indexed-db.service' @@ -15,7 +15,7 @@ import indexedDb from '@/services/indexed-db.service'
import { useSecondaryPageOptional } from '@/PageManager'
import { extractBookMetadata } from '@/lib/bookstr-parser'
import { dTagToTitleCase } from '@/lib/event-metadata'
import Image from '@/components/Image'
import ImageWithLightbox from '@/components/ImageWithLightbox'
import NoteOptions from '@/components/NoteOptions'
import {
getRenderedPublicationEventsVersion,
@ -96,6 +96,7 @@ export default function PublicationIndex({ @@ -96,6 +96,7 @@ export default function PublicationIndex({
className,
isNested = false,
parentImageUrl,
parentSummary,
flattenHierarchy = false,
chapterDepth = 0,
publicationFootnotesContainerId
@ -104,6 +105,7 @@ export default function PublicationIndex({ @@ -104,6 +105,7 @@ export default function PublicationIndex({
className?: string
isNested?: boolean
parentImageUrl?: string
parentSummary?: string
flattenHierarchy?: boolean
chapterDepth?: number
publicationFootnotesContainerId?: string
@ -152,6 +154,9 @@ export default function PublicationIndex({ @@ -152,6 +154,9 @@ export default function PublicationIndex({
const isBookstrEvent = (event.kind === ExtendedKind.PUBLICATION || event.kind === ExtendedKind.PUBLICATION_CONTENT) && !!bookMetadata.book
const isTopLevelPublication = !isNested && event.kind === ExtendedKind.PUBLICATION
const forceFlatHierarchy = flattenHierarchy || isBookstrEvent || isTopLevelPublication
const initialSectionLoadCount = isNested ? 1 : 3
const sectionLoadStep = isNested ? 1 : 3
const effectiveParentSummary = metadata.summary || parentSummary
const resolvedPublicationFootnotesContainerId = useMemo(
() =>
publicationFootnotesContainerId ??
@ -159,6 +164,8 @@ export default function PublicationIndex({ @@ -159,6 +164,8 @@ export default function PublicationIndex({
[publicationFootnotesContainerId, isTopLevelPublication, event.id]
)
const [isRetrying, setIsRetrying] = useState(false)
const [sectionLoadCount, setSectionLoadCount] = useState(initialSectionLoadCount)
const lazyLoadSentinelRef = useRef<HTMLDivElement | null>(null)
// Extract references from 'a' tags (addressable events) and 'e' tags (event IDs)
const referencesData = useMemo(() => {
@ -191,8 +198,8 @@ export default function PublicationIndex({ @@ -191,8 +198,8 @@ export default function PublicationIndex({
return refs
}, [event])
const { retryKeys, failedKeys, referencesWithEvents } =
usePublicationSectionLoader(event, referencesData)
const { requestKeys, retryKeys, failedKeys, referencesWithEvents } =
usePublicationSectionLoader(event, referencesData, { autoLoad: false })
const renderedEventsVersion = useSyncExternalStore(
subscribeRenderedPublicationEvents,
getRenderedPublicationEventsVersion,
@ -369,10 +376,29 @@ export default function PublicationIndex({ @@ -369,10 +376,29 @@ export default function PublicationIndex({
// Scroll to section
const scrollToSection = (coordinate: string) => {
const element = document.getElementById(`section-${coordinate.replace(/:/g, '-')}`)
const targetId = `section-${coordinate.replace(/:/g, '-')}`
const sectionIndex = referencesWithEvents.findIndex(
(ref) => (ref.coordinate || ref.eventId || '') === coordinate
)
if (sectionIndex >= 0) {
setSectionLoadCount((prev) => Math.max(prev, sectionIndex + 1))
const key = publicationRefKey(referencesWithEvents[sectionIndex] || {})
if (key) requestKeys([key])
}
let attempts = 0
const tryScroll = () => {
const element = document.getElementById(targetId)
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' })
return
}
if (attempts < 8) {
attempts += 1
window.setTimeout(tryScroll, 80)
}
}
tryScroll()
}
@ -398,6 +424,41 @@ export default function PublicationIndex({ @@ -398,6 +424,41 @@ export default function PublicationIndex({
return () => clearTimeout(t)
}, [referencesWithEvents, event])
useEffect(() => {
setSectionLoadCount(initialSectionLoadCount)
}, [event.id, initialSectionLoadCount])
useEffect(() => {
const keysToRequest = referencesWithEvents
.slice(0, sectionLoadCount)
.filter((ref) => ref.loadStatus === 'idle')
.map((ref) => publicationRefKey(ref))
.filter(Boolean)
if (keysToRequest.length > 0) {
requestKeys(keysToRequest)
}
}, [referencesWithEvents, requestKeys, sectionLoadCount])
useEffect(() => {
const sentinel = lazyLoadSentinelRef.current
if (!sentinel) return
if (sectionLoadCount >= referencesWithEvents.length) return
const observer = new IntersectionObserver(
(entries) => {
if (!entries[0]?.isIntersecting) return
setSectionLoadCount((prev) => Math.min(prev + sectionLoadStep, referencesWithEvents.length))
},
{ rootMargin: '220px 0px' }
)
observer.observe(sentinel)
return () => observer.disconnect()
}, [referencesWithEvents.length, sectionLoadCount, sectionLoadStep])
const visibleReferences = useMemo(
() => referencesWithEvents.slice(0, sectionLoadCount),
[referencesWithEvents, sectionLoadCount]
)
const handleManualRetry = useCallback(() => {
setIsRetrying(true)
const keys =
@ -408,6 +469,19 @@ export default function PublicationIndex({ @@ -408,6 +469,19 @@ export default function PublicationIndex({
window.setTimeout(() => setIsRetrying(false), 600)
}, [failedKeys, referencesData, retryKeys])
const normalizedParentImage = (parentImageUrl || '').trim()
const normalizedOwnImage = (metadata.image || '').trim()
const normalizedParentSummary = (parentSummary || '').trim()
const normalizedOwnSummary = (metadata.summary || '').trim()
const showNestedImagePreview =
isNested &&
!!normalizedOwnImage &&
normalizedOwnImage !== normalizedParentImage
const showNestedSummaryPreview =
isNested &&
!!normalizedOwnSummary &&
normalizedOwnSummary !== normalizedParentSummary
return (
<div className={cn('space-y-6', className)}>
@ -436,11 +510,25 @@ export default function PublicationIndex({ @@ -436,11 +510,25 @@ export default function PublicationIndex({
</div>
)}
{(metadata.type || metadata.version || metadata.publishedOn || metadata.publishedBy) && (
<div className="mt-4 flex flex-wrap justify-center gap-x-4 gap-y-1 text-xs text-muted-foreground">
{metadata.type && <span>Type: {metadata.type}</span>}
{metadata.version && <span>Version: {metadata.version}</span>}
{metadata.publishedOn && <span>Published: {metadata.publishedOn}</span>}
{metadata.publishedBy && <span>Publisher: {metadata.publishedBy}</span>}
<div className="mt-4 flex flex-wrap items-center justify-center gap-y-1 text-xs text-muted-foreground">
{[
metadata.type ? { label: 'Type', value: metadata.type } : null,
metadata.version ? { label: 'Version', value: metadata.version } : null,
metadata.publishedOn ? { label: 'Published', value: metadata.publishedOn } : null,
metadata.publishedBy ? { label: 'Publisher', value: metadata.publishedBy } : null
]
.filter((item): item is { label: string; value: string } => !!item)
.map((item, index) => (
<div key={item.label} className="inline-flex items-center">
{index > 0 && (
<span aria-hidden className="mx-2 text-muted-foreground/50">
·
</span>
)}
<span className="uppercase tracking-[0.14em] text-muted-foreground/80">{item.label}</span>
<span className="ml-1.5 text-foreground/85">{item.value}</span>
</div>
))}
</div>
)}
{metadata.tags.length > 0 && (
@ -468,24 +556,28 @@ export default function PublicationIndex({ @@ -468,24 +556,28 @@ export default function PublicationIndex({
</a>
</div>
)}
{metadata.summary && (
<blockquote className="mt-5 border-l-4 border-primary/70 pl-4 pr-2 italic text-muted-foreground text-left leading-relaxed">
<p className="break-words">{metadata.summary}</p>
</blockquote>
)}
{/* Display image for top-level 30040 publication */}
{metadata.image && (
<div className="mt-5 flex justify-center">
<Image
<ImageWithLightbox
image={{ url: metadata.image, pubkey: event.pubkey }}
className="max-w-[400px] w-full h-auto rounded-lg"
classNames={{
wrapper: 'rounded-lg',
errorPlaceholder: 'aspect-square h-[30vh]'
wrapper: 'rounded-lg'
}}
/>
</div>
)}
{metadata.summary && (
<div className="mt-6 mx-auto max-w-2xl text-center">
<div className="mb-2 text-[10px] uppercase tracking-[0.2em] text-muted-foreground/75">
Summary
</div>
<p className="break-words text-sm md:text-base leading-relaxed italic text-muted-foreground">
{metadata.summary}
</p>
</div>
)}
<div className="mt-5 mx-auto h-px w-24 bg-border/70" />
</div>
<div className="text-sm text-muted-foreground space-y-1">
@ -520,6 +612,24 @@ export default function PublicationIndex({ @@ -520,6 +612,24 @@ export default function PublicationIndex({
</header>
</div>
)}
{isNested && (showNestedImagePreview || showNestedSummaryPreview) && (
<div className="rounded-lg border border-border/50 bg-muted/15 px-4 py-4">
{showNestedImagePreview && metadata.image && (
<div className="mb-3 flex justify-center">
<ImageWithLightbox
image={{ url: metadata.image, pubkey: event.pubkey }}
className="max-w-[260px] w-full h-auto rounded-lg"
classNames={{ wrapper: 'rounded-lg' }}
/>
</div>
)}
{showNestedSummaryPreview && metadata.summary && (
<p className="mx-auto max-w-2xl text-sm italic leading-relaxed text-muted-foreground text-center break-words">
{metadata.summary}
</p>
)}
</div>
)}
{/* Table of Contents - only show for top-level publications */}
{!isNested && tableOfContents.length > 0 && (
@ -571,7 +681,7 @@ export default function PublicationIndex({ @@ -571,7 +681,7 @@ export default function PublicationIndex({
</div>
) : (
<div className="space-y-8">
{referencesWithEvents.map((ref, index) => {
{visibleReferences.map((ref, index) => {
const sectionKey = publicationRefKey(ref)
const coordinate = ref.coordinate || ref.eventId || ''
const sectionId = `section-${coordinate.replace(/:/g, '-')}`
@ -629,6 +739,18 @@ export default function PublicationIndex({ @@ -629,6 +739,18 @@ export default function PublicationIndex({
const eventKind = ref.event?.kind ?? ref.kind ?? 0
const effectiveParentImageUrl = !isNested ? metadata.image : parentImageUrl
const sectionSummaryTag = ref.event.tags.find((tag) => tag[0] === 'summary')?.[1]
const sectionImageTag = ref.event.tags.find((tag) => tag[0] === 'image')?.[1]
const normalizedParentSummaryForSection = (effectiveParentSummary || '').trim()
const normalizedSectionSummary = (sectionSummaryTag || '').trim()
const normalizedParentImageForSection = (effectiveParentImageUrl || '').trim()
const normalizedSectionImage = (sectionImageTag || '').trim()
const showSectionSummaryPreview =
!!normalizedSectionSummary &&
normalizedSectionSummary !== normalizedParentSummaryForSection
const showSectionImagePreview =
!!normalizedSectionImage &&
normalizedSectionImage !== normalizedParentImageForSection
if (eventKind === ExtendedKind.PUBLICATION) {
const publicationTitleTag = ref.event.tags.find((tag) => tag[0] === 'title')?.[1]
@ -706,6 +828,7 @@ export default function PublicationIndex({ @@ -706,6 +828,7 @@ export default function PublicationIndex({
event={ref.event}
isNested={true}
parentImageUrl={effectiveParentImageUrl}
parentSummary={effectiveParentSummary}
flattenHierarchy={forceFlatHierarchy}
chapterDepth={publicationDepth}
publicationFootnotesContainerId={resolvedPublicationFootnotesContainerId}
@ -736,6 +859,24 @@ export default function PublicationIndex({ @@ -736,6 +859,24 @@ export default function PublicationIndex({
)}
<NoteOptions event={ref.event} />
</div>
{(showSectionImagePreview || showSectionSummaryPreview) && (
<div className="mb-4 rounded-lg border border-border/50 bg-muted/15 px-4 py-4">
{showSectionImagePreview && sectionImageTag && (
<div className="mb-3 flex justify-center">
<ImageWithLightbox
image={{ url: sectionImageTag, pubkey: ref.event.pubkey }}
className="max-w-[260px] w-full h-auto rounded-lg"
classNames={{ wrapper: 'rounded-lg' }}
/>
</div>
)}
{showSectionSummaryPreview && sectionSummaryTag && (
<p className="mx-auto max-w-2xl text-sm italic leading-relaxed text-muted-foreground text-center break-words">
{sectionSummaryTag}
</p>
)}
</div>
)}
<AsciidocArticle
event={ref.event}
hideImagesAndInfo={true}
@ -772,6 +913,9 @@ export default function PublicationIndex({ @@ -772,6 +913,9 @@ export default function PublicationIndex({
</div>
)
})}
{sectionLoadCount < referencesWithEvents.length && (
<div ref={lazyLoadSentinelRef} className="h-8" aria-hidden />
)}
</div>
)}
{isTopLevelPublication && resolvedPublicationFootnotesContainerId && (

8
src/components/Profile/ProfilePublicationsFeed.tsx

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
import ProfileSearchBar from '@/components/ui/ProfileSearchBar'
import { ExtendedKind } from '@/constants'
import { PROFILE_PUBLICATIONS_TAB_KINDS } from '@/constants'
import { forwardRef, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -9,7 +10,11 @@ const ProfilePublicationsFeed = forwardRef<{ refresh: () => void }, { pubkey: st @@ -9,7 +10,11 @@ const ProfilePublicationsFeed = forwardRef<{ refresh: () => void }, { pubkey: st
const [searchQuery, setSearchQuery] = useState('')
const kindsList = useMemo(() => [...PROFILE_PUBLICATIONS_TAB_KINDS], [])
const cacheKey = useMemo(() => `${pubkey}-profile-publications-v2`, [pubkey])
const cacheKey = useMemo(() => `${pubkey}-profile-publications-v3`, [pubkey])
const visiblePublicationFilter = useMemo(
() => (event: { kind: number }) => event.kind !== ExtendedKind.PUBLICATION_CONTENT,
[]
)
const getKindLabel = (_kindValue: string) => t('articles and publications')
@ -30,6 +35,7 @@ const ProfilePublicationsFeed = forwardRef<{ refresh: () => void }, { pubkey: st @@ -30,6 +35,7 @@ const ProfilePublicationsFeed = forwardRef<{ refresh: () => void }, { pubkey: st
kindFilter="all"
kinds={kindsList}
cacheKey={cacheKey}
filterPredicate={visiblePublicationFilter}
getKindLabel={getKindLabel}
refreshLabel={t('Refreshing articles...')}
emptyLabel={t('No articles or publications found')}

57
src/components/SessionRelaysTab/index.tsx

@ -1,10 +1,12 @@ @@ -1,10 +1,12 @@
import client from '@/services/client.service'
import relayInfoService from '@/services/relay-info.service'
import { isHttpRelayUrl } from '@/lib/url'
import { useTranslation } from 'react-i18next'
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { RefreshCw, CheckCircle2, XCircle, Zap, RotateCcw } from 'lucide-react'
import { Button } from '@/components/ui/button'
import type { TRelayInfo } from '@/types'
import { useNostr } from '@/providers/NostrProvider'
type SessionDebug = {
strikedUrls: string[]
@ -19,6 +21,7 @@ function loadDebug(): SessionDebug { @@ -19,6 +21,7 @@ function loadDebug(): SessionDebug {
export default function SessionRelaysTab() {
const { t } = useTranslation()
const { httpRelayListEvent } = useNostr()
const [debug, setDebug] = useState<SessionDebug | null>(null)
const [relayInfoByUrl, setRelayInfoByUrl] = useState<Record<string, TRelayInfo | undefined>>({})
@ -55,8 +58,6 @@ export default function SessionRelaysTab() { @@ -55,8 +58,6 @@ export default function SessionRelaysTab() {
}
}, [debug])
if (debug === null) return null
const clearStrikeForUrl = (url: string) => {
client.clearSessionRelayStrikeForUrl(url)
refresh()
@ -77,6 +78,40 @@ export default function SessionRelaysTab() { @@ -77,6 +78,40 @@ export default function SessionRelaysTab() {
return formatRelayAddress(url)
}
const configuredHttpRelayAddresses = useMemo(() => {
const out = new Set<string>()
if (!httpRelayListEvent) return out
for (const tag of httpRelayListEvent.tags) {
if (tag[0] !== 'r' || !tag[1]) continue
const raw = tag[1].trim()
if (!isHttpRelayUrl(raw)) continue
out.add(formatRelayAddress(raw).toLowerCase())
}
return out
}, [httpRelayListEvent])
const isHttpRelayEntry = (url: string): boolean => {
if (isHttpRelayUrl(url)) return true
const infoUrl = relayInfoByUrl[url]?.url
if (infoUrl && isHttpRelayUrl(infoUrl)) return true
return configuredHttpRelayAddresses.has(formatRelayAddress(url).toLowerCase())
}
if (debug === null) return null
const RelayNameWithTransport = ({ url, mono = true }: { url: string; mono?: boolean }) => (
<span className="min-w-0 inline-flex max-w-full items-center gap-1.5">
<span className={`min-w-0 truncate ${mono ? 'font-mono' : ''}`} title={url}>
{formatRelayLabel(url)}
</span>
{isHttpRelayEntry(url) ? (
<span className="shrink-0 rounded border border-border/70 bg-muted px-1.5 py-0.5 text-[10px] font-semibold tracking-wide text-muted-foreground">
HTTP
</span>
) : null}
</span>
)
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
@ -102,8 +137,8 @@ export default function SessionRelaysTab() { @@ -102,8 +137,8 @@ export default function SessionRelaysTab() {
<li className="text-muted-foreground">{t('None')}</li>
) : (
debug.presetWorking.map((url) => (
<li key={url} className="truncate" title={url}>
{formatRelayLabel(url)}
<li key={url} className="truncate">
<RelayNameWithTransport url={url} />
</li>
))
)}
@ -124,9 +159,7 @@ export default function SessionRelaysTab() { @@ -124,9 +159,7 @@ export default function SessionRelaysTab() {
) : (
debug.presetStriked.map((url) => (
<li key={url} className="flex items-center justify-between gap-2">
<span className="min-w-0 truncate font-mono" title={url}>
{formatRelayLabel(url)}
</span>
<RelayNameWithTransport url={url} />
<Button
type="button"
variant="outline"
@ -158,9 +191,7 @@ export default function SessionRelaysTab() { @@ -158,9 +191,7 @@ export default function SessionRelaysTab() {
) : (
debug.scoredRelays.map(({ url, successCount, avgLatencyMs }) => (
<li key={url} className="flex justify-between items-center gap-2 font-mono">
<span className="truncate min-w-0" title={url}>
{formatRelayLabel(url)}
</span>
<RelayNameWithTransport url={url} />
<span className="shrink-0 text-muted-foreground text-xs">
{successCount} {t('successes')} · ~{avgLatencyMs} ms
</span>
@ -178,9 +209,7 @@ export default function SessionRelaysTab() { @@ -178,9 +209,7 @@ export default function SessionRelaysTab() {
<ul className="rounded-lg border bg-muted/30 p-3 space-y-2 text-sm">
{debug.strikedUrls.map((url) => (
<li key={url} className="flex items-center justify-between gap-2 text-muted-foreground">
<span className="min-w-0 truncate font-mono" title={url}>
{formatRelayLabel(url)}
</span>
<RelayNameWithTransport url={url} />
<Button
type="button"
variant="outline"

10
src/hooks/usePublicationSectionLoader.ts

@ -60,7 +60,11 @@ function dedupeRelayUrls(urls: string[]): string[] { @@ -60,7 +60,11 @@ function dedupeRelayUrls(urls: string[]): string[] {
return out
}
export function usePublicationSectionLoader(indexEvent: Event, refs: PublicationSectionRef[]) {
export function usePublicationSectionLoader(
indexEvent: Event,
refs: PublicationSectionRef[],
options?: { autoLoad?: boolean }
) {
const indexId = indexEvent.id
const refsSignature = useMemo(() => signatureOfRefs(refs), [refs])
const [relayUrls, setRelayUrls] = useState<string[]>([])
@ -68,6 +72,7 @@ export function usePublicationSectionLoader(indexEvent: Event, refs: Publication @@ -68,6 +72,7 @@ export function usePublicationSectionLoader(indexEvent: Event, refs: Publication
const [rows, setRows] = useState<Row[]>([])
const inflightKeysRef = useRef<Set<string>>(new Set())
const autoLoadedSignatureRef = useRef<string | null>(null)
const autoLoad = options?.autoLoad ?? true
useEffect(() => {
const cached = indexCache.get(indexId) ?? { loaded: new Map(), failed: new Set() }
@ -355,6 +360,7 @@ export function usePublicationSectionLoader(indexEvent: Event, refs: Publication @@ -355,6 +360,7 @@ export function usePublicationSectionLoader(indexEvent: Event, refs: Publication
)
useEffect(() => {
if (!autoLoad) return
if (relayUrls.length === 0) return
const sig = `${indexId}:${refsSignature}`
if (autoLoadedSignatureRef.current === sig) return
@ -365,7 +371,7 @@ export function usePublicationSectionLoader(indexEvent: Event, refs: Publication @@ -365,7 +371,7 @@ export function usePublicationSectionLoader(indexEvent: Event, refs: Publication
logger.info('[PublicationSection] flush_start', { keys: idleKeys, relayCount: relayUrls.length })
}
requestKeys(idleKeys)
}, [indexId, refsSignature, relayUrls, rows, requestKeys])
}, [autoLoad, indexId, refsSignature, relayUrls, rows, requestKeys])
const referencesWithEvents = useMemo(
() =>

16
src/lib/relay-list-builder.ts

@ -10,7 +10,7 @@ @@ -10,7 +10,7 @@
*/
import { FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS, PROFILE_FETCH_RELAY_URLS, SEARCHABLE_RELAY_URLS } from '@/constants'
import { normalizeAnyRelayUrl, normalizeUrl } from '@/lib/url'
import { isHttpRelayUrl, normalizeAnyRelayUrl, normalizeUrl } from '@/lib/url'
import { getCacheRelayUrls } from './private-relays'
import client from '@/services/client.service'
import logger from '@/lib/logger'
@ -20,6 +20,7 @@ function dedupeNormalizedRelayUrls(urls: string[]): string[] { @@ -20,6 +20,7 @@ function dedupeNormalizedRelayUrls(urls: string[]): string[] {
const seen = new Set<string>()
const out: string[] = []
for (const u of urls) {
if (isHttpRelayUrl(u)) continue
const n = normalizeAnyRelayUrl(u) || u.trim()
if (!n || seen.has(n)) continue
seen.add(n)
@ -95,6 +96,8 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio @@ -95,6 +96,8 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio
const addRelay = (url: string | undefined) => {
if (!url) return
// This builder feeds WebSocket REQ/publish lists; keep HTTP relays separate.
if (isHttpRelayUrl(url)) return
const normalized = normalizeAnyRelayUrl(url)
if (!normalized) return
// Filter blocked (case-insensitive comparison)
@ -128,13 +131,11 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio @@ -128,13 +131,11 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio
if (authorRelayList) {
const authorOutboxes = [
...(authorRelayList.httpWrite || []).slice(0, 10),
...(authorRelayList.write || []).slice(0, 10)
]
authorOutboxes.forEach(addRelay)
const authorInboxes = [
...(authorRelayList.httpRead || []).slice(0, 10),
...(authorRelayList.read || []).slice(0, 10)
]
authorInboxes.forEach(addRelay)
@ -167,11 +168,9 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio @@ -167,11 +168,9 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio
if (userRelayList) {
const userRead = [
...(userRelayList.httpRead || []).slice(0, 10),
...(userRelayList.read || []).slice(0, 10)
]
const userWrite = [
...(userRelayList.httpWrite || []).slice(0, 10),
...(userRelayList.write || []).slice(0, 10)
]
userRead.forEach(addRelay)
@ -225,7 +224,6 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio @@ -225,7 +224,6 @@ export async function buildComprehensiveRelayList(options: RelayListBuilderOptio
if (userRelayList) {
const userInboxes = [
...(userRelayList.httpRead || []).slice(0, 10),
...(userRelayList.read || []).slice(0, 10)
]
userInboxes.forEach(addRelay)
@ -342,6 +340,7 @@ export async function buildPollResultsReadRelayUrls(options: { @@ -342,6 +340,7 @@ export async function buildPollResultsReadRelayUrls(options: {
const pushLayer = (urls: string[]) => {
for (const raw of urls) {
if (isHttpRelayUrl(raw)) continue
const normalized = normalizeUrl(raw) || raw?.trim()
if (!normalized || normalizedBlocked.has(normalized.toLowerCase())) continue
if (seenNorm.has(normalized)) continue
@ -438,6 +437,7 @@ export async function buildReplyWriteRelayList( @@ -438,6 +437,7 @@ export async function buildReplyWriteRelayList(
const addRelay = (url: string | undefined) => {
if (!url) return
if (isHttpRelayUrl(url)) return
const normalized = normalizeAnyRelayUrl(url)
if (!normalized) return
// Filter blocked (case-insensitive comparison)
@ -457,13 +457,11 @@ export async function buildReplyWriteRelayList( @@ -457,13 +457,11 @@ export async function buildReplyWriteRelayList(
if (opRelayList) {
const opOutboxes = [
...(opRelayList.httpWrite || []).slice(0, 10),
...(opRelayList.write || []).slice(0, 10)
]
opOutboxes.forEach(addRelay)
const opInboxes = [
...(opRelayList.httpRead || []).slice(0, 10),
...(opRelayList.read || []).slice(0, 10)
]
opInboxes.forEach(addRelay)
@ -485,7 +483,6 @@ export async function buildReplyWriteRelayList( @@ -485,7 +483,6 @@ export async function buildReplyWriteRelayList(
if (replyToRelayList) {
const replyToInboxes = [
...(replyToRelayList.httpRead || []).slice(0, 10),
...(replyToRelayList.read || []).slice(0, 10)
]
replyToInboxes.forEach(addRelay)
@ -507,7 +504,6 @@ export async function buildReplyWriteRelayList( @@ -507,7 +504,6 @@ export async function buildReplyWriteRelayList(
if (userRelayList) {
const userOutboxes = [
...(userRelayList.httpWrite || []).slice(0, 10),
...(userRelayList.write || []).slice(0, 10)
]
userOutboxes.forEach(addRelay)

4
src/services/client.service.ts

@ -1880,7 +1880,7 @@ class ClientService extends EventTarget { @@ -1880,7 +1880,7 @@ class ClientService extends EventTarget {
relayReqLog?: { groupId?: string; onBatchEnd?: (rows: RelayOpTerminalRow[]) => void }
) {
const originalDedupedRelays = Array.from(new Set(urls))
let relays = originalDedupedRelays
let relays = originalDedupedRelays.filter((url) => !isHttpRelayUrl(url))
const filters = sanitizeSubscribeFiltersBeforeReq(filter)
if (filters.length === 0) {
logger.debug('[relay-req] batch_skip', {
@ -2578,7 +2578,7 @@ class ClientService extends EventTarget { @@ -2578,7 +2578,7 @@ class ClientService extends EventTarget {
} = {}
) {
const originalDedupedRelays = Array.from(new Set(urls))
let relays = originalDedupedRelays
let relays = originalDedupedRelays.filter((url) => !isHttpRelayUrl(url))
if (relays.length === 0) relays = [...FAST_READ_RELAY_URLS]
const filters = Array.isArray(filter) ? filter : [filter]
relays = withDocumentRelayUrlsForFilters(relays, filters)

Loading…
Cancel
Save