diff --git a/src/components/Image/index.tsx b/src/components/Image/index.tsx index c8812a5..ce329d0 100644 --- a/src/components/Image/index.tsx +++ b/src/components/Image/index.tsx @@ -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(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 ( -
- {isLoading && } - {!hasError ? ( +
+ {displaySkeleton && ( +
+ {blurHash ? ( + + ) : ( + + )} +
+ )} + {!hasError && ( {alt} { - setIsLoading(false) - setHasError(false) - setTimeout(() => setDisplayBlurHash(false), 500) - }} - onError={handleImageError} + width={dim?.width} + height={dim?.height} + {...props} /> - ) : ( + )} + {hasError && (
)} - {displayBlurHash && blurDataUrl && !hasError && ( - {alt} - )}
) } -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(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 ( + + ) } diff --git a/src/components/ImageGallery/index.tsx b/src/components/ImageGallery/index.tsx index 60c042e..3b85d35 100644 --- a/src/components/ImageGallery/index.tsx +++ b/src/components/ImageGallery/index.tsx @@ -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) => ( + + )) + } + let imageContent: ReactNode | null = null if (displayImages.length === 1) { imageContent = ( ( handlePhotoClick(e, i)} /> @@ -70,7 +87,7 @@ export default function ImageGallery({ {displayImages.map((image, i) => ( handlePhotoClick(e, i)} /> diff --git a/src/components/ImageWithLightbox/index.tsx b/src/components/ImageWithLightbox/index.tsx index f6c0f2d..dd96f2a 100644 --- a/src/components/ImageWithLightbox/index.tsx +++ b/src/components/ImageWithLightbox/index.tsx @@ -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 ( +
{ + e.stopPropagation() + setDisplay(true) + }} + > + [{t('Click to load image')}] +
+ ) + } + const handlePhotoClick = (event: React.MouseEvent) => { event.stopPropagation() event.preventDefault() @@ -34,11 +57,12 @@ export default function ImageWithLightbox({ } return ( -
+
(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 ( +
{ + e.stopPropagation() + setDisplay(true) + }} + > + [{t('Click to load media')}] +
+ ) + } if (!mediaType) { return null diff --git a/src/components/Note/CommunityDefinition.tsx b/src/components/Note/CommunityDefinition.tsx index b82781d..673aef3 100644 --- a/src/components/Note/CommunityDefinition.tsx +++ b/src/components/Note/CommunityDefinition.tsx @@ -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 (
- {metadata.image && ( + {metadata.image && autoLoadMedia && ( )} diff --git a/src/components/Note/GroupMetadata.tsx b/src/components/Note/GroupMetadata.tsx index 32af37c..bff5d40 100644 --- a/src/components/Note/GroupMetadata.tsx +++ b/src/components/Note/GroupMetadata.tsx @@ -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 (
- {metadata.picture && ( + {metadata.picture && autoLoadMedia && ( )} diff --git a/src/components/Note/LiveEvent.tsx b/src/components/Note/LiveEvent.tsx index 740c6c2..ca9fb89 100644 --- a/src/components/Note/LiveEvent.tsx +++ b/src/components/Note/LiveEvent.tsx @@ -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 (
- {metadata.image && ( + {metadata.image && autoLoadMedia && ( )} @@ -60,10 +63,10 @@ export default function LiveEvent({ event, className }: { event: Event; classNam return (
- {metadata.image && ( + {metadata.image && autoLoadMedia && ( )} diff --git a/src/components/Note/LongFormArticle/index.tsx b/src/components/Note/LongFormArticle/index.tsx index e0e65d1..ce40e91 100644 --- a/src/components/Note/LongFormArticle/index.tsx +++ b/src/components/Note/LongFormArticle/index.tsx @@ -63,7 +63,16 @@ export default function LongFormArticle({ }, p: (props) =>

, div: (props) =>

, - code: (props) => + code: (props) => , + img: (props) => ( + + ) }) as Components, [] ) @@ -81,7 +90,7 @@ export default function LongFormArticle({ {metadata.image && ( )} getLongFormArticleMetadataFromEvent(event), [event]) const titleComponent =
{metadata.title}
@@ -43,10 +45,10 @@ export default function LongFormArticlePreview({ if (isSmallScreen) { return (
- {metadata.image && ( + {metadata.image && autoLoadMedia && ( )} @@ -62,7 +64,7 @@ export default function LongFormArticlePreview({ return (
- {metadata.image && ( + {metadata.image && autoLoadMedia && ( {`${pubkey} setBannerUrl(defaultBanner)} /> ) diff --git a/src/components/WebPreview/index.tsx b/src/components/WebPreview/index.tsx index 931b53c..432a4be 100644 --- a/src/components/WebPreview/index.tsx +++ b/src/components/WebPreview/index.tsx @@ -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 && ( )} diff --git a/src/components/YoutubeEmbeddedPlayer/index.tsx b/src/components/YoutubeEmbeddedPlayer/index.tsx index be94076..196016d 100644 --- a/src/components/YoutubeEmbeddedPlayer/index.tsx +++ b/src/components/YoutubeEmbeddedPlayer/index.tsx @@ -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(null) const containerRef = useRef(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 ( +
{ + e.stopPropagation() + setDisplay(true) + }} + > + [{t('Click to load YouTube video')}] +
+ ) + } if (!videoId && !initSuccess) { return ( diff --git a/src/constants.ts b/src/constants.ts index adc62e9..3c0839d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -42,6 +42,7 @@ export const StorageKey = { SHOW_KINDS_VERSION: 'showKindsVersion', HIDE_CONTENT_MENTIONING_MUTED_USERS: 'hideContentMentioningMutedUsers', NOTIFICATION_LIST_STYLE: 'notificationListStyle', + MEDIA_AUTO_LOAD_POLICY: 'mediaAutoLoadPolicy', MEDIA_UPLOAD_SERVICE: 'mediaUploadService', // deprecated HIDE_UNTRUSTED_EVENTS: 'hideUntrustedEvents', // deprecated ACCOUNT_RELAY_LIST_EVENT_MAP: 'accountRelayListEventMap', // deprecated @@ -139,3 +140,9 @@ export const NOTIFICATION_LIST_STYLE = { COMPACT: 'compact', DETAILED: 'detailed' } as const + +export const MEDIA_AUTO_LOAD_POLICY = { + ALWAYS: 'always', + WIFI_ONLY: 'wifi-only', + NEVER: 'never' +} as const diff --git a/src/i18n/locales/ar.ts b/src/i18n/locales/ar.ts index de21081..c0eb4fd 100644 --- a/src/i18n/locales/ar.ts +++ b/src/i18n/locales/ar.ts @@ -400,6 +400,12 @@ export default { 'Submit Relay': 'إرسال ريلاي', Homepage: 'الصفحة الرئيسية', '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': 'انقر لتحميل الوسائط' } } diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 7e9373a..ad41a07 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -410,6 +410,12 @@ export default { 'Submit Relay': 'Relay einreichen', Homepage: 'Homepage', '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' } } diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 9bcfa50..7d860b4 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -399,6 +399,12 @@ export default { 'Submit Relay': 'Submit Relay', Homepage: 'Homepage', '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' } } diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index 25cb945..adc98ac 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -405,6 +405,12 @@ export default { 'Submit Relay': 'Enviar relé', Homepage: 'Página principal', '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' } } diff --git a/src/i18n/locales/fa.ts b/src/i18n/locales/fa.ts index 18e5ce6..6b1ced5 100644 --- a/src/i18n/locales/fa.ts +++ b/src/i18n/locales/fa.ts @@ -401,6 +401,12 @@ export default { 'Submit Relay': 'ارسال رله', Homepage: 'صفحه اصلی', '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': 'برای بارگذاری رسانه کلیک کنید' } } diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index 099e6fd..ab7411a 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -410,6 +410,12 @@ export default { 'Submit Relay': 'Soumettre un relais', Homepage: 'Page d’accueil', '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' } } diff --git a/src/i18n/locales/hi.ts b/src/i18n/locales/hi.ts index 4168ee9..42f2282 100644 --- a/src/i18n/locales/hi.ts +++ b/src/i18n/locales/hi.ts @@ -404,6 +404,12 @@ export default { 'Submit Relay': 'रिले सबमिट करें', Homepage: 'होमपेज', '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': 'मीडिया लोड करने के लिए क्लिक करें' } } diff --git a/src/i18n/locales/it.ts b/src/i18n/locales/it.ts index 19ecf60..5a08935 100644 --- a/src/i18n/locales/it.ts +++ b/src/i18n/locales/it.ts @@ -405,6 +405,12 @@ export default { 'Submit Relay': 'Invia Relay', Homepage: 'Homepage', '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' } } diff --git a/src/i18n/locales/ja.ts b/src/i18n/locales/ja.ts index 5934926..f13388a 100644 --- a/src/i18n/locales/ja.ts +++ b/src/i18n/locales/ja.ts @@ -402,6 +402,12 @@ export default { 'Submit Relay': 'リレーを提出', Homepage: 'ホームページ', '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': 'クリックしてメディアを読み込む' } } diff --git a/src/i18n/locales/ko.ts b/src/i18n/locales/ko.ts index 66db292..1154c51 100644 --- a/src/i18n/locales/ko.ts +++ b/src/i18n/locales/ko.ts @@ -402,6 +402,12 @@ export default { 'Submit Relay': '릴레이 제출', Homepage: '홈페이지', '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': '미디어 로드하려면 클릭' } } diff --git a/src/i18n/locales/pl.ts b/src/i18n/locales/pl.ts index bc4cd97..2bee486 100644 --- a/src/i18n/locales/pl.ts +++ b/src/i18n/locales/pl.ts @@ -406,6 +406,12 @@ export default { 'Submit Relay': 'Prześlij przekaźnik', Homepage: 'Strona główna', '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' } } diff --git a/src/i18n/locales/pt-BR.ts b/src/i18n/locales/pt-BR.ts index 37a0d9e..63f6138 100644 --- a/src/i18n/locales/pt-BR.ts +++ b/src/i18n/locales/pt-BR.ts @@ -402,6 +402,12 @@ export default { 'Submit Relay': 'Enviar Relay', Homepage: 'Página inicial', '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' } } diff --git a/src/i18n/locales/pt-PT.ts b/src/i18n/locales/pt-PT.ts index 157fe17..8312390 100644 --- a/src/i18n/locales/pt-PT.ts +++ b/src/i18n/locales/pt-PT.ts @@ -405,6 +405,12 @@ export default { 'Submit Relay': 'Enviar Relay', Homepage: 'Página inicial', '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' } } diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts index 5b9981b..2373534 100644 --- a/src/i18n/locales/ru.ts +++ b/src/i18n/locales/ru.ts @@ -407,6 +407,12 @@ export default { 'Submit Relay': 'Отправить релей', Homepage: 'Домашняя страница', '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': 'Нажмите, чтобы загрузить медиа' } } diff --git a/src/i18n/locales/th.ts b/src/i18n/locales/th.ts index a91bfc1..cd0176e 100644 --- a/src/i18n/locales/th.ts +++ b/src/i18n/locales/th.ts @@ -398,6 +398,12 @@ export default { 'Submit Relay': 'ส่งรีเลย์', Homepage: 'หน้าแรก', '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': 'คลิกเพื่อโหลดสื่อ' } } diff --git a/src/i18n/locales/zh.ts b/src/i18n/locales/zh.ts index 92b6256..a854314 100644 --- a/src/i18n/locales/zh.ts +++ b/src/i18n/locales/zh.ts @@ -396,6 +396,12 @@ export default { 'Submit Relay': '提交服务器', Homepage: '主页', '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': '点击加载音视频' } } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 5acc2dd..4b0ecd7 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -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) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) } diff --git a/src/pages/secondary/GeneralSettingsPage/index.tsx b/src/pages/secondary/GeneralSettingsPage/index.tsx index 8803680..33cdf14 100644 --- a/src/pages/secondary/GeneralSettingsPage/index.tsx +++ b/src/pages/secondary/GeneralSettingsPage/index.tsx @@ -1,14 +1,15 @@ import { Label } from '@/components/ui/label' import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select' 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 SecondaryPageLayout from '@/layouts/SecondaryPageLayout' -import { cn } from '@/lib/utils' +import { cn, isSupportCheckConnectionType } from '@/lib/utils' import { useContentPolicy } from '@/providers/ContentPolicyProvider' import { useTheme } from '@/providers/ThemeProvider' import { useUserPreferences } from '@/providers/UserPreferencesProvider' import { useUserTrust } from '@/providers/UserTrustProvider' +import { TMediaAutoLoadPolicy } from '@/types' import { SelectValue } from '@radix-ui/react-select' import { ExternalLink } from 'lucide-react' import { forwardRef, HTMLProps, useState } from 'react' @@ -24,7 +25,9 @@ const GeneralSettingsPage = forwardRef(({ index }: { index?: number }, ref) => { defaultShowNsfw, setDefaultShowNsfw, hideContentMentioningMutedUsers, - setHideContentMentioningMutedUsers + setHideContentMentioningMutedUsers, + mediaAutoLoadPolicy, + setMediaAutoLoadPolicy } = useContentPolicy() const { hideUntrustedNotes, updateHideUntrustedNotes } = useUserTrust() const { notificationListStyle, updateNotificationListStyle } = useUserPreferences() @@ -92,6 +95,29 @@ const GeneralSettingsPage = forwardRef(({ index }: { index?: number }, ref) => { + + + +