feat: add auto-load media content setting option
This commit is contained in:
@@ -5,10 +5,10 @@ import { TImetaInfo } from '@/types'
|
||||
import { getHashFromURL } from 'blossom-client-sdk'
|
||||
import { decode } from 'blurhash'
|
||||
import { ImageOff } from 'lucide-react'
|
||||
import { HTMLAttributes, useEffect, useState } from 'react'
|
||||
import { HTMLAttributes, useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
export default function Image({
|
||||
image: { url, blurHash, pubkey },
|
||||
image: { url, blurHash, pubkey, dim },
|
||||
alt,
|
||||
className = '',
|
||||
classNames = {},
|
||||
@@ -26,8 +26,7 @@ export default function Image({
|
||||
errorPlaceholder?: React.ReactNode
|
||||
}) {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [displayBlurHash, setDisplayBlurHash] = useState(true)
|
||||
const [blurDataUrl, setBlurDataUrl] = useState<string | null>(null)
|
||||
const [displaySkeleton, setDisplaySkeleton] = useState(true)
|
||||
const [hasError, setHasError] = useState(false)
|
||||
const [imageUrl, setImageUrl] = useState(url)
|
||||
const [tried, setTried] = useState(new Set())
|
||||
@@ -36,32 +35,13 @@ export default function Image({
|
||||
setImageUrl(url)
|
||||
setIsLoading(true)
|
||||
setHasError(false)
|
||||
setDisplayBlurHash(true)
|
||||
setDisplaySkeleton(true)
|
||||
setTried(new Set())
|
||||
}, [url])
|
||||
|
||||
useEffect(() => {
|
||||
if (blurHash) {
|
||||
const { numX, numY } = decodeBlurHashSize(blurHash)
|
||||
const width = numX * 3
|
||||
const height = numY * 3
|
||||
const pixels = decode(blurHash, width, height)
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (ctx) {
|
||||
const imageData = ctx.createImageData(width, height)
|
||||
imageData.data.set(pixels)
|
||||
ctx.putImageData(imageData, 0, 0)
|
||||
setBlurDataUrl(canvas.toDataURL())
|
||||
}
|
||||
}
|
||||
}, [blurHash])
|
||||
|
||||
if (hideIfError && hasError) return null
|
||||
|
||||
const handleImageError = async () => {
|
||||
const handleError = async () => {
|
||||
let oldImageUrl: URL | undefined
|
||||
let hash: string | null = null
|
||||
try {
|
||||
@@ -101,26 +81,52 @@ export default function Image({
|
||||
setImageUrl(nextUrl.toString())
|
||||
}
|
||||
|
||||
const handleLoad = () => {
|
||||
setIsLoading(false)
|
||||
setHasError(false)
|
||||
setTimeout(() => setDisplaySkeleton(false), 600)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('relative', classNames.wrapper)} {...props}>
|
||||
{isLoading && <Skeleton className={cn('absolute inset-0', className)} />}
|
||||
{!hasError ? (
|
||||
<div className={cn('relative overflow-hidden', classNames.wrapper)} {...props}>
|
||||
{displaySkeleton && (
|
||||
<div className="absolute inset-0 z-10">
|
||||
{blurHash ? (
|
||||
<BlurHashCanvas
|
||||
blurHash={blurHash}
|
||||
className={cn(
|
||||
'absolute inset-0 transition-opacity duration-500 rounded-lg',
|
||||
isLoading ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Skeleton
|
||||
className={cn(
|
||||
'absolute inset-0 transition-opacity duration-500 rounded-lg',
|
||||
isLoading ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!hasError && (
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={alt}
|
||||
decoding="async"
|
||||
loading="lazy"
|
||||
onLoad={handleLoad}
|
||||
onError={handleError}
|
||||
className={cn(
|
||||
'object-cover transition-opacity duration-300',
|
||||
isLoading ? 'opacity-0' : 'opacity-100',
|
||||
'object-cover rounded-lg w-full h-full transition-opacity duration-500',
|
||||
className
|
||||
)}
|
||||
onLoad={() => {
|
||||
setIsLoading(false)
|
||||
setHasError(false)
|
||||
setTimeout(() => setDisplayBlurHash(false), 500)
|
||||
}}
|
||||
onError={handleImageError}
|
||||
width={dim?.width}
|
||||
height={dim?.height}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
)}
|
||||
{hasError && (
|
||||
<div
|
||||
className={cn(
|
||||
'object-cover flex flex-col items-center justify-center w-full h-full bg-muted',
|
||||
@@ -131,21 +137,49 @@ export default function Image({
|
||||
{errorPlaceholder}
|
||||
</div>
|
||||
)}
|
||||
{displayBlurHash && blurDataUrl && !hasError && (
|
||||
<img
|
||||
src={blurDataUrl}
|
||||
className={cn('absolute inset-0 object-cover w-full h-full -z-10', className)}
|
||||
alt={alt}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const DIGITS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~'
|
||||
function decodeBlurHashSize(blurHash: string) {
|
||||
const sizeValue = DIGITS.indexOf(blurHash[0])
|
||||
const numY = (sizeValue / 9 + 1) | 0
|
||||
const numX = (sizeValue % 9) + 1
|
||||
return { numX, numY }
|
||||
const blurHashWidth = 32
|
||||
const blurHashHeight = 32
|
||||
function BlurHashCanvas({ blurHash, className = '' }: { blurHash: string; className?: string }) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
|
||||
const pixels = useMemo(() => {
|
||||
if (!blurHash) return null
|
||||
try {
|
||||
return decode(blurHash, blurHashWidth, blurHashHeight)
|
||||
} catch (error) {
|
||||
console.warn('Failed to decode blurhash:', error)
|
||||
return null
|
||||
}
|
||||
}, [blurHash])
|
||||
|
||||
useEffect(() => {
|
||||
if (!pixels || !canvasRef.current) return
|
||||
|
||||
const canvas = canvasRef.current
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
const imageData = ctx.createImageData(blurHashWidth, blurHashHeight)
|
||||
imageData.data.set(pixels)
|
||||
ctx.putImageData(imageData, 0, 0)
|
||||
}, [pixels])
|
||||
|
||||
if (!blurHash) return null
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
width={blurHashWidth}
|
||||
height={blurHashHeight}
|
||||
className={cn('w-full h-full object-cover rounded-lg', className)}
|
||||
style={{
|
||||
imageRendering: 'auto',
|
||||
filter: 'blur(0.5px)'
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { randomString } from '@/lib/random'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||
import modalManager from '@/services/modal-manager.service'
|
||||
import { TImetaInfo } from '@/types'
|
||||
import { ReactNode, useEffect, useMemo, useState } from 'react'
|
||||
@@ -7,6 +8,7 @@ import { createPortal } from 'react-dom'
|
||||
import Lightbox from 'yet-another-react-lightbox'
|
||||
import Zoom from 'yet-another-react-lightbox/plugins/zoom'
|
||||
import Image from '../Image'
|
||||
import ImageWithLightbox from '../ImageWithLightbox'
|
||||
|
||||
export default function ImageGallery({
|
||||
className,
|
||||
@@ -20,6 +22,7 @@ export default function ImageGallery({
|
||||
end?: number
|
||||
}) {
|
||||
const id = useMemo(() => `image-gallery-${randomString()}`, [])
|
||||
const { autoLoadMedia } = useContentPolicy()
|
||||
const [index, setIndex] = useState(-1)
|
||||
useEffect(() => {
|
||||
if (index >= 0) {
|
||||
@@ -38,12 +41,26 @@ export default function ImageGallery({
|
||||
}
|
||||
|
||||
const displayImages = images.slice(start, end)
|
||||
|
||||
if (!autoLoadMedia) {
|
||||
return displayImages.map((image, i) => (
|
||||
<ImageWithLightbox
|
||||
key={i}
|
||||
image={image}
|
||||
className="max-h-[80vh] sm:max-h-[50vh] object-contain"
|
||||
classNames={{
|
||||
wrapper: cn('w-fit max-w-full', className)
|
||||
}}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
let imageContent: ReactNode | null = null
|
||||
if (displayImages.length === 1) {
|
||||
imageContent = (
|
||||
<Image
|
||||
key={0}
|
||||
className="rounded-lg max-h-[80vh] sm:max-h-[50vh] border cursor-zoom-in"
|
||||
className="max-h-[80vh] sm:max-h-[50vh] cursor-zoom-in object-contain"
|
||||
classNames={{
|
||||
errorPlaceholder: 'aspect-square h-[30vh]'
|
||||
}}
|
||||
@@ -57,7 +74,7 @@ export default function ImageGallery({
|
||||
{displayImages.map((image, i) => (
|
||||
<Image
|
||||
key={i}
|
||||
className="aspect-square w-full rounded-lg border cursor-zoom-in"
|
||||
className="aspect-square w-full cursor-zoom-in"
|
||||
image={image}
|
||||
onClick={(e) => handlePhotoClick(e, i)}
|
||||
/>
|
||||
@@ -70,7 +87,7 @@ export default function ImageGallery({
|
||||
{displayImages.map((image, i) => (
|
||||
<Image
|
||||
key={i}
|
||||
className="aspect-square w-full rounded-lg border cursor-zoom-in"
|
||||
className="aspect-square w-full cursor-zoom-in"
|
||||
image={image}
|
||||
onClick={(e) => handlePhotoClick(e, i)}
|
||||
/>
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
import { randomString } from '@/lib/random'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||
import modalManager from '@/services/modal-manager.service'
|
||||
import { TImetaInfo } from '@/types'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Lightbox from 'yet-another-react-lightbox'
|
||||
import Zoom from 'yet-another-react-lightbox/plugins/zoom'
|
||||
import Image from '../Image'
|
||||
|
||||
export default function ImageWithLightbox({
|
||||
image,
|
||||
className
|
||||
className,
|
||||
classNames = {}
|
||||
}: {
|
||||
image: TImetaInfo
|
||||
className?: string
|
||||
classNames?: {
|
||||
wrapper?: string
|
||||
}
|
||||
}) {
|
||||
const id = useMemo(() => `image-with-lightbox-${randomString()}`, [])
|
||||
const { t } = useTranslation()
|
||||
const { autoLoadMedia } = useContentPolicy()
|
||||
const [display, setDisplay] = useState(autoLoadMedia)
|
||||
const [index, setIndex] = useState(-1)
|
||||
useEffect(() => {
|
||||
if (index >= 0) {
|
||||
@@ -27,6 +36,20 @@ export default function ImageWithLightbox({
|
||||
}
|
||||
}, [index])
|
||||
|
||||
if (!display) {
|
||||
return (
|
||||
<div
|
||||
className="text-primary hover:underline truncate w-fit cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setDisplay(true)
|
||||
}}
|
||||
>
|
||||
[{t('Click to load image')}]
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const handlePhotoClick = (event: React.MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
@@ -34,11 +57,12 @@ export default function ImageWithLightbox({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div>
|
||||
<Image
|
||||
key={0}
|
||||
className={cn('rounded-lg border cursor-zoom-in', className)}
|
||||
className={className}
|
||||
classNames={{
|
||||
wrapper: cn('rounded-lg border cursor-zoom-in', classNames.wrapper),
|
||||
errorPlaceholder: 'aspect-square h-[30vh]'
|
||||
}}
|
||||
image={image}
|
||||
|
||||
@@ -1,11 +1,28 @@
|
||||
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AudioPlayer from '../AudioPlayer'
|
||||
import VideoPlayer from '../VideoPlayer'
|
||||
|
||||
export default function MediaPlayer({ src, className }: { src: string; className?: string }) {
|
||||
const { t } = useTranslation()
|
||||
const { autoLoadMedia } = useContentPolicy()
|
||||
const [display, setDisplay] = useState(autoLoadMedia)
|
||||
const [mediaType, setMediaType] = useState<'video' | 'audio' | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (autoLoadMedia) {
|
||||
setDisplay(true)
|
||||
} else {
|
||||
setDisplay(false)
|
||||
}
|
||||
}, [autoLoadMedia])
|
||||
|
||||
useEffect(() => {
|
||||
if (!display) {
|
||||
setMediaType(null)
|
||||
return
|
||||
}
|
||||
if (!src) {
|
||||
setMediaType(null)
|
||||
return
|
||||
@@ -35,7 +52,21 @@ export default function MediaPlayer({ src, className }: { src: string; className
|
||||
return () => {
|
||||
video.src = ''
|
||||
}
|
||||
}, [src])
|
||||
}, [src, display])
|
||||
|
||||
if (!display) {
|
||||
return (
|
||||
<div
|
||||
className="text-primary hover:underline truncate w-fit cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setDisplay(true)
|
||||
}}
|
||||
>
|
||||
[{t('Click to load media')}]
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!mediaType) {
|
||||
return null
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getCommunityDefinitionFromEvent } from '@/lib/event-metadata'
|
||||
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { useMemo } from 'react'
|
||||
import ClientSelect from '../ClientSelect'
|
||||
@@ -11,6 +12,7 @@ export default function CommunityDefinition({
|
||||
event: Event
|
||||
className?: string
|
||||
}) {
|
||||
const { autoLoadMedia } = useContentPolicy()
|
||||
const metadata = useMemo(() => getCommunityDefinitionFromEvent(event), [event])
|
||||
|
||||
const communityNameComponent = (
|
||||
@@ -24,10 +26,10 @@ export default function CommunityDefinition({
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="flex gap-4">
|
||||
{metadata.image && (
|
||||
{metadata.image && autoLoadMedia && (
|
||||
<Image
|
||||
image={{ url: metadata.image, pubkey: event.pubkey }}
|
||||
className="rounded-lg aspect-square object-cover bg-foreground h-20"
|
||||
className="aspect-square bg-foreground h-20"
|
||||
hideIfError
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getGroupMetadataFromEvent } from '@/lib/event-metadata'
|
||||
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { useMemo } from 'react'
|
||||
import ClientSelect from '../ClientSelect'
|
||||
@@ -13,6 +14,7 @@ export default function GroupMetadata({
|
||||
originalNoteId?: string
|
||||
className?: string
|
||||
}) {
|
||||
const { autoLoadMedia } = useContentPolicy()
|
||||
const metadata = useMemo(() => getGroupMetadataFromEvent(event), [event])
|
||||
|
||||
const groupNameComponent = (
|
||||
@@ -26,10 +28,10 @@ export default function GroupMetadata({
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="flex gap-4">
|
||||
{metadata.picture && (
|
||||
{metadata.picture && autoLoadMedia && (
|
||||
<Image
|
||||
image={{ url: metadata.picture, pubkey: event.pubkey }}
|
||||
className="rounded-lg aspect-square object-cover bg-foreground h-20"
|
||||
className="aspect-square bg-foreground h-20"
|
||||
hideIfError
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { getLiveEventMetadataFromEvent } from '@/lib/event-metadata'
|
||||
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { useMemo } from 'react'
|
||||
@@ -8,6 +9,8 @@ import Image from '../Image'
|
||||
|
||||
export default function LiveEvent({ event, className }: { event: Event; className?: string }) {
|
||||
const { isSmallScreen } = useScreenSize()
|
||||
|
||||
const { autoLoadMedia } = useContentPolicy()
|
||||
const metadata = useMemo(() => getLiveEventMetadataFromEvent(event), [event])
|
||||
|
||||
const liveStatusComponent =
|
||||
@@ -39,10 +42,10 @@ export default function LiveEvent({ event, className }: { event: Event; classNam
|
||||
if (isSmallScreen) {
|
||||
return (
|
||||
<div className={className}>
|
||||
{metadata.image && (
|
||||
{metadata.image && autoLoadMedia && (
|
||||
<Image
|
||||
image={{ url: metadata.image, pubkey: event.pubkey }}
|
||||
className="w-full aspect-video object-cover rounded-lg"
|
||||
className="w-full aspect-video"
|
||||
hideIfError
|
||||
/>
|
||||
)}
|
||||
@@ -60,10 +63,10 @@ export default function LiveEvent({ event, className }: { event: Event; classNam
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="flex gap-4">
|
||||
{metadata.image && (
|
||||
{metadata.image && autoLoadMedia && (
|
||||
<Image
|
||||
image={{ url: metadata.image, pubkey: event.pubkey }}
|
||||
className="rounded-lg aspect-[4/3] xl:aspect-video object-cover bg-foreground h-44"
|
||||
className="aspect-[4/3] xl:aspect-video bg-foreground h-44"
|
||||
hideIfError
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -63,7 +63,16 @@ export default function LongFormArticle({
|
||||
},
|
||||
p: (props) => <p {...props} className="break-words" />,
|
||||
div: (props) => <div {...props} className="break-words" />,
|
||||
code: (props) => <code {...props} className="break-words whitespace-pre-wrap" />
|
||||
code: (props) => <code {...props} className="break-words whitespace-pre-wrap" />,
|
||||
img: (props) => (
|
||||
<ImageWithLightbox
|
||||
image={{ url: props.src || '', pubkey: event.pubkey }}
|
||||
className="max-h-[80vh] sm:max-h-[50vh] object-contain my-0"
|
||||
classNames={{
|
||||
wrapper: 'w-fit max-w-full'
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}) as Components,
|
||||
[]
|
||||
)
|
||||
@@ -81,7 +90,7 @@ export default function LongFormArticle({
|
||||
{metadata.image && (
|
||||
<ImageWithLightbox
|
||||
image={{ url: metadata.image, pubkey: event.pubkey }}
|
||||
className="w-full aspect-[3/1] object-cover rounded-lg"
|
||||
className="w-full aspect-[3/1] object-cover my-0"
|
||||
/>
|
||||
)}
|
||||
<Markdown
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata'
|
||||
import { toNoteList } from '@/lib/link'
|
||||
import { useSecondaryPage } from '@/PageManager'
|
||||
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||
import { Event, kinds } from 'nostr-tools'
|
||||
import { useMemo } from 'react'
|
||||
@@ -15,6 +16,7 @@ export default function LongFormArticlePreview({
|
||||
}) {
|
||||
const { isSmallScreen } = useScreenSize()
|
||||
const { push } = useSecondaryPage()
|
||||
const { autoLoadMedia } = useContentPolicy()
|
||||
const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event])
|
||||
|
||||
const titleComponent = <div className="text-xl font-semibold line-clamp-2">{metadata.title}</div>
|
||||
@@ -43,10 +45,10 @@ export default function LongFormArticlePreview({
|
||||
if (isSmallScreen) {
|
||||
return (
|
||||
<div className={className}>
|
||||
{metadata.image && (
|
||||
{metadata.image && autoLoadMedia && (
|
||||
<Image
|
||||
image={{ url: metadata.image, pubkey: event.pubkey }}
|
||||
className="w-full aspect-video object-cover rounded-lg"
|
||||
className="w-full aspect-video"
|
||||
hideIfError
|
||||
/>
|
||||
)}
|
||||
@@ -62,7 +64,7 @@ export default function LongFormArticlePreview({
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="flex gap-4">
|
||||
{metadata.image && (
|
||||
{metadata.image && autoLoadMedia && (
|
||||
<Image
|
||||
image={{ url: metadata.image, pubkey: event.pubkey }}
|
||||
className="rounded-lg aspect-[4/3] xl:aspect-video object-cover bg-foreground h-44"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { generateImageByPubkey } from '@/lib/pubkey'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import Image from '../Image'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export default function ProfileBanner({
|
||||
pubkey,
|
||||
@@ -26,7 +27,7 @@ export default function ProfileBanner({
|
||||
<Image
|
||||
image={{ url: bannerUrl, pubkey }}
|
||||
alt={`${pubkey} banner`}
|
||||
className={className}
|
||||
className={cn('rounded-none', className)}
|
||||
onError={() => setBannerUrl(defaultBanner)}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useFetchWebMetadata } from '@/hooks/useFetchWebMetadata'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||
import { useMemo } from 'react'
|
||||
import Image from '../Image'
|
||||
|
||||
export default function WebPreview({ url, className }: { url: string; className?: string }) {
|
||||
const { autoLoadMedia } = useContentPolicy()
|
||||
const { isSmallScreen } = useScreenSize()
|
||||
const { title, description, image } = useFetchWebMetadata(url)
|
||||
|
||||
@@ -16,6 +18,10 @@ export default function WebPreview({ url, className }: { url: string; className?
|
||||
}
|
||||
}, [url])
|
||||
|
||||
if (!autoLoadMedia) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!title) {
|
||||
return null
|
||||
}
|
||||
@@ -49,7 +55,7 @@ export default function WebPreview({ url, className }: { url: string; className?
|
||||
{image && (
|
||||
<Image
|
||||
image={{ url: image }}
|
||||
className="aspect-[4/3] xl:aspect-video object-cover bg-foreground h-44 rounded-none"
|
||||
className="aspect-[4/3] xl:aspect-video bg-foreground h-44 rounded-none"
|
||||
hideIfError
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||
import mediaManager from '@/services/media-manager.service'
|
||||
import { YouTubePlayer } from '@/types/youtube'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function YoutubeEmbeddedPlayer({
|
||||
url,
|
||||
@@ -10,13 +12,24 @@ export default function YoutubeEmbeddedPlayer({
|
||||
url: string
|
||||
className?: string
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { autoLoadMedia } = useContentPolicy()
|
||||
const [display, setDisplay] = useState(autoLoadMedia)
|
||||
const { videoId, isShort } = useMemo(() => parseYoutubeUrl(url), [url])
|
||||
const [initSuccess, setInitSuccess] = useState(false)
|
||||
const playerRef = useRef<YouTubePlayer | null>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!videoId || !containerRef.current) return
|
||||
if (autoLoadMedia) {
|
||||
setDisplay(true)
|
||||
} else {
|
||||
setDisplay(false)
|
||||
}
|
||||
}, [autoLoadMedia])
|
||||
|
||||
useEffect(() => {
|
||||
if (!videoId || !containerRef.current || !display) return
|
||||
|
||||
if (!window.YT) {
|
||||
const script = document.createElement('script')
|
||||
@@ -62,7 +75,21 @@ export default function YoutubeEmbeddedPlayer({
|
||||
playerRef.current.destroy()
|
||||
}
|
||||
}
|
||||
}, [videoId])
|
||||
}, [videoId, display])
|
||||
|
||||
if (!display) {
|
||||
return (
|
||||
<div
|
||||
className="text-primary hover:underline truncate w-fit cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setDisplay(true)
|
||||
}}
|
||||
>
|
||||
[{t('Click to load YouTube video')}]
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!videoId && !initSuccess) {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user