fix: HTTP avatar image rendering issue

This commit is contained in:
codytseng 2026-03-28 15:18:50 +08:00
parent e4a61740c5
commit 5596e5eb7b
9 changed files with 40 additions and 33 deletions

View file

@ -2,7 +2,7 @@ import { Button } from '@/components/ui/button'
import { Slider } from '@/components/ui/slider' import { Slider } from '@/components/ui/slider'
import { isInsecureUrl } from '@/lib/url' import { isInsecureUrl } from '@/lib/url'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import storage from '@/services/local-storage.service' import { useUserPreferences } from '@/providers/UserPreferencesProvider'
import mediaManager from '@/services/media-manager.service' import mediaManager from '@/services/media-manager.service'
import { Minimize2, Pause, Play, X } from 'lucide-react' import { Minimize2, Pause, Play, X } from 'lucide-react'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
@ -24,6 +24,7 @@ export default function AudioPlayer({
className className
}: AudioPlayerProps) { }: AudioPlayerProps) {
const audioRef = useRef<HTMLAudioElement>(null) const audioRef = useRef<HTMLAudioElement>(null)
const { allowInsecureConnection } = useUserPreferences()
const [isPlaying, setIsPlaying] = useState(false) const [isPlaying, setIsPlaying] = useState(false)
const [currentTime, setCurrentTime] = useState(0) const [currentTime, setCurrentTime] = useState(0)
const [duration, setDuration] = useState(0) const [duration, setDuration] = useState(0)
@ -123,7 +124,7 @@ export default function AudioPlayer({
}, 300) }, 300)
} }
if (error || (!storage.getAllowInsecureConnection() && isInsecureUrl(src))) { if (error || (!allowInsecureConnection && isInsecureUrl(src))) {
return <ExternalLink url={src} /> return <ExternalLink url={src} />
} }

View file

