diff --git a/src/components/Content/index.tsx b/src/components/Content/index.tsx index 25c5f956..ba68b976 100644 --- a/src/components/Content/index.tsx +++ b/src/components/Content/index.tsx @@ -92,6 +92,7 @@ export default function Content({ mustLoadMedia?: boolean }) { const _content = event?.content ?? content + const authorPubkey = event?.pubkey const iArticleUrl = useMemo(() => (event ? getHttpUrlFromITags(event) : undefined), [event]) const iArticleCleaned = useMemo( () => (iArticleUrl ? cleanUrl(iArticleUrl) || iArticleUrl : ''), @@ -485,7 +486,7 @@ export default function Content({
{iArticleUrl && (
- +
)} {/* Render images that appear in content in a single carousel at the top */} @@ -497,6 +498,7 @@ export default function Content({ start={0} end={imagesInContent.length} mustLoad={mustLoadMedia} + authorPubkey={authorPubkey} /> )} @@ -510,6 +512,7 @@ export default function Content({ start={0} end={carouselImages.length} mustLoad={mustLoadMedia} + authorPubkey={authorPubkey} /> )} @@ -520,6 +523,7 @@ export default function Content({ src={video.url} className="w-full max-w-full" mustLoad={mustLoadMedia} + authorPubkey={authorPubkey} deferLoadUntilClick={deferLongVideoLoad} poster={video.image || video.thumb} blurHash={video.blurHash} @@ -534,6 +538,7 @@ export default function Content({ src={audio.url} className="mt-2" mustLoad={mustLoadMedia} + authorPubkey={authorPubkey} poster={audio.thumb} blurHash={audio.blurHash} /> @@ -546,6 +551,7 @@ export default function Content({ url={url} className="mt-2" mustLoad={mustLoadMedia} + authorPubkey={authorPubkey} /> ))} @@ -555,6 +561,7 @@ export default function Content({ url={url} className="mt-2" mustLoad={mustLoadMedia} + authorPubkey={authorPubkey} /> ))} @@ -564,6 +571,7 @@ export default function Content({ url={url} className="mt-2" mustLoad={mustLoadMedia} + authorPubkey={authorPubkey} /> ))} @@ -573,6 +581,7 @@ export default function Content({ url={url} className="mt-2" mustLoad={mustLoadMedia} + authorPubkey={authorPubkey} /> ))} @@ -613,6 +622,7 @@ export default function Content({ key={index} src={cleanedUrl} mustLoad={mustLoadMedia} + authorPubkey={authorPubkey} deferLoadUntilClick={deferLongVideoLoad} poster={tagMediaInfo?.image || tagMediaInfo?.thumb} blurHash={tagMediaInfo?.blurHash} @@ -642,6 +652,7 @@ export default function Content({ key={`url-media-${index}`} src={cleanedUrl} mustLoad={mustLoadMedia} + authorPubkey={authorPubkey} deferLoadUntilClick={deferLongVideoLoad} poster={poster} blurHash={mediaInfo?.blurHash} @@ -667,6 +678,7 @@ export default function Content({ start={0} end={1} mustLoad={mustLoadMedia} + authorPubkey={authorPubkey} /> ) } @@ -729,6 +741,7 @@ export default function Content({ url={node.data} className="mt-2" mustLoad={mustLoadMedia} + authorPubkey={authorPubkey} /> ) } @@ -739,6 +752,7 @@ export default function Content({ url={node.data} className="mt-2" mustLoad={mustLoadMedia} + authorPubkey={authorPubkey} /> ) } @@ -749,6 +763,7 @@ export default function Content({ url={node.data} className="mt-2" mustLoad={mustLoadMedia} + authorPubkey={authorPubkey} /> ) } @@ -759,6 +774,7 @@ export default function Content({ url={node.data} className="mt-2" mustLoad={mustLoadMedia} + authorPubkey={authorPubkey} /> ) } @@ -781,7 +797,7 @@ export default function Content({

Links

{contentLinks.map((url, index) => ( - + ))}
)} @@ -791,7 +807,7 @@ export default function Content({

Related Links

{tagLinks.map((url, index) => ( - + ))}
)} diff --git a/src/components/FountainEmbeddedPlayer/index.tsx b/src/components/FountainEmbeddedPlayer/index.tsx index c43a15c2..a37dcdbf 100644 --- a/src/components/FountainEmbeddedPlayer/index.tsx +++ b/src/components/FountainEmbeddedPlayer/index.tsx @@ -8,7 +8,7 @@ import { } from '@/lib/fountain-url' import { cleanUrl } from '@/lib/url' import { cn } from '@/lib/utils' -import { useContentPolicyOptional } from '@/providers/ContentPolicyProvider' +import { useShouldAutoLoadMedia } from '@/hooks/useShouldAutoLoadMedia' import { Skeleton } from '@/components/ui/skeleton' import { useLayoutEffect, useMemo, useState } from 'react' import LazyMediaTapPlaceholder from '../MediaPlayer/LazyMediaTapPlaceholder' @@ -68,14 +68,15 @@ const cardShell = (className?: string) => export default function FountainEmbeddedPlayer({ url, className, - mustLoad = false + mustLoad = false, + authorPubkey }: { url: string className?: string mustLoad?: boolean + authorPubkey?: string | null }) { - const contentPolicy = useContentPolicyOptional() - const autoLoadMedia = contentPolicy?.autoLoadMedia ?? true + const autoLoadMedia = useShouldAutoLoadMedia(authorPubkey) const [userClickedLoad, setUserClickedLoad] = useState(false) const cleanedUrl = useMemo(() => cleanUrl(url) || url, [url]) const minHeight = useMemo(() => fountainEmbedMinHeight(cleanedUrl), [cleanedUrl]) diff --git a/src/components/Image/index.tsx b/src/components/Image/index.tsx index 76c5fab1..035008ba 100644 --- a/src/components/Image/index.tsx +++ b/src/components/Image/index.tsx @@ -22,7 +22,7 @@ import { useRef, useState } from 'react' -import { useContentPolicyOptional } from '@/providers/ContentPolicyProvider' +import { useShouldAutoLoadMedia } from '@/hooks/useShouldAutoLoadMedia' import { useTranslation } from 'react-i18next' /** Browsers often never fire `onError` for invalid URIs, ORB, or stalled fetches — this forces a visible error. */ @@ -65,7 +65,7 @@ function extensionWithDotFromUrl(url: string): string { } export default function Image({ - image: { url, blurHash, dim, alt: imetaAlt, fallback, size: fileSizeBytes, x: imetaHash }, + image: { url, blurHash, dim, alt: imetaAlt, fallback, size: fileSizeBytes, x: imetaHash, pubkey }, alt, className = '', classNames = {}, @@ -108,10 +108,9 @@ export default function Image({ holdUntilClick?: boolean }) { const { t } = useTranslation() - const contentPolicy = useContentPolicyOptional() + const autoLoadForAuthor = useShouldAutoLoadMedia(pubkey) /** Tap-to-load only if the parent asked and policy allows (or there is no policy — trust the parent). */ - const effectiveHoldUntilClick = - holdUntilClick && (contentPolicy !== undefined ? !contentPolicy.autoLoadMedia : true) + const effectiveHoldUntilClick = holdUntilClick && !autoLoadForAuthor const urlOk = !!url?.trim() const [revealed, setRevealed] = useState(!effectiveHoldUntilClick) diff --git a/src/components/ImageGallery/index.tsx b/src/components/ImageGallery/index.tsx index 43635a58..d3f9e89e 100644 --- a/src/components/ImageGallery/index.tsx +++ b/src/components/ImageGallery/index.tsx @@ -1,7 +1,7 @@ import { randomString } from '@/lib/random' import { cn } from '@/lib/utils' import logger from '@/lib/logger' -import { useContentPolicyOptional } from '@/providers/ContentPolicyProvider' +import { useShouldAutoLoadMedia } from '@/hooks/useShouldAutoLoadMedia' import modalManager from '@/services/modal-manager.service' import { TImetaInfo } from '@/types' import { ReactNode, useEffect, useLayoutEffect, useMemo, useState } from 'react' @@ -23,17 +23,18 @@ export default function ImageGallery({ images, start = 0, end = images.length, - mustLoad = false + mustLoad = false, + authorPubkey }: { className?: string images: TImetaInfo[] start?: number end?: number mustLoad?: boolean + authorPubkey?: string | null }) { const id = useMemo(() => `image-gallery-${randomString()}`, []) - const contentPolicy = useContentPolicyOptional() - const autoLoadMedia = contentPolicy?.autoLoadMedia ?? true + const autoLoadMedia = useShouldAutoLoadMedia(authorPubkey ?? images[start]?.pubkey) const [index, setIndex] = useState(-1) const [lightboxPortalActive, setLightboxPortalActive] = useState(false) diff --git a/src/components/ImageWithLightbox/index.tsx b/src/components/ImageWithLightbox/index.tsx index f31c8934..943be8f3 100644 --- a/src/components/ImageWithLightbox/index.tsx +++ b/src/components/ImageWithLightbox/index.tsx @@ -1,6 +1,6 @@ import { randomString } from '@/lib/random' import { cn } from '@/lib/utils' -import { useContentPolicyOptional } from '@/providers/ContentPolicyProvider' +import { useShouldAutoLoadMedia } from '@/hooks/useShouldAutoLoadMedia' import modalManager from '@/services/modal-manager.service' import { TImetaInfo } from '@/types' import { useEffect, useMemo, useState } from 'react' @@ -28,8 +28,7 @@ export default function ImageWithLightbox({ mustLoad?: boolean }) { const id = useMemo(() => `image-with-lightbox-${randomString()}`, []) - const contentPolicy = useContentPolicyOptional() - const autoLoadMedia = contentPolicy?.autoLoadMedia ?? true + const autoLoadMedia = useShouldAutoLoadMedia(image.pubkey) const [index, setIndex] = useState(-1) const [lightboxPortalActive, setLightboxPortalActive] = useState(false) diff --git a/src/components/MediaPlayer/index.tsx b/src/components/MediaPlayer/index.tsx index 6ea0c594..27d44adf 100644 --- a/src/components/MediaPlayer/index.tsx +++ b/src/components/MediaPlayer/index.tsx @@ -6,7 +6,7 @@ import { resolvePrimalBlossomPlayableUrl } from '@/lib/url' import { cn } from '@/lib/utils' -import { useContentPolicy } from '@/providers/ContentPolicyProvider' +import { useShouldAutoLoadMedia } from '@/hooks/useShouldAutoLoadMedia' import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import AudioPlayer from '../AudioPlayer' @@ -48,6 +48,7 @@ export default function MediaPlayer({ className, mustLoad = false, deferLoadUntilClick = false, + authorPubkey, poster, blurHash, fallbackPageUrl @@ -60,6 +61,8 @@ export default function MediaPlayer({ * placeholder. Used for NIP-71 long-form video events in feeds. */ deferLoadUntilClick?: boolean + /** Note author; when set, follow-only and related policies apply per author. */ + authorPubkey?: string | null poster?: string /** NIP-94 / imeta blurhash for lazy placeholder when poster is missing */ blurHash?: string @@ -67,8 +70,8 @@ export default function MediaPlayer({ fallbackPageUrl?: string }) { const { t } = useTranslation() - const { autoLoadMedia } = useContentPolicy() - /** Tap-to-load when {@link autoLoadMedia} is off; cleared when policy switches back to never. */ + const authorAutoLoad = useShouldAutoLoadMedia(authorPubkey) + /** Tap-to-load when auto-load is off for this author; cleared when policy switches back to never. */ const [userClickedLoad, setUserClickedLoad] = useState(false) const [mediaType, setMediaType] = useState(null) const [probeFailed, setProbeFailed] = useState(false) @@ -96,11 +99,11 @@ export default function MediaPlayer({ const effectiveMediaType = mediaType ?? urlEmbedSurfaceHint const showEmbed = - mustLoad || (!deferLoadUntilClick && autoLoadMedia) || userClickedLoad + mustLoad || (!deferLoadUntilClick && authorAutoLoad) || userClickedLoad useLayoutEffect(() => { - if (!autoLoadMedia) setUserClickedLoad(false) - }, [autoLoadMedia]) + if (!authorAutoLoad) setUserClickedLoad(false) + }, [authorAutoLoad]) useEffect(() => { readyOnceRef.current = false diff --git a/src/components/Note/ArticleCardCoverImage.tsx b/src/components/Note/ArticleCardCoverImage.tsx index c362e72d..70e4c75f 100644 --- a/src/components/Note/ArticleCardCoverImage.tsx +++ b/src/components/Note/ArticleCardCoverImage.tsx @@ -1,5 +1,6 @@ import ContentImage from '@/components/Image' import UserAvatar from '@/components/UserAvatar' +import { useShouldAutoLoadMedia } from '@/hooks/useShouldAutoLoadMedia' import { cn } from '@/lib/utils' import type { Event } from 'nostr-tools' @@ -9,19 +10,22 @@ import type { Event } from 'nostr-tools' export default function ArticleCardCoverImage({ event, imageUrl, - autoLoadMedia, + autoLoadMedia: autoLoadMediaProp, layout, hideImageIfError = false }: { event: Event imageUrl?: string - autoLoadMedia: boolean + /** Deprecated: prefer per-author policy via {@link useShouldAutoLoadMedia}. Kept for callers that pass it. */ + autoLoadMedia?: boolean layout: 'stacked' | 'row' /** Passed through to {@link ContentImage} when an `image` tag URL exists. */ hideImageIfError?: boolean }) { + const autoLoadFromPolicy = useShouldAutoLoadMedia(event.pubkey) + const autoLoadMedia = autoLoadMediaProp ?? autoLoadFromPolicy const trimmed = imageUrl?.trim() - if (trimmed && autoLoadMedia) { + if (trimmed) { return ( ) } - if (trimmed) return null return (
getCommunityDefinitionFromEvent(event), [event]) const communityNameComponent = ( @@ -27,13 +26,14 @@ export default function CommunityDefinition({ return (
- {metadata.image && autoLoadMedia && ( + {metadata.image ? ( - )} + ) : null}
{communityNameComponent} {communityDescriptionComponent} diff --git a/src/components/Note/GroupMetadata.tsx b/src/components/Note/GroupMetadata.tsx index 519ec31a..e45a6e94 100644 --- a/src/components/Note/GroupMetadata.tsx +++ b/src/components/Note/GroupMetadata.tsx @@ -1,5 +1,5 @@ +import { useShouldAutoLoadMedia } from '@/hooks/useShouldAutoLoadMedia' import { getGroupMetadataFromEvent } from '@/lib/event-metadata' -import { useContentPolicyOptional } from '@/providers/ContentPolicyProvider' import { Event } from 'nostr-tools' import { useMemo } from 'react' import ClientSelect from '../ClientSelect' @@ -14,8 +14,7 @@ export default function GroupMetadata({ originalNoteId?: string className?: string }) { - const contentPolicy = useContentPolicyOptional() - const autoLoadMedia = contentPolicy?.autoLoadMedia ?? true + const autoLoadMedia = useShouldAutoLoadMedia(event.pubkey) const metadata = useMemo(() => getGroupMetadataFromEvent(event), [event]) const groupNameComponent = ( @@ -29,13 +28,14 @@ export default function GroupMetadata({ return (
- {metadata.picture && autoLoadMedia && ( + {metadata.picture ? ( - )} + ) : null}
{groupNameComponent} {groupAboutComponent} diff --git a/src/components/Note/LiveEvent.tsx b/src/components/Note/LiveEvent.tsx index 14e5de5a..12de8174 100644 --- a/src/components/Note/LiveEvent.tsx +++ b/src/components/Note/LiveEvent.tsx @@ -9,7 +9,7 @@ import { preferredLiveJoinUrlForEvent } from '@/lib/live-activities' import { cn } from '@/lib/utils' -import { useContentPolicyOptional } from '@/providers/ContentPolicyProvider' +import { useShouldAutoLoadMedia } from '@/hooks/useShouldAutoLoadMedia' import { useLiveActivitiesOptional } from '@/providers/useLiveActivities' import { useScreenSizeOptional } from '@/providers/ScreenSizeProvider' import { Event, kinds } from 'nostr-tools' @@ -26,8 +26,7 @@ export default function LiveEvent({ event, className }: { event: Event; classNam const liveActivities = useLiveActivitiesOptional() const screenSize = useScreenSizeOptional() const isSmallScreen = screenSize?.isSmallScreen ?? false - const contentPolicy = useContentPolicyOptional() - const autoLoadMedia = contentPolicy?.autoLoadMedia ?? true + const autoLoadMedia = useShouldAutoLoadMedia(event.pubkey) const metadata = useMemo(() => getLiveEventMetadataFromEvent(event), [event]) const playback = useMemo(() => liveEventInlinePlaybackFromEvent(event), [event]) const joinUrl = useMemo(() => preferredLiveJoinUrlForEvent(event), [event]) @@ -87,7 +86,7 @@ export default function LiveEvent({ event, className }: { event: Event; classNam
@@ -168,6 +167,7 @@ export default function LiveEvent({ event, className }: { event: Event; classNam src={playback.src} poster={inlinePlayerPoster} className="w-full" + authorPubkey={event.pubkey} fallbackPageUrl={zapStreamFallbackUrl ?? joinUrl ?? undefined} />
diff --git a/src/components/Note/LongFormCard.tsx b/src/components/Note/LongFormCard.tsx index 7fbd5c5b..2c8cdee4 100644 --- a/src/components/Note/LongFormCard.tsx +++ b/src/components/Note/LongFormCard.tsx @@ -2,7 +2,7 @@ import { cardEventBodyBlurb } from '@/lib/card-event-body-blurb' import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata' import { toNote, toNoteList } from '@/lib/link' import { useSecondaryPageOptional } from '@/PageManager' -import { useContentPolicyOptional } from '@/providers/ContentPolicyProvider' +import { useShouldAutoLoadMedia } from '@/hooks/useShouldAutoLoadMedia' import { useScreenSizeOptional } from '@/providers/ScreenSizeProvider' import { cn } from '@/lib/utils' import { Event, kinds } from 'nostr-tools' @@ -31,8 +31,7 @@ export default function LongFormCard({ const push = secondaryPage?.push ?? ((url: string) => { window.location.href = url }) - const contentPolicy = useContentPolicyOptional() - const autoLoadMedia = contentPolicy?.autoLoadMedia ?? true + const autoLoadMedia = useShouldAutoLoadMedia(event.pubkey) const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event]) const bodyBlurb = useMemo(() => cardEventBodyBlurb(event.content), [event.content]) const summaryText = (metadata.summary?.trim() || bodyBlurb).trim() diff --git a/src/components/Note/MusicTrackNote.tsx b/src/components/Note/MusicTrackNote.tsx index 7c6ad569..08381163 100644 --- a/src/components/Note/MusicTrackNote.tsx +++ b/src/components/Note/MusicTrackNote.tsx @@ -7,7 +7,7 @@ import { } from '@/lib/music-track' import { primalR2aMirrorForBlossomPrimalUrl } from '@/lib/url' import { cn } from '@/lib/utils' -import { useContentPolicyOptional } from '@/providers/ContentPolicyProvider' +import { useShouldAutoLoadMedia } from '@/hooks/useShouldAutoLoadMedia' import { Event } from 'nostr-tools' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' @@ -42,8 +42,7 @@ export default function MusicTrackNote({ className?: string loadMedia?: boolean }) { - const contentPolicy = useContentPolicyOptional() - const autoLoadMedia = contentPolicy?.autoLoadMedia ?? true + const autoLoadMedia = useShouldAutoLoadMedia(event.pubkey) const mustLoad = loadMedia || autoLoadMedia const { t } = useTranslation() @@ -107,7 +106,12 @@ export default function MusicTrackNote({

{t('Music video', { defaultValue: 'Music video' })}

- +
) : null}
diff --git a/src/components/Note/PublicationCard.tsx b/src/components/Note/PublicationCard.tsx index fc21f55e..89787b3e 100644 --- a/src/components/Note/PublicationCard.tsx +++ b/src/components/Note/PublicationCard.tsx @@ -3,7 +3,7 @@ import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata' import { toNote, toNoteList } from '@/lib/link' import { cn } from '@/lib/utils' import { useSecondaryPageOptional, useSmartNoteNavigationOptional } from '@/PageManager' -import { useContentPolicyOptional } from '@/providers/ContentPolicyProvider' +import { useShouldAutoLoadMedia } from '@/hooks/useShouldAutoLoadMedia' import { useScreenSizeOptional } from '@/providers/ScreenSizeProvider' import { Event, kinds } from 'nostr-tools' import { useMemo } from 'react' @@ -26,8 +26,7 @@ export default function PublicationCard({ const { navigateToNote } = useSmartNoteNavigationOptional() const secondaryPage = useSecondaryPageOptional() const push = secondaryPage?.push ?? ((url: string) => { window.location.href = url }) - const contentPolicy = useContentPolicyOptional() - const autoLoadMedia = contentPolicy?.autoLoadMedia ?? true + const autoLoadMedia = useShouldAutoLoadMedia(event.pubkey) const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event]) const bodyBlurb = useMemo(() => cardEventBodyBlurb(event.content), [event.content]) const summaryText = (metadata.summary?.trim() || bodyBlurb).trim() @@ -93,13 +92,14 @@ export default function PublicationCard({ )} onClick={disableNavigation ? undefined : handleCardClick} > - {metadata.image && autoLoadMedia && ( + {metadata.image ? ( - )} + ) : null}
{titleComponent} {bookstrMetadataComponent} @@ -122,14 +122,15 @@ export default function PublicationCard({ onClick={disableNavigation ? undefined : handleCardClick} >
- {metadata.image && autoLoadMedia && ( + {metadata.image ? ( - )} + ) : null}
{titleComponent} {bookstrMetadataComponent} diff --git a/src/components/Note/WikiCard.tsx b/src/components/Note/WikiCard.tsx index ed18b43a..ff73dc07 100644 --- a/src/components/Note/WikiCard.tsx +++ b/src/components/Note/WikiCard.tsx @@ -2,7 +2,7 @@ import { cardEventBodyBlurb } from '@/lib/card-event-body-blurb' import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata' import { toNote, toNoteList } from '@/lib/link' import { useSecondaryPageOptional } from '@/PageManager' -import { useContentPolicyOptional } from '@/providers/ContentPolicyProvider' +import { useShouldAutoLoadMedia } from '@/hooks/useShouldAutoLoadMedia' import { useScreenSizeOptional } from '@/providers/ScreenSizeProvider' import { Event, kinds } from 'nostr-tools' import { useMemo } from 'react' @@ -19,8 +19,7 @@ export default function WikiCard({ const isSmallScreen = screenSize?.isSmallScreen ?? false const secondaryPage = useSecondaryPageOptional() const push = secondaryPage?.push ?? ((url: string) => { window.location.href = url }) - const contentPolicy = useContentPolicyOptional() - const autoLoadMedia = contentPolicy?.autoLoadMedia ?? true + const autoLoadMedia = useShouldAutoLoadMedia(event.pubkey) const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event]) const bodyBlurb = useMemo(() => cardEventBodyBlurb(event.content), [event.content]) const summaryText = (metadata.summary?.trim() || bodyBlurb).trim() diff --git a/src/components/Note/index.tsx b/src/components/Note/index.tsx index 2beb30a2..007921ca 100644 --- a/src/components/Note/index.tsx +++ b/src/components/Note/index.tsx @@ -24,6 +24,7 @@ import { } from '@/hooks/useNotificationReactionDisplay' import logger from '@/lib/logger' import client from '@/services/client.service' +import { useShouldAutoLoadMedia } from '@/hooks/useShouldAutoLoadMedia' import { useContentPolicyOptional } from '@/providers/ContentPolicyProvider' import { useMuteListOptional } from '@/contexts/mute-list-context' import { muteSetHas } from '@/lib/mute-set' @@ -277,7 +278,7 @@ export default function Note({ const parentFetchRelayHints = useMemo(() => relayHintsFromEventTags(event), [event]) const contentPolicy = useContentPolicyOptional() const defaultShowNsfw = contentPolicy?.defaultShowNsfw ?? true - const autoLoadMedia = contentPolicy?.autoLoadMedia ?? true + const autoLoadMedia = useShouldAutoLoadMedia(event.pubkey) const [showNsfw, setShowNsfw] = useState(false) const muteList = useMuteListOptional() const mutePubkeySet = muteList?.mutePubkeySet ?? new Set() diff --git a/src/components/SpotifyEmbeddedPlayer/index.tsx b/src/components/SpotifyEmbeddedPlayer/index.tsx index ab2daa3b..1c0c8f39 100644 --- a/src/components/SpotifyEmbeddedPlayer/index.tsx +++ b/src/components/SpotifyEmbeddedPlayer/index.tsx @@ -1,6 +1,6 @@ +import { useShouldAutoLoadMedia } from '@/hooks/useShouldAutoLoadMedia' import { spotifyOpenUrlToEmbedSrc } from '@/lib/spotify-url' import { cn } from '@/lib/utils' -import { useContentPolicyOptional } from '@/providers/ContentPolicyProvider' import { useLayoutEffect, useMemo, useState } from 'react' import ExternalLink from '../ExternalLink' import LazyMediaTapPlaceholder from '../MediaPlayer/LazyMediaTapPlaceholder' @@ -8,14 +8,15 @@ import LazyMediaTapPlaceholder from '../MediaPlayer/LazyMediaTapPlaceholder' export default function SpotifyEmbeddedPlayer({ url, className, - mustLoad = false + mustLoad = false, + authorPubkey }: { url: string className?: string mustLoad?: boolean + authorPubkey?: string | null }) { - const contentPolicy = useContentPolicyOptional() - const autoLoadMedia = contentPolicy?.autoLoadMedia ?? true + const autoLoadMedia = useShouldAutoLoadMedia(authorPubkey) const [userClickedLoad, setUserClickedLoad] = useState(false) const embedSrc = useMemo(() => spotifyOpenUrlToEmbedSrc(url), [url]) const showEmbed = mustLoad || autoLoadMedia || userClickedLoad diff --git a/src/components/WavlakeEmbeddedPlayer/index.tsx b/src/components/WavlakeEmbeddedPlayer/index.tsx index 41b69d5c..a64ef401 100644 --- a/src/components/WavlakeEmbeddedPlayer/index.tsx +++ b/src/components/WavlakeEmbeddedPlayer/index.tsx @@ -3,8 +3,8 @@ import { wavlakeEmbedMinHeight, wavlakeOpenUrlToEmbedSrc } from '@/lib/wavlake-url' +import { useShouldAutoLoadMedia } from '@/hooks/useShouldAutoLoadMedia' import { cn } from '@/lib/utils' -import { useContentPolicyOptional } from '@/providers/ContentPolicyProvider' import { useLayoutEffect, useMemo, useState } from 'react' import ExternalLink from '../ExternalLink' import LazyMediaTapPlaceholder from '../MediaPlayer/LazyMediaTapPlaceholder' @@ -12,14 +12,15 @@ import LazyMediaTapPlaceholder from '../MediaPlayer/LazyMediaTapPlaceholder' export default function WavlakeEmbeddedPlayer({ url, className, - mustLoad = false + mustLoad = false, + authorPubkey }: { url: string className?: string mustLoad?: boolean + authorPubkey?: string | null }) { - const contentPolicy = useContentPolicyOptional() - const autoLoadMedia = contentPolicy?.autoLoadMedia ?? true + const autoLoadMedia = useShouldAutoLoadMedia(authorPubkey) const [userClickedLoad, setUserClickedLoad] = useState(false) const embedSrc = useMemo(() => wavlakeOpenUrlToEmbedSrc(url), [url]) const minHeight = useMemo(() => wavlakeEmbedMinHeight(url), [url]) diff --git a/src/components/WebPreview/index.tsx b/src/components/WebPreview/index.tsx index 6ad23406..fa290118 100644 --- a/src/components/WebPreview/index.tsx +++ b/src/components/WebPreview/index.tsx @@ -5,7 +5,7 @@ import { ExtendedKind } from '@/constants' import { getLongFormArticleMetadataFromEvent, dTagToTitleCase } from '@/lib/event-metadata' import { extractBookMetadata } from '@/lib/bookstr-parser' import { cn } from '@/lib/utils' -import { useContentPolicy } from '@/providers/ContentPolicyProvider' +import { useShouldAutoLoadMedia } from '@/hooks/useShouldAutoLoadMedia' import { useScreenSize } from '@/providers/ScreenSizeProvider' import { Skeleton } from '@/components/ui/skeleton' import { ExternalLink } from 'lucide-react' @@ -134,8 +134,16 @@ function getTitleWithFallbacks(event: Event | null, eventMetadata: { title?: str return null } -export default function WebPreview({ url, className }: { url: string; className?: string }) { - const { autoLoadMedia } = useContentPolicy() +export default function WebPreview({ + url, + className, + authorPubkey +}: { + url: string + className?: string + authorPubkey?: string | null +}) { + const autoLoadMedia = useShouldAutoLoadMedia(authorPubkey) const { isSmallScreen } = useScreenSize() const cleanedUrl = useMemo(() => cleanUrl(url), [url]) diff --git a/src/components/YoutubeEmbeddedPlayer/index.tsx b/src/components/YoutubeEmbeddedPlayer/index.tsx index 59590451..55e3b1d4 100644 --- a/src/components/YoutubeEmbeddedPlayer/index.tsx +++ b/src/components/YoutubeEmbeddedPlayer/index.tsx @@ -1,8 +1,8 @@ import { isImwaldElectron } from '@/lib/client-platform' import { ensureYouTubeIframeApi } from '@/lib/youtube-iframe-api' import { parseYoutubeUrl } from '@/lib/youtube-url' +import { useShouldAutoLoadMedia } from '@/hooks/useShouldAutoLoadMedia' import { cn } from '@/lib/utils' -import { useContentPolicyOptional } from '@/providers/ContentPolicyProvider' import mediaManager from '@/services/media-manager.service' import { YouTubePlayer } from '@/types/youtube' import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' @@ -13,14 +13,15 @@ import logger from '@/lib/logger' export default function YoutubeEmbeddedPlayer({ url, className, - mustLoad = false + mustLoad = false, + authorPubkey }: { url: string className?: string mustLoad?: boolean + authorPubkey?: string | null }) { - const contentPolicy = useContentPolicyOptional() - const autoLoadMedia = contentPolicy?.autoLoadMedia ?? true + const autoLoadMedia = useShouldAutoLoadMedia(authorPubkey) const [userClickedLoad, setUserClickedLoad] = useState(false) const { videoId, isShort } = useMemo(() => parseYoutubeUrl(url), [url]) const [initSuccess, setInitSuccess] = useState(false) diff --git a/src/constants.ts b/src/constants.ts index fce9fb6d..259cb854 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1146,6 +1146,7 @@ export const NOTIFICATION_LIST_STYLE = { } as const export const MEDIA_AUTO_LOAD_POLICY = { + FOLLOWS_ONLY: 'follows-only', ALWAYS: 'always', WIFI_ONLY: 'wifi-only', NEVER: 'never' diff --git a/src/hooks/index.tsx b/src/hooks/index.tsx index 020fba01..0814dadf 100644 --- a/src/hooks/index.tsx +++ b/src/hooks/index.tsx @@ -1,3 +1,4 @@ +export * from './useShouldAutoLoadMedia' export * from './useSignGatedControl' export * from './useRelayPageFeedPolicy' export * from './useNearViewport' diff --git a/src/hooks/useShouldAutoLoadMedia.ts b/src/hooks/useShouldAutoLoadMedia.ts new file mode 100644 index 00000000..2aa0dd35 --- /dev/null +++ b/src/hooks/useShouldAutoLoadMedia.ts @@ -0,0 +1,34 @@ +import { getPubkeysFromPTags } from '@/lib/tag' +import { resolveAutoLoadMediaForAuthor } from '@/lib/media-auto-load-policy' +import { useContentPolicyOptional } from '@/providers/ContentPolicyProvider' +import { useNostrOptional } from '@/providers/nostr-context' +import storage from '@/services/local-storage.service' +import { useMemo } from 'react' + +export function useShouldAutoLoadMedia(authorPubkey?: string | null): boolean { + const contentPolicy = useContentPolicyOptional() + const nostr = useNostrOptional() + + const followings = useMemo( + () => (nostr?.followListEvent ? getPubkeysFromPTags(nostr.followListEvent.tags) : []), + [nostr?.followListEvent] + ) + + return useMemo( + () => + resolveAutoLoadMediaForAuthor({ + policy: contentPolicy?.mediaAutoLoadPolicy ?? storage.getMediaAutoLoadPolicy(), + connectionType: contentPolicy?.connectionType, + authorPubkey, + followings, + accountPubkey: nostr?.pubkey ?? null + }), + [ + contentPolicy?.mediaAutoLoadPolicy, + contentPolicy?.connectionType, + authorPubkey, + followings, + nostr?.pubkey + ] + ) +} diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 94b50492..7d60ccfb 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -1643,6 +1643,7 @@ export default { 'POW {{difficulty}}': 'POW {{difficulty}}', 'via {{client}}': 'via {{client}}', 'Auto-load media': 'Auto-load media', + 'Follows only': 'Follows only', Always: 'Always', 'Wi-Fi only': 'Wi-Fi only', Never: 'Never', diff --git a/src/lib/media-auto-load-policy.ts b/src/lib/media-auto-load-policy.ts new file mode 100644 index 00000000..6a951648 --- /dev/null +++ b/src/lib/media-auto-load-policy.ts @@ -0,0 +1,44 @@ +import { MEDIA_AUTO_LOAD_POLICY } from '@/constants' +import type { TMediaAutoLoadPolicy } from '@/types' + +export type TResolveAutoLoadMediaParams = { + policy: TMediaAutoLoadPolicy + connectionType?: string + authorPubkey?: string | null + followings?: readonly string[] + accountPubkey?: string | null +} + +/** Whether media for a given author should load without an explicit tap. */ +export function resolveAutoLoadMediaForAuthor({ + policy, + connectionType, + authorPubkey, + followings = [], + accountPubkey +}: TResolveAutoLoadMediaParams): boolean { + if (policy === MEDIA_AUTO_LOAD_POLICY.NEVER) return false + if (policy === MEDIA_AUTO_LOAD_POLICY.ALWAYS) return true + if (policy === MEDIA_AUTO_LOAD_POLICY.WIFI_ONLY) { + return connectionType !== 'cellular' + } + if (policy === MEDIA_AUTO_LOAD_POLICY.FOLLOWS_ONLY) { + if (!authorPubkey) return false + if (accountPubkey && authorPubkey === accountPubkey) return true + return followings.includes(authorPubkey) + } + return false +} + +/** Coarse global flag (false when the policy needs per-author resolution). */ +export function resolveGlobalAutoLoadMedia( + policy: TMediaAutoLoadPolicy, + connectionType?: string +): boolean { + if (policy === MEDIA_AUTO_LOAD_POLICY.ALWAYS) return true + if (policy === MEDIA_AUTO_LOAD_POLICY.NEVER) return false + if (policy === MEDIA_AUTO_LOAD_POLICY.WIFI_ONLY) { + return connectionType !== 'cellular' + } + return false +} diff --git a/src/pages/secondary/GeneralSettingsPage/index.tsx b/src/pages/secondary/GeneralSettingsPage/index.tsx index 6aa0fce3..cd7d5125 100644 --- a/src/pages/secondary/GeneralSettingsPage/index.tsx +++ b/src/pages/secondary/GeneralSettingsPage/index.tsx @@ -154,7 +154,7 @@ const GeneralSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index {t('Auto-load media')}