Browse Source

make sure that media autoloading doesn't override content blocking

imwald
Silberengel 1 week ago
parent
commit
9a40e301a4
  1. 2
      src/components/Note/ArticleCardCoverImage.tsx
  2. 2
      src/components/Note/CommunityDefinition.tsx
  3. 2
      src/components/Note/GroupMetadata.tsx
  4. 2
      src/components/Note/LiveEvent.tsx
  5. 2
      src/components/Note/LongFormCard.tsx
  6. 3
      src/components/Note/MarkdownArticle/MarkdownArticle.tsx
  7. 2
      src/components/Note/MusicTrackNote.tsx
  8. 2
      src/components/Note/PublicationCard.tsx
  9. 2
      src/components/Note/WikiCard.tsx
  10. 6
      src/components/Note/index.tsx
  11. 10
      src/components/WebPreview/index.tsx
  12. 16
      src/hooks/useShouldAutoLoadMedia.ts
  13. 52
      src/lib/media-auto-load-policy.test.ts
  14. 8
      src/lib/media-auto-load-policy.ts
  15. 21
      src/providers/MediaAutoLoadEventContext.tsx

2
src/components/Note/ArticleCardCoverImage.tsx

@ -22,7 +22,7 @@ export default function ArticleCardCoverImage({ @@ -22,7 +22,7 @@ export default function ArticleCardCoverImage({
/** Passed through to {@link ContentImage} when an `image` tag URL exists. */
hideImageIfError?: boolean
}) {
const autoLoadFromPolicy = useShouldAutoLoadMedia(event.pubkey)
const autoLoadFromPolicy = useShouldAutoLoadMedia(event.pubkey, event)
const autoLoadMedia = autoLoadMediaProp ?? autoLoadFromPolicy
const trimmed = imageUrl?.trim()
if (trimmed) {

2
src/components/Note/CommunityDefinition.tsx

@ -12,7 +12,7 @@ export default function CommunityDefinition({ @@ -12,7 +12,7 @@ export default function CommunityDefinition({
event: Event
className?: string
}) {
const autoLoadMedia = useShouldAutoLoadMedia(event.pubkey)
const autoLoadMedia = useShouldAutoLoadMedia(event.pubkey, event)
const metadata = useMemo(() => getCommunityDefinitionFromEvent(event), [event])
const communityNameComponent = (

2
src/components/Note/GroupMetadata.tsx

@ -14,7 +14,7 @@ export default function GroupMetadata({ @@ -14,7 +14,7 @@ export default function GroupMetadata({
originalNoteId?: string
className?: string
}) {
const autoLoadMedia = useShouldAutoLoadMedia(event.pubkey)
const autoLoadMedia = useShouldAutoLoadMedia(event.pubkey, event)
const metadata = useMemo(() => getGroupMetadataFromEvent(event), [event])
const groupNameComponent = (

2
src/components/Note/LiveEvent.tsx

@ -26,7 +26,7 @@ export default function LiveEvent({ event, className }: { event: Event; classNam @@ -26,7 +26,7 @@ export default function LiveEvent({ event, className }: { event: Event; classNam
const liveActivities = useLiveActivitiesOptional()
const screenSize = useScreenSizeOptional()
const isSmallScreen = screenSize?.isSmallScreen ?? false
const autoLoadMedia = useShouldAutoLoadMedia(event.pubkey)
const autoLoadMedia = useShouldAutoLoadMedia(event.pubkey, event)
const metadata = useMemo(() => getLiveEventMetadataFromEvent(event), [event])
const playback = useMemo(() => liveEventInlinePlaybackFromEvent(event), [event])
const joinUrl = useMemo(() => preferredLiveJoinUrlForEvent(event), [event])

2
src/components/Note/LongFormCard.tsx

@ -31,7 +31,7 @@ export default function LongFormCard({ @@ -31,7 +31,7 @@ export default function LongFormCard({
const push = secondaryPage?.push ?? ((url: string) => {
window.location.href = url
})
const autoLoadMedia = useShouldAutoLoadMedia(event.pubkey)
const autoLoadMedia = useShouldAutoLoadMedia(event.pubkey, event)
const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event])
const bodyBlurb = useMemo(() => cardEventBodyBlurb(event.content), [event.content])
const summaryText = (metadata.summary?.trim() || bodyBlurb).trim()

3
src/components/Note/MarkdownArticle/MarkdownArticle.tsx

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
import { useSecondaryPageOptional, useSmartHashtagNavigationOptional, useSmartRelayNavigationOptional } from '@/PageManager'
import Image from '@/components/Image'
import UserAvatar from '@/components/UserAvatar'
import { MediaAutoLoadEventProvider } from '@/providers/MediaAutoLoadEventContext'
import MediaPlayer from '@/components/MediaPlayer'
import Wikilink from '@/components/UniversalContent/Wikilink'
import { BookstrContent } from '@/components/Bookstr'
@ -6192,6 +6193,7 @@ export default function MarkdownArticle({ @@ -6192,6 +6193,7 @@ export default function MarkdownArticle({
}, [metadata.tags, hashtagsInContent])
return (
<MediaAutoLoadEventProvider event={event}>
<>
<style>{`
/* Padding (not margin) so separation does not collapse with the prior list's margin */
@ -6562,5 +6564,6 @@ export default function MarkdownArticle({ @@ -6562,5 +6564,6 @@ export default function MarkdownArticle({
document.body
)}
</>
</MediaAutoLoadEventProvider>
)
}

2
src/components/Note/MusicTrackNote.tsx

@ -42,7 +42,7 @@ export default function MusicTrackNote({ @@ -42,7 +42,7 @@ export default function MusicTrackNote({
className?: string
loadMedia?: boolean
}) {
const autoLoadMedia = useShouldAutoLoadMedia(event.pubkey)
const autoLoadMedia = useShouldAutoLoadMedia(event.pubkey, event)
const mustLoad = loadMedia || autoLoadMedia
const { t } = useTranslation()

2
src/components/Note/PublicationCard.tsx

@ -26,7 +26,7 @@ export default function PublicationCard({ @@ -26,7 +26,7 @@ export default function PublicationCard({
const { navigateToNote } = useSmartNoteNavigationOptional()
const secondaryPage = useSecondaryPageOptional()
const push = secondaryPage?.push ?? ((url: string) => { window.location.href = url })
const autoLoadMedia = useShouldAutoLoadMedia(event.pubkey)
const autoLoadMedia = useShouldAutoLoadMedia(event.pubkey, event)
const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event])
const bodyBlurb = useMemo(() => cardEventBodyBlurb(event.content), [event.content])
const summaryText = (metadata.summary?.trim() || bodyBlurb).trim()

2
src/components/Note/WikiCard.tsx

@ -19,7 +19,7 @@ export default function WikiCard({ @@ -19,7 +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 autoLoadMedia = useShouldAutoLoadMedia(event.pubkey)
const autoLoadMedia = useShouldAutoLoadMedia(event.pubkey, event)
const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event])
const bodyBlurb = useMemo(() => cardEventBodyBlurb(event.content), [event.content])
const summaryText = (metadata.summary?.trim() || bodyBlurb).trim()

6
src/components/Note/index.tsx

@ -278,7 +278,7 @@ export default function Note({ @@ -278,7 +278,7 @@ export default function Note({
const parentFetchRelayHints = useMemo(() => relayHintsFromEventTags(event), [event])
const contentPolicy = useContentPolicyOptional()
const defaultShowNsfw = contentPolicy?.defaultShowNsfw ?? true
const autoLoadMedia = useShouldAutoLoadMedia(event.pubkey)
const autoLoadMedia = useShouldAutoLoadMedia(event.pubkey, event)
const [showNsfw, setShowNsfw] = useState(false)
const muteList = useMuteListOptional()
const mutePubkeySet = muteList?.mutePubkeySet ?? new Set<string>()
@ -469,7 +469,7 @@ export default function Note({ @@ -469,7 +469,7 @@ export default function Note({
>
{href}
</a>
<WebPreview url={href} className="w-full" />
<WebPreview url={href} className="w-full" authorPubkey={event.pubkey} sourceEvent={event} />
</div>
) : null}
{displayEvent.content?.trim() ? renderEventContent({ hideMetadata: true }) : null}
@ -560,7 +560,7 @@ export default function Note({ @@ -560,7 +560,7 @@ export default function Note({
<>
{voiceArticleUrl && (
<div className="mt-2 not-prose max-w-full">
<WebPreview url={voiceArticleUrl} className="w-full" />
<WebPreview url={voiceArticleUrl} className="w-full" authorPubkey={event.pubkey} sourceEvent={event} />
</div>
)}
<AudioPlayer className="mt-2" src={event.content} />

10
src/components/WebPreview/index.tsx

@ -9,7 +9,7 @@ import { useShouldAutoLoadMedia } from '@/hooks/useShouldAutoLoadMedia' @@ -9,7 +9,7 @@ import { useShouldAutoLoadMedia } from '@/hooks/useShouldAutoLoadMedia'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { Skeleton } from '@/components/ui/skeleton'
import { ExternalLink } from 'lucide-react'
import { nip19, kinds } from 'nostr-tools'
import { nip19, kinds, type Event } from 'nostr-tools'
import { useMemo, useEffect, useState } from 'react'
import Image from '../Image'
import Username from '../Username'
@ -17,7 +17,6 @@ import { resolveImwaldRouteSocialCopy } from '@/lib/document-meta' @@ -17,7 +17,6 @@ import { resolveImwaldRouteSocialCopy } from '@/lib/document-meta'
import { cleanUrl, isSafeMediaUrl } from '@/lib/url'
import { tagNameEquals } from '@/lib/tag'
import { queryService } from '@/services/client.service'
import { Event } from 'nostr-tools'
import { FAST_READ_RELAY_URLS } from '@/constants'
import { getImetaInfosFromEvent } from '@/lib/event'
import MarkdownArticle from '../Note/MarkdownArticle/MarkdownArticle'
@ -137,13 +136,16 @@ function getTitleWithFallbacks(event: Event | null, eventMetadata: { title?: str @@ -137,13 +136,16 @@ function getTitleWithFallbacks(event: Event | null, eventMetadata: { title?: str
export default function WebPreview({
url,
className,
authorPubkey
authorPubkey,
sourceEvent
}: {
url: string
className?: string
authorPubkey?: string | null
/** Note being rendered; content-warning tags block OG/image autoload. */
sourceEvent?: Event | null
}) {
const autoLoadMedia = useShouldAutoLoadMedia(authorPubkey)
const autoLoadMedia = useShouldAutoLoadMedia(authorPubkey, sourceEvent)
const { isSmallScreen } = useScreenSize()
const cleanedUrl = useMemo(() => cleanUrl(url), [url])

16
src/hooks/useShouldAutoLoadMedia.ts

@ -1,19 +1,27 @@ @@ -1,19 +1,27 @@
import { getPubkeysFromPTags } from '@/lib/tag'
import { resolveAutoLoadMediaForAuthor } from '@/lib/media-auto-load-policy'
import { useContentPolicyOptional } from '@/providers/ContentPolicyProvider'
import { useMediaAutoLoadSourceEvent } from '@/providers/MediaAutoLoadEventContext'
import { useNostrOptional } from '@/providers/nostr-context'
import storage from '@/services/local-storage.service'
import type { Event } from 'nostr-tools'
import { useMemo } from 'react'
export function useShouldAutoLoadMedia(authorPubkey?: string | null): boolean {
export function useShouldAutoLoadMedia(
authorPubkey?: string | null,
sourceEvent?: Event | null
): boolean {
const contentPolicy = useContentPolicyOptional()
const nostr = useNostrOptional()
const contextSourceEvent = useMediaAutoLoadSourceEvent()
const followings = useMemo(
() => (nostr?.followListEvent ? getPubkeysFromPTags(nostr.followListEvent.tags) : []),
[nostr?.followListEvent]
)
const effectiveSourceEvent = sourceEvent ?? contextSourceEvent
return useMemo(
() =>
resolveAutoLoadMediaForAuthor({
@ -21,14 +29,16 @@ export function useShouldAutoLoadMedia(authorPubkey?: string | null): boolean { @@ -21,14 +29,16 @@ export function useShouldAutoLoadMedia(authorPubkey?: string | null): boolean {
connectionType: contentPolicy?.connectionType,
authorPubkey,
followings,
accountPubkey: nostr?.pubkey ?? null
accountPubkey: nostr?.pubkey ?? null,
sourceEvent: effectiveSourceEvent
}),
[
contentPolicy?.mediaAutoLoadPolicy,
contentPolicy?.connectionType,
authorPubkey,
followings,
nostr?.pubkey
nostr?.pubkey,
effectiveSourceEvent
]
)
}

52
src/lib/media-auto-load-policy.test.ts

@ -0,0 +1,52 @@ @@ -0,0 +1,52 @@
import { describe, expect, it } from 'vitest'
import { MEDIA_AUTO_LOAD_POLICY } from '@/constants'
import { resolveAutoLoadMediaForAuthor } from '@/lib/media-auto-load-policy'
import type { Event } from 'nostr-tools'
const author = 'aa'.repeat(32)
function warnedNote(): Event {
return {
id: 'id',
sig: 'sig',
kind: 1,
tags: [['content-warning', 'Sensitive content']],
content: 'hello',
created_at: 1,
pubkey: author
}
}
describe('resolveAutoLoadMediaForAuthor', () => {
it('blocks autoload for content-warning notes even when author is followed', () => {
expect(
resolveAutoLoadMediaForAuthor({
policy: MEDIA_AUTO_LOAD_POLICY.FOLLOWS_ONLY,
authorPubkey: author,
followings: [author],
sourceEvent: warnedNote()
})
).toBe(false)
})
it('allows autoload for followed authors on notes without content warnings', () => {
expect(
resolveAutoLoadMediaForAuthor({
policy: MEDIA_AUTO_LOAD_POLICY.FOLLOWS_ONLY,
authorPubkey: author,
followings: [author],
sourceEvent: { ...warnedNote(), tags: [] }
})
).toBe(true)
})
it('blocks autoload for content warnings under ALWAYS policy', () => {
expect(
resolveAutoLoadMediaForAuthor({
policy: MEDIA_AUTO_LOAD_POLICY.ALWAYS,
authorPubkey: author,
sourceEvent: warnedNote()
})
).toBe(false)
})
})

8
src/lib/media-auto-load-policy.ts

@ -1,5 +1,7 @@ @@ -1,5 +1,7 @@
import { MEDIA_AUTO_LOAD_POLICY } from '@/constants'
import { isNsfwEvent } from '@/lib/event'
import type { TMediaAutoLoadPolicy } from '@/types'
import type { Event } from 'nostr-tools'
export type TResolveAutoLoadMediaParams = {
policy: TMediaAutoLoadPolicy
@ -7,6 +9,8 @@ export type TResolveAutoLoadMediaParams = { @@ -7,6 +9,8 @@ export type TResolveAutoLoadMediaParams = {
authorPubkey?: string | null
followings?: readonly string[]
accountPubkey?: string | null
/** When set, NIP-36 / legacy NSFW tags block automatic media load (tap-to-reveal still works). */
sourceEvent?: Event | null
}
/** Whether media for a given author should load without an explicit tap. */
@ -15,8 +19,10 @@ export function resolveAutoLoadMediaForAuthor({ @@ -15,8 +19,10 @@ export function resolveAutoLoadMediaForAuthor({
connectionType,
authorPubkey,
followings = [],
accountPubkey
accountPubkey,
sourceEvent
}: TResolveAutoLoadMediaParams): boolean {
if (sourceEvent && isNsfwEvent(sourceEvent)) return false
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) {

21
src/providers/MediaAutoLoadEventContext.tsx

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
import type { Event } from 'nostr-tools'
import { createContext, useContext } from 'react'
const MediaAutoLoadEventContext = createContext<Event | null>(null)
/** Supplies the note/event being rendered so media policy can respect content warnings. */
export function MediaAutoLoadEventProvider({
event,
children
}: {
event: Event
children: React.ReactNode
}) {
return (
<MediaAutoLoadEventContext.Provider value={event}>{children}</MediaAutoLoadEventContext.Provider>
)
}
export function useMediaAutoLoadSourceEvent(): Event | null {
return useContext(MediaAutoLoadEventContext)
}
Loading…
Cancel
Save