Browse Source

reorder trending on nostr relay

fix rendering of media
imwald
Silberengel 4 weeks ago
parent
commit
2f55f050b6
  1. 10
      src/components/Image/index.tsx
  2. 3
      src/components/ImageGallery/index.tsx
  3. 40
      src/components/ImageWithLightbox/index.tsx
  4. 2
      src/components/Note/AsciidocArticle/AsciidocArticle.tsx
  5. 2
      src/components/Note/MarkdownArticle/MarkdownArticle.tsx
  6. 4
      src/components/Note/index.tsx
  7. 16
      src/components/NoteList/index.tsx
  8. 2
      src/pages/primary/NoteListPage/RelaysFeed.tsx
  9. 6
      src/providers/ContentPolicyProvider.tsx
  10. 40
      src/providers/FeedProvider.tsx

10
src/components/Image/index.tsx

@ -67,6 +67,11 @@ export default function Image({
const [imageUrl, setImageUrl] = useState(url) const [imageUrl, setImageUrl] = useState(url)
const [fallbackIndex, setFallbackIndex] = useState(0) const [fallbackIndex, setFallbackIndex] = useState(0)
const loadWatchRef = useRef<number | null>(null) const loadWatchRef = useRef<number | null>(null)
// Track whether this image started in the held state (required an explicit click to reveal).
// The timeout is only meaningful when the user already triggered a load — for auto-revealed
// images, <img loading="lazy"> delays the browser request until the element nears the viewport,
// so a 10 s timeout would fire before off-screen images are even fetched.
const wasInitiallyHeldRef = useRef(holdUntilClick)
const finalAlt = imetaAlt || alt const finalAlt = imetaAlt || alt
const openLinkHref = const openLinkHref =
@ -102,6 +107,11 @@ export default function Image({
useEffect(() => { useEffect(() => {
clearLoadWatch() clearLoadWatch()
if (badSrc || !url?.trim() || !revealed) return if (badSrc || !url?.trim() || !revealed) return
// Skip the timeout for auto-load images (holdUntilClick was false from mount).
// Their <img loading="lazy"> request hasn't necessarily started yet when revealed
// becomes true, so the timeout would fire before the browser even fetches the image.
// For those images, onError is sufficient — it fires whenever the browser does try.
if (!wasInitiallyHeldRef.current) return
loadWatchRef.current = window.setTimeout(() => { loadWatchRef.current = window.setTimeout(() => {
loadWatchRef.current = null loadWatchRef.current = null
setIsLoading(false) setIsLoading(false)

3
src/components/ImageGallery/index.tsx

@ -107,8 +107,7 @@ export default function ImageGallery({
return ( return (
<div className={cn(displayImages.length === 1 ? 'w-fit max-w-[400px]' : 'w-full', className)}> <div className={cn(displayImages.length === 1 ? 'w-fit max-w-[400px]' : 'w-full', className)}>
{imageContent} {imageContent}
{index >= 0 && {createPortal(
createPortal(
<div onClick={(e) => e.stopPropagation()}> <div onClick={(e) => e.stopPropagation()}>
<Lightbox <Lightbox
index={index} index={index}

40
src/components/ImageWithLightbox/index.tsx

@ -31,6 +31,13 @@ export default function ImageWithLightbox({
const [display, setDisplay] = useState(autoLoadMedia) const [display, setDisplay] = useState(autoLoadMedia)
const [index, setIndex] = useState(-1) const [index, setIndex] = useState(-1)
useEffect(() => {
setDisplay(autoLoadMedia)
if (!autoLoadMedia) {
setIndex(-1)
}
}, [autoLoadMedia])
const logLightboxEvent = useCallback((stage: string, details?: Record<string, unknown>) => { const logLightboxEvent = useCallback((stage: string, details?: Record<string, unknown>) => {
logger.info('[LightboxTrace]', { logger.info('[LightboxTrace]', {
stage, stage,
@ -84,20 +91,6 @@ export default function ImageWithLightbox({
} }
}, [index, logLightboxEvent]) }, [index, logLightboxEvent])
if (!display) {
return (
<span
className="text-primary hover:underline truncate w-fit cursor-pointer inline-block"
onClick={(e) => {
e.stopPropagation()
setDisplay(true)
}}
>
[{t('Click to load image')}]
</span>
)
}
const handlePhotoClick = (event: React.MouseEvent) => { const handlePhotoClick = (event: React.MouseEvent) => {
logLightboxEvent('thumbnail-click', { logLightboxEvent('thumbnail-click', {
defaultPreventedBefore: event.defaultPrevented defaultPreventedBefore: event.defaultPrevented
@ -108,8 +101,13 @@ export default function ImageWithLightbox({
setIndex(0) setIndex(0)
} }
// The portal is always mounted (not conditional on `index >= 0`) so that React
// never removes it while yet-another-react-lightbox is mid-cleanup, which would
// otherwise cause "Node.removeChild: The node to be removed is not a child of
// this node". Visibility is controlled via the `open` prop instead.
return ( return (
<div className="max-w-[400px]"> <div className="max-w-[400px]">
{display ? (
<Image <Image
key={0} key={0}
className={className} className={className}
@ -120,8 +118,18 @@ export default function ImageWithLightbox({
image={image} image={image}
onClick={(e) => handlePhotoClick(e)} onClick={(e) => handlePhotoClick(e)}
/> />
{index >= 0 && ) : (
createPortal( <span
className="text-primary hover:underline truncate w-fit cursor-pointer inline-block"
onClick={(e) => {
e.stopPropagation()
setDisplay(true)
}}
>
[{t('Click to load image')}]
</span>
)}
{createPortal(
<div <div
data-lightbox-overlay data-lightbox-overlay
onClick={(e) => { onClick={(e) => {

2
src/components/Note/AsciidocArticle/AsciidocArticle.tsx

@ -2121,7 +2121,7 @@ export default function AsciidocArticle({
</div> </div>
{/* Image gallery lightbox */} {/* Image gallery lightbox */}
{allImages.length > 0 && lightboxIndex >= 0 && createPortal( {allImages.length > 0 && createPortal(
<div onClick={(e) => e.stopPropagation()}> <div onClick={(e) => e.stopPropagation()}>
<Lightbox <Lightbox
index={lightboxIndex} index={lightboxIndex}

2
src/components/Note/MarkdownArticle/MarkdownArticle.tsx

@ -5083,7 +5083,7 @@ export default function MarkdownArticle({
</div> </div>
{/* Image gallery lightbox */} {/* Image gallery lightbox */}
{allImages.length > 0 && lightboxOpen && createPortal( {allImages.length > 0 && createPortal(
<div <div
data-lightbox-overlay data-lightbox-overlay
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}

4
src/components/Note/index.tsx

@ -214,12 +214,12 @@ export default function Note({
className={className} className={className}
event={event} event={event}
hideMetadata={hideMetadata} hideMetadata={hideMetadata}
lazyMedia={!showFull || !autoLoadMedia} lazyMedia={!autoLoadMedia}
fullCalendarInvite={fullCalendarInvite} fullCalendarInvite={fullCalendarInvite}
/> />
) )
}, },
[event, fullCalendarInvite, showFull, autoLoadMedia] [event, fullCalendarInvite, autoLoadMedia]
) )
let content: React.ReactNode let content: React.ReactNode

16
src/components/NoteList/index.tsx

@ -122,7 +122,17 @@ const FEED_PROFILE_BATCH_DEBOUNCE_MS = 50
/** Larger chunks + parallel fetches below — sequential 36-pubkey rounds made notification avatars lag. */ /** Larger chunks + parallel fetches below — sequential 36-pubkey rounds made notification avatars lag. */
const FEED_PROFILE_CHUNK = 80 const FEED_PROFILE_CHUNK = 80
function mergeEventBatchesById(prev: Event[], incoming: Event[], cap: number): Event[] { function mergeEventBatchesById(
prev: Event[],
incoming: Event[],
cap: number,
preserveOrder = false
): Event[] {
if (preserveOrder) {
const incomingIds = new Set(incoming.map((e) => e.id))
const prevOnly = prev.filter((e) => !incomingIds.has(e.id))
return [...incoming, ...prevOnly].slice(0, cap)
}
const byId = new Map<string, Event>() const byId = new Map<string, Event>()
for (const e of prev) { for (const e of prev) {
byId.set(e.id, e) byId.set(e.id, e)
@ -1822,7 +1832,7 @@ const NoteList = forwardRef(
narrowed, narrowed,
oneShotAfterMergeComparatorRef.current oneShotAfterMergeComparatorRef.current
) )
: mergeEventBatchesById(prev, narrowed, eventCap) : mergeEventBatchesById(prev, narrowed, eventCap, areAlgoRelays)
lastEventsForTimelinePrefetchRef.current = next lastEventsForTimelinePrefetchRef.current = next
return next return next
}) })
@ -2145,7 +2155,7 @@ const NoteList = forwardRef(
if (batch.length > 0) { if (batch.length > 0) {
if (narrowed.length > 0) { if (narrowed.length > 0) {
setEvents((prev) => { setEvents((prev) => {
const next = mergeEventBatchesById(prev, narrowed, eventCapDelta) const next = mergeEventBatchesById(prev, narrowed, eventCapDelta, areAlgoRelays)
lastEventsForTimelinePrefetchRef.current = next lastEventsForTimelinePrefetchRef.current = next
return next return next
}) })

2
src/pages/primary/NoteListPage/RelaysFeed.tsx

@ -169,7 +169,7 @@ const RelaysFeed = forwardRef<
<NormalFeed <NormalFeed
ref={ref} ref={ref}
subRequests={subRequests} subRequests={subRequests}
areAlgoRelays={areAlgoRelays} areAlgoRelays={wispTrendingSingleRelay || areAlgoRelays}
isMainFeed isMainFeed
setSubHeader={setSubHeader} setSubHeader={setSubHeader}
onSubHeaderRefresh={onSubHeaderRefresh} onSubHeaderRefresh={onSubHeaderRefresh}

6
src/providers/ContentPolicyProvider.tsx

@ -64,8 +64,10 @@ export function ContentPolicyProvider({ children }: { children: React.ReactNode
if (mediaAutoLoadPolicy === MEDIA_AUTO_LOAD_POLICY.NEVER) { if (mediaAutoLoadPolicy === MEDIA_AUTO_LOAD_POLICY.NEVER) {
return false return false
} }
// WIFI_ONLY // WIFI_ONLY: block only when explicitly on cellular — connection.type returns
return connectionType === 'wifi' || connectionType === 'ethernet' // 'unknown' on Linux/Windows desktop (Network Information API is reliable only
// on Android/ChromeOS), so an allowlist would wrongly block desktop wifi.
return connectionType !== 'cellular'
}, [mediaAutoLoadPolicy, connectionType]) }, [mediaAutoLoadPolicy, connectionType])
const updateAutoplay = (autoplay: boolean) => { const updateAutoplay = (autoplay: boolean) => {

40
src/providers/FeedProvider.tsx

@ -1,13 +1,14 @@
import { DEFAULT_FAVORITE_RELAYS } from '@/constants' import { DEFAULT_FAVORITE_RELAYS } from '@/constants'
import { getFavoritesFeedRelayUrls } from '@/lib/favorites-feed-relays' import { getFavoritesFeedRelayUrls, mergeRelayUrlLayers } from '@/lib/favorites-feed-relays'
import { getRelaySetFromEvent } from '@/lib/event-metadata' import { getRelaySetFromEvent, getRelayListFromEvent, getHttpRelayListFromEvent } from '@/lib/event-metadata'
import logger from '@/lib/logger' import logger from '@/lib/logger'
import { isHttpRelayUrl, isWebsocketUrl, normalizeAnyRelayUrl } from '@/lib/url' import { isHttpRelayUrl, isWebsocketUrl, normalizeAnyRelayUrl } from '@/lib/url'
import { buildWispTrendingNotesRelayUrl } from '@/lib/wisp-trending-relay'
import indexedDb from '@/services/indexed-db.service' import indexedDb from '@/services/indexed-db.service'
import storage from '@/services/local-storage.service' import storage from '@/services/local-storage.service'
import { TFeedInfo, TFeedType } from '@/types' import { TFeedInfo, TFeedType } from '@/types'
import { kinds } from 'nostr-tools' import { kinds } from 'nostr-tools'
import { useEffect, useRef, useState, useCallback } from 'react' import { useEffect, useMemo, useRef, useState, useCallback } from 'react'
import { FeedContext } from './feed-context' import { FeedContext } from './feed-context'
import { useFavoriteRelays } from './FavoriteRelaysProvider' import { useFavoriteRelays } from './FavoriteRelaysProvider'
import { useNostr } from './NostrProvider' import { useNostr } from './NostrProvider'
@ -16,8 +17,27 @@ export { useFeed } from './feed-context'
export type { TFeedContext } from './feed-context' export type { TFeedContext } from './feed-context'
export function FeedProvider({ children }: { children: React.ReactNode }) { export function FeedProvider({ children }: { children: React.ReactNode }) {
const { pubkey, isInitialized } = useNostr() const { pubkey, isInitialized, cacheRelayListEvent, httpRelayListEvent } = useNostr()
const { relaySets, favoriteRelays, blockedRelays } = useFavoriteRelays() const { relaySets, favoriteRelays, blockedRelays } = useFavoriteRelays()
/**
* Extra relay URLs always merged into the all-favorites feed:
* - Cache relays (kind 10432) if the user has configured any
* - HTTP index relays (kind 10243) if the user has configured any
* - The Wisp trending relay (always included)
*/
const extraFeedRelayUrls = useMemo(() => {
const extra: string[] = [buildWispTrendingNotesRelayUrl()]
if (cacheRelayListEvent) {
const list = getRelayListFromEvent(cacheRelayListEvent)
extra.push(...list.read, ...list.write)
}
if (httpRelayListEvent) {
const list = getHttpRelayListFromEvent(httpRelayListEvent)
extra.push(...list.httpRead, ...list.httpWrite)
}
return extra
}, [cacheRelayListEvent, httpRelayListEvent])
const [relayUrls, setRelayUrls] = useState<string[]>([]) const [relayUrls, setRelayUrls] = useState<string[]>([])
const [isReady, setIsReady] = useState(false) const [isReady, setIsReady] = useState(false)
const [feedInfo, setFeedInfo] = useState<TFeedInfo>({ const [feedInfo, setFeedInfo] = useState<TFeedInfo>({
@ -103,7 +123,8 @@ export function FeedProvider({ children }: { children: React.ReactNode }) {
return return
} }
if (feedType === 'all-favorites') { if (feedType === 'all-favorites') {
const finalRelays = getFavoritesFeedRelayUrls(favoriteRelays, blockedRelays) const baseRelays = getFavoritesFeedRelayUrls(favoriteRelays, blockedRelays)
const finalRelays = mergeRelayUrlLayers([baseRelays, extraFeedRelayUrls], blockedRelays)
logger.debug('Switching to all-favorites, finalRelays:', finalRelays) logger.debug('Switching to all-favorites, finalRelays:', finalRelays)
const newFeedInfo = { feedType } const newFeedInfo = { feedType }
setFeedInfo(newFeedInfo) setFeedInfo(newFeedInfo)
@ -116,7 +137,7 @@ export function FeedProvider({ children }: { children: React.ReactNode }) {
return return
} }
setIsReady(true) setIsReady(true)
}, [pubkey, favoriteRelays, blockedRelays, relaySets]) }, [pubkey, favoriteRelays, blockedRelays, relaySets, extraFeedRelayUrls])
useEffect(() => { useEffect(() => {
const init = async () => { const init = async () => {
@ -199,13 +220,14 @@ export function FeedProvider({ children }: { children: React.ReactNode }) {
init() init()
}, [pubkey, isInitialized, favoriteRelays, blockedRelays, switchFeed]) }, [pubkey, isInitialized, favoriteRelays, blockedRelays, switchFeed])
// Update relay URLs when favoriteRelays change and we're in all-favorites mode // Update relay URLs when favoriteRelays, blocked, or extra relay lists change while in all-favorites mode
useEffect(() => { useEffect(() => {
if (feedInfo.feedType !== 'all-favorites') return if (feedInfo.feedType !== 'all-favorites') return
const finalRelays = getFavoritesFeedRelayUrls(favoriteRelays, blockedRelays) const baseRelays = getFavoritesFeedRelayUrls(favoriteRelays, blockedRelays)
const finalRelays = mergeRelayUrlLayers([baseRelays, extraFeedRelayUrls], blockedRelays)
logger.debug('Updating relay URLs for all-favorites:', finalRelays) logger.debug('Updating relay URLs for all-favorites:', finalRelays)
setRelayUrls(finalRelays) setRelayUrls(finalRelays)
}, [feedInfo.feedType, favoriteRelays, blockedRelays]) }, [feedInfo.feedType, favoriteRelays, blockedRelays, extraFeedRelayUrls])
return ( return (
<FeedContext.Provider <FeedContext.Provider

Loading…
Cancel
Save