@ -1,6 +1,6 @@
import { isInsecureUrl } from '@/lib/url' import { isInsecureUrl } from '@/lib/url'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import storage from '@/services/local-storage.service' import { useUserPreferences } from '@/providers/UserPreferencesProvider'
import { TEmoji } from '@/types' import { TEmoji } from '@/types'
import { Heart } from 'lucide-react' import { Heart } from 'lucide-react'
import { HTMLAttributes, useState } from 'react' import { HTMLAttributes, useState } from 'react'
@ -15,6 +15,7 @@ export default function Emoji({
img?: string img?: string
} }
}) { }) {
const { allowInsecureConnection } = useUserPreferences()
const [hasError, setHasError] = useState(false) const [hasError, setHasError] = useState(false)
if (typeof emoji === 'string') { if (typeof emoji === 'string') {
@ -25,7 +26,7 @@ export default function Emoji({
) )
} }
if (hasError || (!storage.getAllowInsecureConnection() && isInsecureUrl(emoji.url))) { if (hasError || (!allowInsecureConnection && isInsecureUrl(emoji.url))) {
return ( return (
<span className={cn('whitespace-nowrap', classNames?.text)}>{`:${emoji.shortcode}:`}</span> <span className={cn('whitespace-nowrap', classNames?.text)}>{`:${emoji.shortcode}:`}</span>
) )

View file

@ -1,14 +1,13 @@
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { isInsecureUrl } from '@/lib/url' import { isInsecureUrl } from '@/lib/url'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
import blossomService from '@/services/blossom.service' import blossomService from '@/services/blossom.service'
import storage from '@/services/local-storage.service'
import { TImetaInfo } from '@/types' import { TImetaInfo } from '@/types'
import { decode } from 'blurhash' import { decode } from 'blurhash'
import { ImageOff } from 'lucide-react' import { ImageOff } from 'lucide-react'
import { HTMLAttributes, useEffect, useMemo, useRef, useState } from 'react' import { HTMLAttributes, useEffect, useMemo, useRef, useState } from 'react'
import { thumbHashToDataURL } from 'thumbhash' import { thumbHashToDataURL } from 'thumbhash'
import ExternalLink from '../ExternalLink'
export default function Image({ export default function Image({
image: { url, blurHash, thumbHash, pubkey, dim }, image: { url, blurHash, thumbHash, pubkey, dim },
@ -29,21 +28,20 @@ export default function Image({
hideIfError?: boolean hideIfError?: boolean
errorPlaceholder?: React.ReactNode errorPlaceholder?: React.ReactNode
}) { }) {
const { allowInsecureConnection } = useUserPreferences()
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [displaySkeleton, setDisplaySkeleton] = useState(true) const [displaySkeleton, setDisplaySkeleton] = useState(true)
const [hasError, setHasError] = useState(false) const [hasError, setHasError] = useState(false)
const [isBlocked, setIsBlocked] = useState(false)
const [imageUrl, setImageUrl] = useState<string>() const [imageUrl, setImageUrl] = useState<string>()
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null) const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
useEffect(() => { useEffect(() => {
setIsLoading(true) setIsLoading(true)
setHasError(false) setHasError(false)
setIsBlocked(false)
setDisplaySkeleton(true) setDisplaySkeleton(true)
if (!storage.getAllowInsecureConnection() && isInsecureUrl(url)) { if (!allowInsecureConnection && isInsecureUrl(url)) {
setIsBlocked(true) setHasError(true)
setIsLoading(false) setIsLoading(false)
return return
} }
@ -62,11 +60,7 @@ export default function Image({
} else { } else {
setImageUrl(url) setImageUrl(url)
} }
}, [url]) }, [url, allowInsecureConnection])
if (isBlocked) {
return <ExternalLink url={url} />
}
if (hideIfError && hasError) return null if (hideIfError && hasError) return null

View file

@ -1,11 +1,11 @@
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card' import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useFetchProfile } from '@/hooks' import { useFetchProfile } from '@/hooks'
import { toProfile } from '@/lib/link' import { toProfile } from '@/lib/link'
import { generateImageByPubkey } from '@/lib/pubkey' import { generateImageByPubkey } from '@/lib/pubkey'
import { cn, isTouchDevice } from '@/lib/utils' import { cn, isTouchDevice } from '@/lib/utils'
import { SecondaryPageLink } from '@/PageManager' import { SecondaryPageLink } from '@/PageManager'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useMemo } from 'react' import { useMemo } from 'react'
import Image from '../Image' import Image from '../Image'
import ProfileCard from '../ProfileCard' import ProfileCard from '../ProfileCard'

View file

@ -2,14 +2,13 @@ import { isInsecureUrl } from '@/lib/url'
import { cn, isInViewport } from '@/lib/utils' import { cn, isInViewport } from '@/lib/utils'
import { useContentPolicy } from '@/providers/ContentPolicyProvider' import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useUserPreferences } from '@/providers/UserPreferencesProvider' import { useUserPreferences } from '@/providers/UserPreferencesProvider'
import storage from '@/services/local-storage.service'
import mediaManager from '@/services/media-manager.service' import mediaManager from '@/services/media-manager.service'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import ExternalLink from '../ExternalLink' import ExternalLink from '../ExternalLink'
export default function VideoPlayer({ src, className }: { src: string; className?: string }) { export default function VideoPlayer({ src, className }: { src: string; className?: string }) {
const { autoplay, videoLoop } = useContentPolicy() const { autoplay, videoLoop } = useContentPolicy()
const { muteMedia, updateMuteMedia } = useUserPreferences() const { muteMedia, updateMuteMedia, allowInsecureConnection } = useUserPreferences()
const [error, setError] = useState(false) const [error, setError] = useState(false)
const videoRef = useRef<HTMLVideoElement>(null) const videoRef = useRef<HTMLVideoElement>(null)
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
@ -71,7 +70,7 @@ export default function VideoPlayer({ src, className }: { src: string; className
} }
}, [muteMedia]) }, [muteMedia])
if (error || (!storage.getAllowInsecureConnection() && isInsecureUrl(src))) { if (error || (!allowInsecureConnection && isInsecureUrl(src))) {
return <ExternalLink url={src} /> return <ExternalLink url={src} />
} }

View file

@ -3,10 +3,10 @@ import { isInsecureUrl } from '@/lib/url'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useContentPolicy } from '@/providers/ContentPolicyProvider' import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider'
import storage from '@/services/local-storage.service' import { useUserPreferences } from '@/providers/UserPreferencesProvider'
import { useMemo } from 'react' import { useMemo } from 'react'
import Image from '../Image'
import ExternalLink from '../ExternalLink' import ExternalLink from '../ExternalLink'
import Image from '../Image'
export default function WebPreview({ export default function WebPreview({
url, url,
@ -18,6 +18,7 @@ export default function WebPreview({
mustLoad?: boolean mustLoad?: boolean
}) { }) {
const { autoLoadMedia } = useContentPolicy() const { autoLoadMedia } = useContentPolicy()
const { allowInsecureConnection } = useUserPreferences()
const { isSmallScreen } = useScreenSize() const { isSmallScreen } = useScreenSize()
const { title, description, image } = useFetchWebMetadata(url) const { title, description, image } = useFetchWebMetadata(url)
@ -29,7 +30,7 @@ export default function WebPreview({
} }
}, [url]) }, [url])
if (!storage.getAllowInsecureConnection() && isInsecureUrl(url)) { if (!allowInsecureConnection && isInsecureUrl(url)) {
return null return null
} }

View file

@ -1,10 +1,11 @@
import { isInsecureUrl } from '@/lib/url' import { isInsecureUrl } from '@/lib/url'
import storage from '@/services/local-storage.service' import { useUserPreferences } from '@/providers/UserPreferencesProvider'
import webService from '@/services/web.service' import webService from '@/services/web.service'
import { TWebMetadata } from '@/types' import { TWebMetadata } from '@/types'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
export function useFetchWebMetadata(url: string) { export function useFetchWebMetadata(url: string) {
const { allowInsecureConnection } = useUserPreferences()
const [metadata, setMetadata] = useState<TWebMetadata>({}) const [metadata, setMetadata] = useState<TWebMetadata>({})
const proxyServer = import.meta.env.VITE_PROXY_SERVER const proxyServer = import.meta.env.VITE_PROXY_SERVER
if (proxyServer) { if (proxyServer) {
@ -12,10 +13,10 @@ export function useFetchWebMetadata(url: string) {
} }
useEffect(() => { useEffect(() => {
if (!storage.getAllowInsecureConnection() && isInsecureUrl(url)) return if (!allowInsecureConnection && isInsecureUrl(url)) return
webService.fetchWebMetadata(url).then((metadata) => setMetadata(metadata)) webService.fetchWebMetadata(url).then((metadata) => setMetadata(metadata))
}, [url]) }, [url, allowInsecureConnection])
return metadata return metadata
} }

View file

@ -6,6 +6,7 @@ import { Switch } from '@/components/ui/switch'
import { DEFAULT_FAVICON_URL_TEMPLATE } from '@/constants' import { DEFAULT_FAVICON_URL_TEMPLATE } from '@/constants'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { useContentPolicy } from '@/providers/ContentPolicyProvider' import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
import storage from '@/services/local-storage.service' import storage from '@/services/local-storage.service'
import { forwardRef, useState } from 'react' import { forwardRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -13,12 +14,10 @@ import { useTranslation } from 'react-i18next'
const SystemSettingsPage = forwardRef(({ index }: { index?: number }, ref) => { const SystemSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
const { t } = useTranslation() const { t } = useTranslation()
const { faviconUrlTemplate, setFaviconUrlTemplate } = useContentPolicy() const { faviconUrlTemplate, setFaviconUrlTemplate } = useContentPolicy()
const { allowInsecureConnection, updateAllowInsecureConnection } = useUserPreferences()
const [filterOutOnionRelays, setFilterOutOnionRelays] = useState( const [filterOutOnionRelays, setFilterOutOnionRelays] = useState(
storage.getFilterOutOnionRelays() storage.getFilterOutOnionRelays()
) )
const [allowInsecureConnection, setAllowInsecureConnection] = useState(
storage.getAllowInsecureConnection()
)
return ( return (
<SecondaryPageLayout ref={ref} index={index} title={t('System')}> <SecondaryPageLayout ref={ref} index={index} title={t('System')}>
@ -58,10 +57,7 @@ const SystemSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
<Switch <Switch
id="allow-insecure-connection" id="allow-insecure-connection"
checked={allowInsecureConnection} checked={allowInsecureConnection}
onCheckedChange={(checked) => { onCheckedChange={updateAllowInsecureConnection}
storage.setAllowInsecureConnection(checked)
setAllowInsecureConnection(checked)
}}
/> />
</div> </div>
<div className="space-y-2 px-4"> <div className="space-y-2 px-4">

View file

@ -21,6 +21,9 @@ type TUserPreferencesContext = {
quickReactionEmoji: string | TEmoji quickReactionEmoji: string | TEmoji
updateQuickReactionEmoji: (emoji: string | TEmoji) => void updateQuickReactionEmoji: (emoji: string | TEmoji) => void
allowInsecureConnection: boolean
updateAllowInsecureConnection: (allow: boolean) => void
} }
const UserPreferencesContext = createContext<TUserPreferencesContext | undefined>(undefined) const UserPreferencesContext = createContext<TUserPreferencesContext | undefined>(undefined)
@ -46,6 +49,10 @@ export function UserPreferencesProvider({ children }: { children: React.ReactNod
const [quickReaction, setQuickReaction] = useState(storage.getQuickReaction()) const [quickReaction, setQuickReaction] = useState(storage.getQuickReaction())
const [quickReactionEmoji, setQuickReactionEmoji] = useState(storage.getQuickReactionEmoji()) const [quickReactionEmoji, setQuickReactionEmoji] = useState(storage.getQuickReactionEmoji())
const [allowInsecureConnection, setAllowInsecureConnection] = useState(
storage.getAllowInsecureConnection()
)
useEffect(() => { useEffect(() => {
if (!isSmallScreen && enableSingleColumnLayout) { if (!isSmallScreen && enableSingleColumnLayout) {
document.documentElement.style.setProperty('overflow-y', 'scroll') document.documentElement.style.setProperty('overflow-y', 'scroll')
@ -79,6 +86,11 @@ export function UserPreferencesProvider({ children }: { children: React.ReactNod
storage.setQuickReactionEmoji(emoji) storage.setQuickReactionEmoji(emoji)
} }
const updateAllowInsecureConnection = (allow: boolean) => {
setAllowInsecureConnection(allow)
storage.setAllowInsecureConnection(allow)
}
return ( return (
<UserPreferencesContext.Provider <UserPreferencesContext.Provider
value={{ value={{
@ -93,7 +105,9 @@ export function UserPreferencesProvider({ children }: { children: React.ReactNod
quickReaction, quickReaction,
updateQuickReaction, updateQuickReaction,
quickReactionEmoji, quickReactionEmoji,
updateQuickReactionEmoji updateQuickReactionEmoji,
allowInsecureConnection,
updateAllowInsecureConnection
}} }}
> >
{children} {children}