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 { getHashFromURL } from 'blossom-client-sdk'
|
||||||
import { decode } from 'blurhash'
|
import { decode } from 'blurhash'
|
||||||
import { ImageOff } from 'lucide-react'
|
import { ImageOff } from 'lucide-react'
|
||||||
import { HTMLAttributes, useEffect, useState } from 'react'
|
import { HTMLAttributes, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
|
||||||
export default function Image({
|
export default function Image({
|
||||||
image: { url, blurHash, pubkey },
|
image: { url, blurHash, pubkey, dim },
|
||||||
alt,
|
alt,
|
||||||
className = '',
|
className = '',
|
||||||
classNames = {},
|
classNames = {},
|
||||||
@@ -26,8 +26,7 @@ export default function Image({
|
|||||||
errorPlaceholder?: React.ReactNode
|
errorPlaceholder?: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const [displayBlurHash, setDisplayBlurHash] = useState(true)
|
const [displaySkeleton, setDisplaySkeleton] = useState(true)
|
||||||
const [blurDataUrl, setBlurDataUrl] = useState<string | null>(null)
|
|
||||||
const [hasError, setHasError] = useState(false)
|
const [hasError, setHasError] = useState(false)
|
||||||
const [imageUrl, setImageUrl] = useState(url)
|
const [imageUrl, setImageUrl] = useState(url)
|
||||||
const [tried, setTried] = useState(new Set())
|
const [tried, setTried] = useState(new Set())
|
||||||
@@ -36,32 +35,13 @@ export default function Image({
|
|||||||
setImageUrl(url)
|
setImageUrl(url)
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
setHasError(false)
|
setHasError(false)
|
||||||
setDisplayBlurHash(true)
|
setDisplaySkeleton(true)
|
||||||
setTried(new Set())
|
setTried(new Set())
|
||||||
}, [url])
|
}, [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
|
if (hideIfError && hasError) return null
|
||||||
|
|
||||||
const handleImageError = async () => {
|
const handleError = async () => {
|
||||||
let oldImageUrl: URL | undefined
|
let oldImageUrl: URL | undefined
|
||||||
let hash: string | null = null
|
let hash: string | null = null
|
||||||
try {
|
try {
|
||||||
@@ -101,26 +81,52 @@ export default function Image({
|
|||||||
setImageUrl(nextUrl.toString())
|
setImageUrl(nextUrl.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleLoad = () => {
|
||||||
|
setIsLoading(false)
|
||||||
|
setHasError(false)
|
||||||
|
setTimeout(() => setDisplaySkeleton(false), 600)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('relative', classNames.wrapper)} {...props}>
|
<div className={cn('relative overflow-hidden', classNames.wrapper)} {...props}>
|
||||||
{isLoading && <Skeleton className={cn('absolute inset-0', className)} />}
|
{displaySkeleton && (
|
||||||
{!hasError ? (
|
<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
|
<img
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt={alt}
|
alt={alt}
|
||||||
|
decoding="async"
|
||||||
|
loading="lazy"
|
||||||
|
onLoad={handleLoad}
|
||||||
|
onError={handleError}
|
||||||
className={cn(
|
className={cn(
|
||||||
'object-cover transition-opacity duration-300',
|
'object-cover rounded-lg w-full h-full transition-opacity duration-500',
|
||||||
isLoading ? 'opacity-0' : 'opacity-100',
|
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
onLoad={() => {
|
width={dim?.width}
|
||||||
setIsLoading(false)
|
height={dim?.height}
|
||||||
setHasError(false)
|
{...props}
|
||||||
setTimeout(() => setDisplayBlurHash(false), 500)
|
|
||||||
}}
|
|
||||||
onError={handleImageError}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
)}
|
||||||
|
{hasError && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'object-cover flex flex-col items-center justify-center w-full h-full bg-muted',
|
'object-cover flex flex-col items-center justify-center w-full h-full bg-muted',
|
||||||
@@ -131,21 +137,49 @@ export default function Image({
|
|||||||
{errorPlaceholder}
|
{errorPlaceholder}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{displayBlurHash && blurDataUrl && !hasError && (
|
|
||||||
<img
|
|
||||||
src={blurDataUrl}
|
|
||||||
className={cn('absolute inset-0 object-cover w-full h-full -z-10', className)}
|
|
||||||
alt={alt}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const DIGITS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~'
|
const blurHashWidth = 32
|
||||||
function decodeBlurHashSize(blurHash: string) {
|
const blurHashHeight = 32
|
||||||
const sizeValue = DIGITS.indexOf(blurHash[0])
|
function BlurHashCanvas({ blurHash, className = '' }: { blurHash: string; className?: string }) {
|
||||||
const numY = (sizeValue / 9 + 1) | 0
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||||
const numX = (sizeValue % 9) + 1
|
|
||||||
return { numX, numY }
|
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 { randomString } from '@/lib/random'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||||
import modalManager from '@/services/modal-manager.service'
|
import modalManager from '@/services/modal-manager.service'
|
||||||
import { TImetaInfo } from '@/types'
|
import { TImetaInfo } from '@/types'
|
||||||
import { ReactNode, useEffect, useMemo, useState } from 'react'
|
import { ReactNode, useEffect, useMemo, useState } from 'react'
|
||||||
@@ -7,6 +8,7 @@ import { createPortal } from 'react-dom'
|
|||||||
import Lightbox from 'yet-another-react-lightbox'
|
import Lightbox from 'yet-another-react-lightbox'
|
||||||
import Zoom from 'yet-another-react-lightbox/plugins/zoom'
|
import Zoom from 'yet-another-react-lightbox/plugins/zoom'
|
||||||
import Image from '../Image'
|
import Image from '../Image'
|
||||||
|
import ImageWithLightbox from '../ImageWithLightbox'
|
||||||
|
|
||||||
export default function ImageGallery({
|
export default function ImageGallery({
|
||||||
className,
|
className,
|
||||||
@@ -20,6 +22,7 @@ export default function ImageGallery({
|
|||||||
end?: number
|
end?: number
|
||||||
}) {
|
}) {
|
||||||
const id = useMemo(() => `image-gallery-${randomString()}`, [])
|
const id = useMemo(() => `image-gallery-${randomString()}`, [])
|
||||||
|
const { autoLoadMedia } = useContentPolicy()
|
||||||
const [index, setIndex] = useState(-1)
|
const [index, setIndex] = useState(-1)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
@@ -38,12 +41,26 @@ export default function ImageGallery({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const displayImages = images.slice(start, end)
|
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
|
let imageContent: ReactNode | null = null
|
||||||
if (displayImages.length === 1) {
|
if (displayImages.length === 1) {
|
||||||
imageContent = (
|
imageContent = (
|
||||||
<Image
|
<Image
|
||||||
key={0}
|
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={{
|
classNames={{
|
||||||
errorPlaceholder: 'aspect-square h-[30vh]'
|
errorPlaceholder: 'aspect-square h-[30vh]'
|
||||||
}}
|
}}
|
||||||
@@ -57,7 +74,7 @@ export default function ImageGallery({
|
|||||||
{displayImages.map((image, i) => (
|
{displayImages.map((image, i) => (
|
||||||
<Image
|
<Image
|
||||||
key={i}
|
key={i}
|
||||||
className="aspect-square w-full rounded-lg border cursor-zoom-in"
|
className="aspect-square w-full cursor-zoom-in"
|
||||||
image={image}
|
image={image}
|
||||||
onClick={(e) => handlePhotoClick(e, i)}
|
onClick={(e) => handlePhotoClick(e, i)}
|
||||||
/>
|
/>
|
||||||
@@ -70,7 +87,7 @@ export default function ImageGallery({
|
|||||||
{displayImages.map((image, i) => (
|
{displayImages.map((image, i) => (
|
||||||
<Image
|
<Image
|
||||||
key={i}
|
key={i}
|
||||||
className="aspect-square w-full rounded-lg border cursor-zoom-in"
|
className="aspect-square w-full cursor-zoom-in"
|
||||||
image={image}
|
image={image}
|
||||||
onClick={(e) => handlePhotoClick(e, i)}
|
onClick={(e) => handlePhotoClick(e, i)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,21 +1,30 @@
|
|||||||
import { randomString } from '@/lib/random'
|
import { randomString } from '@/lib/random'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||||
import modalManager from '@/services/modal-manager.service'
|
import modalManager from '@/services/modal-manager.service'
|
||||||
import { TImetaInfo } from '@/types'
|
import { TImetaInfo } from '@/types'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { createPortal } from 'react-dom'
|
import { createPortal } from 'react-dom'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import Lightbox from 'yet-another-react-lightbox'
|
import Lightbox from 'yet-another-react-lightbox'
|
||||||
import Zoom from 'yet-another-react-lightbox/plugins/zoom'
|
import Zoom from 'yet-another-react-lightbox/plugins/zoom'
|
||||||
import Image from '../Image'
|
import Image from '../Image'
|
||||||
|
|
||||||
export default function ImageWithLightbox({
|
export default function ImageWithLightbox({
|
||||||
image,
|
image,
|
||||||
className
|
className,
|
||||||
|
classNames = {}
|
||||||
}: {
|
}: {
|
||||||
image: TImetaInfo
|
image: TImetaInfo
|
||||||
className?: string
|
className?: string
|
||||||
|
classNames?: {
|
||||||
|
wrapper?: string
|
||||||
|
}
|
||||||
}) {
|
}) {
|
||||||
const id = useMemo(() => `image-with-lightbox-${randomString()}`, [])
|
const id = useMemo(() => `image-with-lightbox-${randomString()}`, [])
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { autoLoadMedia } = useContentPolicy()
|
||||||
|
const [display, setDisplay] = useState(autoLoadMedia)
|
||||||
const [index, setIndex] = useState(-1)
|
const [index, setIndex] = useState(-1)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
@@ -27,6 +36,20 @@ export default function ImageWithLightbox({
|
|||||||
}
|
}
|
||||||
}, [index])
|
}, [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) => {
|
const handlePhotoClick = (event: React.MouseEvent) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@@ -34,11 +57,12 @@ export default function ImageWithLightbox({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div>
|
||||||
<Image
|
<Image
|
||||||
key={0}
|
key={0}
|
||||||
className={cn('rounded-lg border cursor-zoom-in', className)}
|
className={className}
|
||||||
classNames={{
|
classNames={{
|
||||||
|
wrapper: cn('rounded-lg border cursor-zoom-in', classNames.wrapper),
|
||||||
errorPlaceholder: 'aspect-square h-[30vh]'
|
errorPlaceholder: 'aspect-square h-[30vh]'
|
||||||
}}
|
}}
|
||||||
image={image}
|
image={image}
|
||||||
|
|||||||
@@ -1,11 +1,28 @@
|
|||||||
|
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import AudioPlayer from '../AudioPlayer'
|
import AudioPlayer from '../AudioPlayer'
|
||||||
import VideoPlayer from '../VideoPlayer'
|
import VideoPlayer from '../VideoPlayer'
|
||||||
|
|
||||||
export default function MediaPlayer({ src, className }: { src: string; className?: string }) {
|
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)
|
const [mediaType, setMediaType] = useState<'video' | 'audio' | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (autoLoadMedia) {
|
||||||
|
setDisplay(true)
|
||||||
|
} else {
|
||||||
|
setDisplay(false)
|
||||||
|
}
|
||||||
|
}, [autoLoadMedia])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!display) {
|
||||||
|
setMediaType(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!src) {
|
if (!src) {
|
||||||
setMediaType(null)
|
setMediaType(null)
|
||||||
return
|
return
|
||||||
@@ -35,7 +52,21 @@ export default function MediaPlayer({ src, className }: { src: string; className
|
|||||||
return () => {
|
return () => {
|
||||||
video.src = ''
|
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) {
|
if (!mediaType) {
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { getCommunityDefinitionFromEvent } from '@/lib/event-metadata'
|
import { getCommunityDefinitionFromEvent } from '@/lib/event-metadata'
|
||||||
|
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||||
import { Event } from 'nostr-tools'
|
import { Event } from 'nostr-tools'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import ClientSelect from '../ClientSelect'
|
import ClientSelect from '../ClientSelect'
|
||||||
@@ -11,6 +12,7 @@ export default function CommunityDefinition({
|
|||||||
event: Event
|
event: Event
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
|
const { autoLoadMedia } = useContentPolicy()
|
||||||
const metadata = useMemo(() => getCommunityDefinitionFromEvent(event), [event])
|
const metadata = useMemo(() => getCommunityDefinitionFromEvent(event), [event])
|
||||||
|
|
||||||
const communityNameComponent = (
|
const communityNameComponent = (
|
||||||
@@ -24,10 +26,10 @@ export default function CommunityDefinition({
|
|||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
{metadata.image && (
|
{metadata.image && autoLoadMedia && (
|
||||||
<Image
|
<Image
|
||||||
image={{ url: metadata.image, pubkey: event.pubkey }}
|
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
|
hideIfError
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { getGroupMetadataFromEvent } from '@/lib/event-metadata'
|
import { getGroupMetadataFromEvent } from '@/lib/event-metadata'
|
||||||
|
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||||
import { Event } from 'nostr-tools'
|
import { Event } from 'nostr-tools'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import ClientSelect from '../ClientSelect'
|
import ClientSelect from '../ClientSelect'
|
||||||
@@ -13,6 +14,7 @@ export default function GroupMetadata({
|
|||||||
originalNoteId?: string
|
originalNoteId?: string
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
|
const { autoLoadMedia } = useContentPolicy()
|
||||||
const metadata = useMemo(() => getGroupMetadataFromEvent(event), [event])
|
const metadata = useMemo(() => getGroupMetadataFromEvent(event), [event])
|
||||||
|
|
||||||
const groupNameComponent = (
|
const groupNameComponent = (
|
||||||
@@ -26,10 +28,10 @@ export default function GroupMetadata({
|
|||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
{metadata.picture && (
|
{metadata.picture && autoLoadMedia && (
|
||||||
<Image
|
<Image
|
||||||
image={{ url: metadata.picture, pubkey: event.pubkey }}
|
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
|
hideIfError
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { getLiveEventMetadataFromEvent } from '@/lib/event-metadata'
|
import { getLiveEventMetadataFromEvent } from '@/lib/event-metadata'
|
||||||
|
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||||
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||||
import { Event } from 'nostr-tools'
|
import { Event } from 'nostr-tools'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
@@ -8,6 +9,8 @@ import Image from '../Image'
|
|||||||
|
|
||||||
export default function LiveEvent({ event, className }: { event: Event; className?: string }) {
|
export default function LiveEvent({ event, className }: { event: Event; className?: string }) {
|
||||||
const { isSmallScreen } = useScreenSize()
|
const { isSmallScreen } = useScreenSize()
|
||||||
|
|
||||||
|
const { autoLoadMedia } = useContentPolicy()
|
||||||
const metadata = useMemo(() => getLiveEventMetadataFromEvent(event), [event])
|
const metadata = useMemo(() => getLiveEventMetadataFromEvent(event), [event])
|
||||||
|
|
||||||
const liveStatusComponent =
|
const liveStatusComponent =
|
||||||
@@ -39,10 +42,10 @@ export default function LiveEvent({ event, className }: { event: Event; classNam
|
|||||||
if (isSmallScreen) {
|
if (isSmallScreen) {
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
{metadata.image && (
|
{metadata.image && autoLoadMedia && (
|
||||||
<Image
|
<Image
|
||||||
image={{ url: metadata.image, pubkey: event.pubkey }}
|
image={{ url: metadata.image, pubkey: event.pubkey }}
|
||||||
className="w-full aspect-video object-cover rounded-lg"
|
className="w-full aspect-video"
|
||||||
hideIfError
|
hideIfError
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -60,10 +63,10 @@ export default function LiveEvent({ event, className }: { event: Event; classNam
|
|||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
{metadata.image && (
|
{metadata.image && autoLoadMedia && (
|
||||||
<Image
|
<Image
|
||||||
image={{ url: metadata.image, pubkey: event.pubkey }}
|
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
|
hideIfError
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -63,7 +63,16 @@ export default function LongFormArticle({
|
|||||||
},
|
},
|
||||||
p: (props) => <p {...props} className="break-words" />,
|
p: (props) => <p {...props} className="break-words" />,
|
||||||
div: (props) => <div {...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,
|
}) as Components,
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
@@ -81,7 +90,7 @@ export default function LongFormArticle({
|
|||||||
{metadata.image && (
|
{metadata.image && (
|
||||||
<ImageWithLightbox
|
<ImageWithLightbox
|
||||||
image={{ url: metadata.image, pubkey: event.pubkey }}
|
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
|
<Markdown
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata'
|
import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata'
|
||||||
import { toNoteList } from '@/lib/link'
|
import { toNoteList } from '@/lib/link'
|
||||||
import { useSecondaryPage } from '@/PageManager'
|
import { useSecondaryPage } from '@/PageManager'
|
||||||
|
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||||
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||||
import { Event, kinds } from 'nostr-tools'
|
import { Event, kinds } from 'nostr-tools'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
@@ -15,6 +16,7 @@ export default function LongFormArticlePreview({
|
|||||||
}) {
|
}) {
|
||||||
const { isSmallScreen } = useScreenSize()
|
const { isSmallScreen } = useScreenSize()
|
||||||
const { push } = useSecondaryPage()
|
const { push } = useSecondaryPage()
|
||||||
|
const { autoLoadMedia } = useContentPolicy()
|
||||||
const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event])
|
const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event])
|
||||||
|
|
||||||
const titleComponent = <div className="text-xl font-semibold line-clamp-2">{metadata.title}</div>
|
const titleComponent = <div className="text-xl font-semibold line-clamp-2">{metadata.title}</div>
|
||||||
@@ -43,10 +45,10 @@ export default function LongFormArticlePreview({
|
|||||||
if (isSmallScreen) {
|
if (isSmallScreen) {
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
{metadata.image && (
|
{metadata.image && autoLoadMedia && (
|
||||||
<Image
|
<Image
|
||||||
image={{ url: metadata.image, pubkey: event.pubkey }}
|
image={{ url: metadata.image, pubkey: event.pubkey }}
|
||||||
className="w-full aspect-video object-cover rounded-lg"
|
className="w-full aspect-video"
|
||||||
hideIfError
|
hideIfError
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -62,7 +64,7 @@ export default function LongFormArticlePreview({
|
|||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
{metadata.image && (
|
{metadata.image && autoLoadMedia && (
|
||||||
<Image
|
<Image
|
||||||
image={{ url: metadata.image, pubkey: event.pubkey }}
|
image={{ url: metadata.image, pubkey: event.pubkey }}
|
||||||
className="rounded-lg aspect-[4/3] xl:aspect-video object-cover bg-foreground h-44"
|
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 { generateImageByPubkey } from '@/lib/pubkey'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import Image from '../Image'
|
import Image from '../Image'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
export default function ProfileBanner({
|
export default function ProfileBanner({
|
||||||
pubkey,
|
pubkey,
|
||||||
@@ -26,7 +27,7 @@ export default function ProfileBanner({
|
|||||||
<Image
|
<Image
|
||||||
image={{ url: bannerUrl, pubkey }}
|
image={{ url: bannerUrl, pubkey }}
|
||||||
alt={`${pubkey} banner`}
|
alt={`${pubkey} banner`}
|
||||||
className={className}
|
className={cn('rounded-none', className)}
|
||||||
onError={() => setBannerUrl(defaultBanner)}
|
onError={() => setBannerUrl(defaultBanner)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { useFetchWebMetadata } from '@/hooks/useFetchWebMetadata'
|
import { useFetchWebMetadata } from '@/hooks/useFetchWebMetadata'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||||
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import Image from '../Image'
|
import Image from '../Image'
|
||||||
|
|
||||||
export default function WebPreview({ url, className }: { url: string; className?: string }) {
|
export default function WebPreview({ url, className }: { url: string; className?: string }) {
|
||||||
|
const { autoLoadMedia } = useContentPolicy()
|
||||||
const { isSmallScreen } = useScreenSize()
|
const { isSmallScreen } = useScreenSize()
|
||||||
const { title, description, image } = useFetchWebMetadata(url)
|
const { title, description, image } = useFetchWebMetadata(url)
|
||||||
|
|
||||||
@@ -16,6 +18,10 @@ export default function WebPreview({ url, className }: { url: string; className?
|
|||||||
}
|
}
|
||||||
}, [url])
|
}, [url])
|
||||||
|
|
||||||
|
if (!autoLoadMedia) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
if (!title) {
|
if (!title) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -49,7 +55,7 @@ export default function WebPreview({ url, className }: { url: string; className?
|
|||||||
{image && (
|
{image && (
|
||||||
<Image
|
<Image
|
||||||
image={{ url: 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
|
hideIfError
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||||
import mediaManager from '@/services/media-manager.service'
|
import mediaManager from '@/services/media-manager.service'
|
||||||
import { YouTubePlayer } from '@/types/youtube'
|
import { YouTubePlayer } from '@/types/youtube'
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
export default function YoutubeEmbeddedPlayer({
|
export default function YoutubeEmbeddedPlayer({
|
||||||
url,
|
url,
|
||||||
@@ -10,13 +12,24 @@ export default function YoutubeEmbeddedPlayer({
|
|||||||
url: string
|
url: string
|
||||||
className?: string
|
className?: string
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { autoLoadMedia } = useContentPolicy()
|
||||||
|
const [display, setDisplay] = useState(autoLoadMedia)
|
||||||
const { videoId, isShort } = useMemo(() => parseYoutubeUrl(url), [url])
|
const { videoId, isShort } = useMemo(() => parseYoutubeUrl(url), [url])
|
||||||
const [initSuccess, setInitSuccess] = useState(false)
|
const [initSuccess, setInitSuccess] = useState(false)
|
||||||
const playerRef = useRef<YouTubePlayer | null>(null)
|
const playerRef = useRef<YouTubePlayer | null>(null)
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!videoId || !containerRef.current) return
|
if (autoLoadMedia) {
|
||||||
|
setDisplay(true)
|
||||||
|
} else {
|
||||||
|
setDisplay(false)
|
||||||
|
}
|
||||||
|
}, [autoLoadMedia])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!videoId || !containerRef.current || !display) return
|
||||||
|
|
||||||
if (!window.YT) {
|
if (!window.YT) {
|
||||||
const script = document.createElement('script')
|
const script = document.createElement('script')
|
||||||
@@ -62,7 +75,21 @@ export default function YoutubeEmbeddedPlayer({
|
|||||||
playerRef.current.destroy()
|
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) {
|
if (!videoId && !initSuccess) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export const StorageKey = {
|
|||||||
SHOW_KINDS_VERSION: 'showKindsVersion',
|
SHOW_KINDS_VERSION: 'showKindsVersion',
|
||||||
HIDE_CONTENT_MENTIONING_MUTED_USERS: 'hideContentMentioningMutedUsers',
|
HIDE_CONTENT_MENTIONING_MUTED_USERS: 'hideContentMentioningMutedUsers',
|
||||||
NOTIFICATION_LIST_STYLE: 'notificationListStyle',
|
NOTIFICATION_LIST_STYLE: 'notificationListStyle',
|
||||||
|
MEDIA_AUTO_LOAD_POLICY: 'mediaAutoLoadPolicy',
|
||||||
MEDIA_UPLOAD_SERVICE: 'mediaUploadService', // deprecated
|
MEDIA_UPLOAD_SERVICE: 'mediaUploadService', // deprecated
|
||||||
HIDE_UNTRUSTED_EVENTS: 'hideUntrustedEvents', // deprecated
|
HIDE_UNTRUSTED_EVENTS: 'hideUntrustedEvents', // deprecated
|
||||||
ACCOUNT_RELAY_LIST_EVENT_MAP: 'accountRelayListEventMap', // deprecated
|
ACCOUNT_RELAY_LIST_EVENT_MAP: 'accountRelayListEventMap', // deprecated
|
||||||
@@ -139,3 +140,9 @@ export const NOTIFICATION_LIST_STYLE = {
|
|||||||
COMPACT: 'compact',
|
COMPACT: 'compact',
|
||||||
DETAILED: 'detailed'
|
DETAILED: 'detailed'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
export const MEDIA_AUTO_LOAD_POLICY = {
|
||||||
|
ALWAYS: 'always',
|
||||||
|
WIFI_ONLY: 'wifi-only',
|
||||||
|
NEVER: 'never'
|
||||||
|
} as const
|
||||||
|
|||||||
@@ -400,6 +400,12 @@ export default {
|
|||||||
'Submit Relay': 'إرسال ريلاي',
|
'Submit Relay': 'إرسال ريلاي',
|
||||||
Homepage: 'الصفحة الرئيسية',
|
Homepage: 'الصفحة الرئيسية',
|
||||||
'Proof of Work (difficulty {{minPow}})': 'إثبات العمل (الصعوبة {{minPow}})',
|
'Proof of Work (difficulty {{minPow}})': 'إثبات العمل (الصعوبة {{minPow}})',
|
||||||
'via {{client}}': 'عبر {{client}}'
|
'via {{client}}': 'عبر {{client}}',
|
||||||
|
'Auto-load media': 'تحميل الوسائط تلقائياً',
|
||||||
|
Always: 'دائماً',
|
||||||
|
'Wi-Fi only': 'Wi-Fi فقط',
|
||||||
|
Never: 'أبداً',
|
||||||
|
'Click to load image': 'انقر لتحميل الصورة',
|
||||||
|
'Click to load media': 'انقر لتحميل الوسائط'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -410,6 +410,12 @@ export default {
|
|||||||
'Submit Relay': 'Relay einreichen',
|
'Submit Relay': 'Relay einreichen',
|
||||||
Homepage: 'Homepage',
|
Homepage: 'Homepage',
|
||||||
'Proof of Work (difficulty {{minPow}})': 'Arbeitsnachweis (Schwierigkeit {{minPow}})',
|
'Proof of Work (difficulty {{minPow}})': 'Arbeitsnachweis (Schwierigkeit {{minPow}})',
|
||||||
'via {{client}}': 'über {{client}}'
|
'via {{client}}': 'über {{client}}',
|
||||||
|
'Auto-load media': 'Medien automatisch laden',
|
||||||
|
Always: 'Immer',
|
||||||
|
'Wi-Fi only': 'Nur WLAN',
|
||||||
|
Never: 'Nie',
|
||||||
|
'Click to load image': 'Klicken, um Bild zu laden',
|
||||||
|
'Click to load media': 'Klicken, um Medien zu laden'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -399,6 +399,12 @@ export default {
|
|||||||
'Submit Relay': 'Submit Relay',
|
'Submit Relay': 'Submit Relay',
|
||||||
Homepage: 'Homepage',
|
Homepage: 'Homepage',
|
||||||
'Proof of Work (difficulty {{minPow}})': 'Proof of Work (difficulty {{minPow}})',
|
'Proof of Work (difficulty {{minPow}})': 'Proof of Work (difficulty {{minPow}})',
|
||||||
'via {{client}}': 'via {{client}}'
|
'via {{client}}': 'via {{client}}',
|
||||||
|
'Auto-load media': 'Auto-load media',
|
||||||
|
Always: 'Always',
|
||||||
|
'Wi-Fi only': 'Wi-Fi only',
|
||||||
|
Never: 'Never',
|
||||||
|
'Click to load image': 'Click to load image',
|
||||||
|
'Click to load media': 'Click to load media'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -405,6 +405,12 @@ export default {
|
|||||||
'Submit Relay': 'Enviar relé',
|
'Submit Relay': 'Enviar relé',
|
||||||
Homepage: 'Página principal',
|
Homepage: 'Página principal',
|
||||||
'Proof of Work (difficulty {{minPow}})': 'Prueba de Trabajo (dificultad {{minPow}})',
|
'Proof of Work (difficulty {{minPow}})': 'Prueba de Trabajo (dificultad {{minPow}})',
|
||||||
'via {{client}}': 'vía {{client}}'
|
'via {{client}}': 'vía {{client}}',
|
||||||
|
'Auto-load media': 'Cargar medios automáticamente',
|
||||||
|
Always: 'Siempre',
|
||||||
|
'Wi-Fi only': 'Solo Wi-Fi',
|
||||||
|
Never: 'Nunca',
|
||||||
|
'Click to load image': 'Haz clic para cargar la imagen',
|
||||||
|
'Click to load media': 'Haz clic para cargar los medios'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -401,6 +401,12 @@ export default {
|
|||||||
'Submit Relay': 'ارسال رله',
|
'Submit Relay': 'ارسال رله',
|
||||||
Homepage: 'صفحه اصلی',
|
Homepage: 'صفحه اصلی',
|
||||||
'Proof of Work (difficulty {{minPow}})': 'اثبات کار (دشواری {{minPow}})',
|
'Proof of Work (difficulty {{minPow}})': 'اثبات کار (دشواری {{minPow}})',
|
||||||
'via {{client}}': 'از طریق {{client}}'
|
'via {{client}}': 'از طریق {{client}}',
|
||||||
|
'Auto-load media': 'بارگذاری خودکار رسانه',
|
||||||
|
Always: 'همیشه',
|
||||||
|
'Wi-Fi only': 'فقط Wi-Fi',
|
||||||
|
Never: 'هرگز',
|
||||||
|
'Click to load image': 'برای بارگذاری تصویر کلیک کنید',
|
||||||
|
'Click to load media': 'برای بارگذاری رسانه کلیک کنید'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -410,6 +410,12 @@ export default {
|
|||||||
'Submit Relay': 'Soumettre un relais',
|
'Submit Relay': 'Soumettre un relais',
|
||||||
Homepage: 'Page d’accueil',
|
Homepage: 'Page d’accueil',
|
||||||
'Proof of Work (difficulty {{minPow}})': 'Preuve de travail (difficulté {{minPow}})',
|
'Proof of Work (difficulty {{minPow}})': 'Preuve de travail (difficulté {{minPow}})',
|
||||||
'via {{client}}': 'via {{client}}'
|
'via {{client}}': 'via {{client}}',
|
||||||
|
'Auto-load media': 'Auto-chargement des médias',
|
||||||
|
Always: 'Toujours',
|
||||||
|
'Wi-Fi only': 'Wi-Fi uniquement',
|
||||||
|
Never: 'Jamais',
|
||||||
|
'Click to load image': 'Cliquez pour charger l’image',
|
||||||
|
'Click to load media': 'Cliquez pour charger les médias'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -404,6 +404,12 @@ export default {
|
|||||||
'Submit Relay': 'रिले सबमिट करें',
|
'Submit Relay': 'रिले सबमिट करें',
|
||||||
Homepage: 'होमपेज',
|
Homepage: 'होमपेज',
|
||||||
'Proof of Work (difficulty {{minPow}})': 'कार्य प्रमाण (कठिनाई {{minPow}})',
|
'Proof of Work (difficulty {{minPow}})': 'कार्य प्रमाण (कठिनाई {{minPow}})',
|
||||||
'via {{client}}': 'के माध्यम से {{client}}'
|
'via {{client}}': 'के माध्यम से {{client}}',
|
||||||
|
'Auto-load media': 'मीडिया स्वतः लोड करें',
|
||||||
|
Always: 'हमेशा',
|
||||||
|
'Wi-Fi only': 'केवल Wi-Fi',
|
||||||
|
Never: 'कभी नहीं',
|
||||||
|
'Click to load image': 'इमेज लोड करने के लिए क्लिक करें',
|
||||||
|
'Click to load media': 'मीडिया लोड करने के लिए क्लिक करें'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -405,6 +405,12 @@ export default {
|
|||||||
'Submit Relay': 'Invia Relay',
|
'Submit Relay': 'Invia Relay',
|
||||||
Homepage: 'Homepage',
|
Homepage: 'Homepage',
|
||||||
'Proof of Work (difficulty {{minPow}})': 'Proof of Work (difficoltà {{minPow}})',
|
'Proof of Work (difficulty {{minPow}})': 'Proof of Work (difficoltà {{minPow}})',
|
||||||
'via {{client}}': 'tramite {{client}}'
|
'via {{client}}': 'tramite {{client}}',
|
||||||
|
'Auto-load media': 'Caricamento automatico media',
|
||||||
|
Always: 'Sempre',
|
||||||
|
'Wi-Fi only': 'Solo Wi-Fi',
|
||||||
|
Never: 'Mai',
|
||||||
|
'Click to load image': "Clicca per caricare l'immagine",
|
||||||
|
'Click to load media': 'Clicca per caricare i media'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -402,6 +402,12 @@ export default {
|
|||||||
'Submit Relay': 'リレーを提出',
|
'Submit Relay': 'リレーを提出',
|
||||||
Homepage: 'ホームページ',
|
Homepage: 'ホームページ',
|
||||||
'Proof of Work (difficulty {{minPow}})': 'プルーフオブワーク (難易度 {{minPow}})',
|
'Proof of Work (difficulty {{minPow}})': 'プルーフオブワーク (難易度 {{minPow}})',
|
||||||
'via {{client}}': '{{client}} 経由'
|
'via {{client}}': '{{client}} 経由',
|
||||||
|
'Auto-load media': 'メディアの自動読み込み',
|
||||||
|
Always: '常に',
|
||||||
|
'Wi-Fi only': 'Wi-Fiのみ',
|
||||||
|
Never: 'しない',
|
||||||
|
'Click to load image': 'クリックして画像を読み込む',
|
||||||
|
'Click to load media': 'クリックしてメディアを読み込む'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -402,6 +402,12 @@ export default {
|
|||||||
'Submit Relay': '릴레이 제출',
|
'Submit Relay': '릴레이 제출',
|
||||||
Homepage: '홈페이지',
|
Homepage: '홈페이지',
|
||||||
'Proof of Work (difficulty {{minPow}})': '작업 증명 (난이도 {{minPow}})',
|
'Proof of Work (difficulty {{minPow}})': '작업 증명 (난이도 {{minPow}})',
|
||||||
'via {{client}}': '{{client}} 통해'
|
'via {{client}}': '{{client}} 통해',
|
||||||
|
'Auto-load media': '미디어 자동 로드',
|
||||||
|
Always: '항상',
|
||||||
|
'Wi-Fi only': 'Wi-Fi만',
|
||||||
|
Never: '안함',
|
||||||
|
'Click to load image': '이미지 로드하려면 클릭',
|
||||||
|
'Click to load media': '미디어 로드하려면 클릭'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -406,6 +406,12 @@ export default {
|
|||||||
'Submit Relay': 'Prześlij przekaźnik',
|
'Submit Relay': 'Prześlij przekaźnik',
|
||||||
Homepage: 'Strona główna',
|
Homepage: 'Strona główna',
|
||||||
'Proof of Work (difficulty {{minPow}})': 'Dowód pracy (trudność {{minPow}})',
|
'Proof of Work (difficulty {{minPow}})': 'Dowód pracy (trudność {{minPow}})',
|
||||||
'via {{client}}': 'przez {{client}}'
|
'via {{client}}': 'przez {{client}}',
|
||||||
|
'Auto-load media': 'Automatyczne ładowanie mediów',
|
||||||
|
Always: 'Zawsze',
|
||||||
|
'Wi-Fi only': 'Tylko Wi-Fi',
|
||||||
|
Never: 'Nigdy',
|
||||||
|
'Click to load image': 'Kliknij, aby załadować obraz',
|
||||||
|
'Click to load media': 'Kliknij, aby załadować media'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -402,6 +402,12 @@ export default {
|
|||||||
'Submit Relay': 'Enviar Relay',
|
'Submit Relay': 'Enviar Relay',
|
||||||
Homepage: 'Página inicial',
|
Homepage: 'Página inicial',
|
||||||
'Proof of Work (difficulty {{minPow}})': 'Prova de Trabalho (dificuldade {{minPow}})',
|
'Proof of Work (difficulty {{minPow}})': 'Prova de Trabalho (dificuldade {{minPow}})',
|
||||||
'via {{client}}': 'via {{client}}'
|
'via {{client}}': 'via {{client}}',
|
||||||
|
'Auto-load media': 'Carregamento automático de mídia',
|
||||||
|
Always: 'Sempre',
|
||||||
|
'Wi-Fi only': 'Apenas Wi-Fi',
|
||||||
|
Never: 'Nunca',
|
||||||
|
'Click to load image': 'Clique para carregar a imagem',
|
||||||
|
'Click to load media': 'Clique para carregar a mídia'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -405,6 +405,12 @@ export default {
|
|||||||
'Submit Relay': 'Enviar Relay',
|
'Submit Relay': 'Enviar Relay',
|
||||||
Homepage: 'Página inicial',
|
Homepage: 'Página inicial',
|
||||||
'Proof of Work (difficulty {{minPow}})': 'Prova de Trabalho (dificuldade {{minPow}})',
|
'Proof of Work (difficulty {{minPow}})': 'Prova de Trabalho (dificuldade {{minPow}})',
|
||||||
'via {{client}}': 'via {{client}}'
|
'via {{client}}': 'via {{client}}',
|
||||||
|
'Auto-load media': 'Carregamento automático de multimédia',
|
||||||
|
Always: 'Sempre',
|
||||||
|
'Wi-Fi only': 'Apenas Wi-Fi',
|
||||||
|
Never: 'Nunca',
|
||||||
|
'Click to load image': 'Clique para carregar a imagem',
|
||||||
|
'Click to load media': 'Clique para carregar a mídia'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -407,6 +407,12 @@ export default {
|
|||||||
'Submit Relay': 'Отправить релей',
|
'Submit Relay': 'Отправить релей',
|
||||||
Homepage: 'Домашняя страница',
|
Homepage: 'Домашняя страница',
|
||||||
'Proof of Work (difficulty {{minPow}})': 'Доказательство работы (сложность {{minPow}})',
|
'Proof of Work (difficulty {{minPow}})': 'Доказательство работы (сложность {{minPow}})',
|
||||||
'via {{client}}': 'через {{client}}'
|
'via {{client}}': 'через {{client}}',
|
||||||
|
'Auto-load media': 'Автозагрузка медиа',
|
||||||
|
Always: 'Всегда',
|
||||||
|
'Wi-Fi only': 'Только Wi-Fi',
|
||||||
|
Never: 'Никогда',
|
||||||
|
'Click to load image': 'Нажмите, чтобы загрузить изображение',
|
||||||
|
'Click to load media': 'Нажмите, чтобы загрузить медиа'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -398,6 +398,12 @@ export default {
|
|||||||
'Submit Relay': 'ส่งรีเลย์',
|
'Submit Relay': 'ส่งรีเลย์',
|
||||||
Homepage: 'หน้าแรก',
|
Homepage: 'หน้าแรก',
|
||||||
'Proof of Work (difficulty {{minPow}})': 'หลักฐานการทำงาน (ความยาก {{minPow}})',
|
'Proof of Work (difficulty {{minPow}})': 'หลักฐานการทำงาน (ความยาก {{minPow}})',
|
||||||
'via {{client}}': 'ผ่าน {{client}}'
|
'via {{client}}': 'ผ่าน {{client}}',
|
||||||
|
'Auto-load media': 'โหลดสื่ออัตโนมัติ',
|
||||||
|
Always: 'เสมอ',
|
||||||
|
'Wi-Fi only': 'Wi-Fi เท่านั้น',
|
||||||
|
Never: 'ไม่เลย',
|
||||||
|
'Click to load image': 'คลิกเพื่อโหลดรูปภาพ',
|
||||||
|
'Click to load media': 'คลิกเพื่อโหลดสื่อ'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -396,6 +396,12 @@ export default {
|
|||||||
'Submit Relay': '提交服务器',
|
'Submit Relay': '提交服务器',
|
||||||
Homepage: '主页',
|
Homepage: '主页',
|
||||||
'Proof of Work (difficulty {{minPow}})': '工作量证明 (难度 {{minPow}})',
|
'Proof of Work (difficulty {{minPow}})': '工作量证明 (难度 {{minPow}})',
|
||||||
'via {{client}}': '来自 {{client}}'
|
'via {{client}}': '来自 {{client}}',
|
||||||
|
'Auto-load media': '自动加载媒体文件',
|
||||||
|
Always: '始终',
|
||||||
|
'Wi-Fi only': '仅WiFi',
|
||||||
|
Never: '从不',
|
||||||
|
'Click to load image': '点击加载图片',
|
||||||
|
'Click to load media': '点击加载音视频'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ export function isPartiallyInViewport(el: HTMLElement) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isSupportCheckConnectionType() {
|
||||||
|
if (typeof window === 'undefined' || !(navigator as any).connection) return false
|
||||||
|
return typeof (navigator as any).connection.type === 'string'
|
||||||
|
}
|
||||||
|
|
||||||
export function isEmail(email: string) {
|
export function isEmail(email: string) {
|
||||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select'
|
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select'
|
||||||
import { Switch } from '@/components/ui/switch'
|
import { Switch } from '@/components/ui/switch'
|
||||||
import { NOTIFICATION_LIST_STYLE } from '@/constants'
|
import { MEDIA_AUTO_LOAD_POLICY, NOTIFICATION_LIST_STYLE } from '@/constants'
|
||||||
import { LocalizedLanguageNames, TLanguage } from '@/i18n'
|
import { LocalizedLanguageNames, TLanguage } from '@/i18n'
|
||||||
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
|
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn, isSupportCheckConnectionType } from '@/lib/utils'
|
||||||
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||||
import { useTheme } from '@/providers/ThemeProvider'
|
import { useTheme } from '@/providers/ThemeProvider'
|
||||||
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
|
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
|
||||||
import { useUserTrust } from '@/providers/UserTrustProvider'
|
import { useUserTrust } from '@/providers/UserTrustProvider'
|
||||||
|
import { TMediaAutoLoadPolicy } from '@/types'
|
||||||
import { SelectValue } from '@radix-ui/react-select'
|
import { SelectValue } from '@radix-ui/react-select'
|
||||||
import { ExternalLink } from 'lucide-react'
|
import { ExternalLink } from 'lucide-react'
|
||||||
import { forwardRef, HTMLProps, useState } from 'react'
|
import { forwardRef, HTMLProps, useState } from 'react'
|
||||||
@@ -24,7 +25,9 @@ const GeneralSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
|
|||||||
defaultShowNsfw,
|
defaultShowNsfw,
|
||||||
setDefaultShowNsfw,
|
setDefaultShowNsfw,
|
||||||
hideContentMentioningMutedUsers,
|
hideContentMentioningMutedUsers,
|
||||||
setHideContentMentioningMutedUsers
|
setHideContentMentioningMutedUsers,
|
||||||
|
mediaAutoLoadPolicy,
|
||||||
|
setMediaAutoLoadPolicy
|
||||||
} = useContentPolicy()
|
} = useContentPolicy()
|
||||||
const { hideUntrustedNotes, updateHideUntrustedNotes } = useUserTrust()
|
const { hideUntrustedNotes, updateHideUntrustedNotes } = useUserTrust()
|
||||||
const { notificationListStyle, updateNotificationListStyle } = useUserPreferences()
|
const { notificationListStyle, updateNotificationListStyle } = useUserPreferences()
|
||||||
@@ -92,6 +95,29 @@ const GeneralSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
<SettingItem>
|
||||||
|
<Label htmlFor="media-auto-load-policy" className="text-base font-normal">
|
||||||
|
{t('Auto-load media')}
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
defaultValue="wifi-only"
|
||||||
|
value={mediaAutoLoadPolicy}
|
||||||
|
onValueChange={(value: TMediaAutoLoadPolicy) =>
|
||||||
|
setMediaAutoLoadPolicy(value as TMediaAutoLoadPolicy)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="media-auto-load-policy" className="w-48">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={MEDIA_AUTO_LOAD_POLICY.ALWAYS}>{t('Always')}</SelectItem>
|
||||||
|
{isSupportCheckConnectionType() && (
|
||||||
|
<SelectItem value={MEDIA_AUTO_LOAD_POLICY.WIFI_ONLY}>{t('Wi-Fi only')}</SelectItem>
|
||||||
|
)}
|
||||||
|
<SelectItem value={MEDIA_AUTO_LOAD_POLICY.NEVER}>{t('Never')}</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</SettingItem>
|
||||||
<SettingItem>
|
<SettingItem>
|
||||||
<Label htmlFor="autoplay" className="text-base font-normal">
|
<Label htmlFor="autoplay" className="text-base font-normal">
|
||||||
<div>{t('Autoplay')}</div>
|
<div>{t('Autoplay')}</div>
|
||||||
|
|||||||
@@ -131,11 +131,7 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
|
|||||||
onUploadEnd={() => setUploadingBanner(false)}
|
onUploadEnd={() => setUploadingBanner(false)}
|
||||||
className="w-full relative cursor-pointer"
|
className="w-full relative cursor-pointer"
|
||||||
>
|
>
|
||||||
<ProfileBanner
|
<ProfileBanner banner={banner} pubkey={account.pubkey} className="w-full aspect-[3/1]" />
|
||||||
banner={banner}
|
|
||||||
pubkey={account.pubkey}
|
|
||||||
className="w-full aspect-[3/1] object-cover"
|
|
||||||
/>
|
|
||||||
<div className="absolute top-0 bg-muted/30 w-full h-full flex flex-col justify-center items-center">
|
<div className="absolute top-0 bg-muted/30 w-full h-full flex flex-col justify-center items-center">
|
||||||
{uploadingBanner ? <Loader size={36} className="animate-spin" /> : <Upload size={36} />}
|
{uploadingBanner ? <Loader size={36} className="animate-spin" /> : <Upload size={36} />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import { MEDIA_AUTO_LOAD_POLICY } from '@/constants'
|
||||||
import storage from '@/services/local-storage.service'
|
import storage from '@/services/local-storage.service'
|
||||||
import { createContext, useContext, useState } from 'react'
|
import { TMediaAutoLoadPolicy } from '@/types'
|
||||||
|
import { createContext, useContext, useEffect, useMemo, useState } from 'react'
|
||||||
|
|
||||||
type TContentPolicyContext = {
|
type TContentPolicyContext = {
|
||||||
autoplay: boolean
|
autoplay: boolean
|
||||||
@@ -10,6 +12,10 @@ type TContentPolicyContext = {
|
|||||||
|
|
||||||
hideContentMentioningMutedUsers?: boolean
|
hideContentMentioningMutedUsers?: boolean
|
||||||
setHideContentMentioningMutedUsers?: (hide: boolean) => void
|
setHideContentMentioningMutedUsers?: (hide: boolean) => void
|
||||||
|
|
||||||
|
autoLoadMedia: boolean
|
||||||
|
mediaAutoLoadPolicy: TMediaAutoLoadPolicy
|
||||||
|
setMediaAutoLoadPolicy: (policy: TMediaAutoLoadPolicy) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const ContentPolicyContext = createContext<TContentPolicyContext | undefined>(undefined)
|
const ContentPolicyContext = createContext<TContentPolicyContext | undefined>(undefined)
|
||||||
@@ -23,11 +29,39 @@ export const useContentPolicy = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ContentPolicyProvider({ children }: { children: React.ReactNode }) {
|
export function ContentPolicyProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [autoplay, setAutoplay] = useState<boolean>(storage.getAutoplay())
|
const [autoplay, setAutoplay] = useState(storage.getAutoplay())
|
||||||
const [defaultShowNsfw, setDefaultShowNsfw] = useState<boolean>(storage.getDefaultShowNsfw())
|
const [defaultShowNsfw, setDefaultShowNsfw] = useState(storage.getDefaultShowNsfw())
|
||||||
const [hideContentMentioningMutedUsers, setHideContentMentioningMutedUsers] = useState<boolean>(
|
const [hideContentMentioningMutedUsers, setHideContentMentioningMutedUsers] = useState(
|
||||||
storage.getHideContentMentioningMutedUsers()
|
storage.getHideContentMentioningMutedUsers()
|
||||||
)
|
)
|
||||||
|
const [mediaAutoLoadPolicy, setMediaAutoLoadPolicy] = useState(storage.getMediaAutoLoadPolicy())
|
||||||
|
const [connectionType, setConnectionType] = useState((navigator as any).connection?.type)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const connection = (navigator as any).connection
|
||||||
|
if (!connection) {
|
||||||
|
setConnectionType(undefined)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const handleConnectionChange = () => {
|
||||||
|
setConnectionType(connection.type)
|
||||||
|
}
|
||||||
|
connection.addEventListener('change', handleConnectionChange)
|
||||||
|
return () => {
|
||||||
|
connection.removeEventListener('change', handleConnectionChange)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const autoLoadMedia = useMemo(() => {
|
||||||
|
if (mediaAutoLoadPolicy === MEDIA_AUTO_LOAD_POLICY.ALWAYS) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (mediaAutoLoadPolicy === MEDIA_AUTO_LOAD_POLICY.NEVER) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// WIFI_ONLY
|
||||||
|
return connectionType === 'wifi' || connectionType === 'ethernet'
|
||||||
|
}, [mediaAutoLoadPolicy, connectionType])
|
||||||
|
|
||||||
const updateAutoplay = (autoplay: boolean) => {
|
const updateAutoplay = (autoplay: boolean) => {
|
||||||
storage.setAutoplay(autoplay)
|
storage.setAutoplay(autoplay)
|
||||||
@@ -44,6 +78,11 @@ export function ContentPolicyProvider({ children }: { children: React.ReactNode
|
|||||||
setHideContentMentioningMutedUsers(hide)
|
setHideContentMentioningMutedUsers(hide)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateMediaAutoLoadPolicy = (policy: TMediaAutoLoadPolicy) => {
|
||||||
|
storage.setMediaAutoLoadPolicy(policy)
|
||||||
|
setMediaAutoLoadPolicy(policy)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContentPolicyContext.Provider
|
<ContentPolicyContext.Provider
|
||||||
value={{
|
value={{
|
||||||
@@ -52,7 +91,10 @@ export function ContentPolicyProvider({ children }: { children: React.ReactNode
|
|||||||
defaultShowNsfw,
|
defaultShowNsfw,
|
||||||
setDefaultShowNsfw: updateDefaultShowNsfw,
|
setDefaultShowNsfw: updateDefaultShowNsfw,
|
||||||
hideContentMentioningMutedUsers,
|
hideContentMentioningMutedUsers,
|
||||||
setHideContentMentioningMutedUsers: updateHideContentMentioningMutedUsers
|
setHideContentMentioningMutedUsers: updateHideContentMentioningMutedUsers,
|
||||||
|
autoLoadMedia,
|
||||||
|
mediaAutoLoadPolicy,
|
||||||
|
setMediaAutoLoadPolicy: updateMediaAutoLoadPolicy
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
DEFAULT_NIP_96_SERVICE,
|
DEFAULT_NIP_96_SERVICE,
|
||||||
ExtendedKind,
|
ExtendedKind,
|
||||||
|
MEDIA_AUTO_LOAD_POLICY,
|
||||||
NOTIFICATION_LIST_STYLE,
|
NOTIFICATION_LIST_STYLE,
|
||||||
SUPPORTED_KINDS,
|
SUPPORTED_KINDS,
|
||||||
StorageKey
|
StorageKey
|
||||||
@@ -11,6 +12,7 @@ import {
|
|||||||
TAccount,
|
TAccount,
|
||||||
TAccountPointer,
|
TAccountPointer,
|
||||||
TFeedInfo,
|
TFeedInfo,
|
||||||
|
TMediaAutoLoadPolicy,
|
||||||
TMediaUploadServiceConfig,
|
TMediaUploadServiceConfig,
|
||||||
TNoteListMode,
|
TNoteListMode,
|
||||||
TNotificationStyle,
|
TNotificationStyle,
|
||||||
@@ -44,6 +46,7 @@ class LocalStorageService {
|
|||||||
private showKinds: number[] = []
|
private showKinds: number[] = []
|
||||||
private hideContentMentioningMutedUsers: boolean = false
|
private hideContentMentioningMutedUsers: boolean = false
|
||||||
private notificationListStyle: TNotificationStyle = NOTIFICATION_LIST_STYLE.DETAILED
|
private notificationListStyle: TNotificationStyle = NOTIFICATION_LIST_STYLE.DETAILED
|
||||||
|
private mediaAutoLoadPolicy: TMediaAutoLoadPolicy = MEDIA_AUTO_LOAD_POLICY.ALWAYS
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
if (!LocalStorageService.instance) {
|
if (!LocalStorageService.instance) {
|
||||||
@@ -174,6 +177,14 @@ class LocalStorageService {
|
|||||||
? NOTIFICATION_LIST_STYLE.COMPACT
|
? NOTIFICATION_LIST_STYLE.COMPACT
|
||||||
: NOTIFICATION_LIST_STYLE.DETAILED
|
: NOTIFICATION_LIST_STYLE.DETAILED
|
||||||
|
|
||||||
|
const mediaAutoLoadPolicy = window.localStorage.getItem(StorageKey.MEDIA_AUTO_LOAD_POLICY)
|
||||||
|
if (
|
||||||
|
mediaAutoLoadPolicy &&
|
||||||
|
Object.values(MEDIA_AUTO_LOAD_POLICY).includes(mediaAutoLoadPolicy as TMediaAutoLoadPolicy)
|
||||||
|
) {
|
||||||
|
this.mediaAutoLoadPolicy = mediaAutoLoadPolicy as TMediaAutoLoadPolicy
|
||||||
|
}
|
||||||
|
|
||||||
// Clean up deprecated data
|
// Clean up deprecated data
|
||||||
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)
|
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)
|
||||||
window.localStorage.removeItem(StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP)
|
window.localStorage.removeItem(StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP)
|
||||||
@@ -433,6 +444,15 @@ class LocalStorageService {
|
|||||||
this.notificationListStyle = style
|
this.notificationListStyle = style
|
||||||
window.localStorage.setItem(StorageKey.NOTIFICATION_LIST_STYLE, style)
|
window.localStorage.setItem(StorageKey.NOTIFICATION_LIST_STYLE, style)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMediaAutoLoadPolicy() {
|
||||||
|
return this.mediaAutoLoadPolicy
|
||||||
|
}
|
||||||
|
|
||||||
|
setMediaAutoLoadPolicy(policy: TMediaAutoLoadPolicy) {
|
||||||
|
this.mediaAutoLoadPolicy = policy
|
||||||
|
window.localStorage.setItem(StorageKey.MEDIA_AUTO_LOAD_POLICY, policy)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const instance = new LocalStorageService()
|
const instance = new LocalStorageService()
|
||||||
|
|||||||
7
src/types/index.d.ts
vendored
7
src/types/index.d.ts
vendored
@@ -1,5 +1,5 @@
|
|||||||
import { Event, VerifiedEvent, Filter } from 'nostr-tools'
|
import { Event, Filter, VerifiedEvent } from 'nostr-tools'
|
||||||
import { NOTIFICATION_LIST_STYLE, POLL_TYPE } from '../constants'
|
import { MEDIA_AUTO_LOAD_POLICY, NOTIFICATION_LIST_STYLE, POLL_TYPE } from '../constants'
|
||||||
|
|
||||||
export type TSubRequestFilter = Omit<Filter, 'since' | 'until'> & { limit: number }
|
export type TSubRequestFilter = Omit<Filter, 'since' | 'until'> & { limit: number }
|
||||||
|
|
||||||
@@ -186,3 +186,6 @@ export type TAwesomeRelayCollection = {
|
|||||||
description: string
|
description: string
|
||||||
relays: string[]
|
relays: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TMediaAutoLoadPolicy =
|
||||||
|
(typeof MEDIA_AUTO_LOAD_POLICY)[keyof typeof MEDIA_AUTO_LOAD_POLICY]
|
||||||
|
|||||||
Reference in New Issue
Block a user