feat: add auto-load profile pictures setting (#712)

This commit is contained in:
gzuuus 2025-12-29 15:42:02 +01:00 committed by GitHub
parent ec03a49e32
commit 6dc662bd2b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 170 additions and 47 deletions

View file

@ -1,5 +1,6 @@
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'
import { Skeleton } from '@/components/ui/skeleton'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useFetchProfile } from '@/hooks'
import { toProfile } from '@/lib/link'
import { generateImageByPubkey } from '@/lib/pubkey'
@ -63,6 +64,7 @@ export function SimpleUserAvatar({
onClick?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void
}) {
const { profile } = useFetchProfile(userId)
const { autoLoadProfilePicture } = useContentPolicy()
const defaultAvatar = useMemo(
() => (profile?.pubkey ? generateImageByPubkey(profile.pubkey) : ''),
[profile]
@ -75,9 +77,11 @@ export function SimpleUserAvatar({
}
const { avatar, pubkey } = profile || {}
const imageUrl = autoLoadProfilePicture ? (avatar ?? defaultAvatar) : defaultAvatar
return (
<Image
image={{ url: avatar ?? defaultAvatar, pubkey }}
image={{ url: imageUrl, pubkey }}
errorPlaceholder={defaultAvatar}
className="object-cover object-center"
classNames={{

View file

@ -6,7 +6,10 @@ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElemen
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('rounded-xl border bg-card text-card-foreground transition-all duration-200', className)}
className={cn(
'rounded-xl border bg-card text-card-foreground transition-all duration-200',
className
)}
{...props}
/>
)

View file

@ -34,6 +34,7 @@ export const StorageKey = {
HIDE_CONTENT_MENTIONING_MUTED_USERS: 'hideContentMentioningMutedUsers',
NOTIFICATION_LIST_STYLE: 'notificationListStyle',
MEDIA_AUTO_LOAD_POLICY: 'mediaAutoLoadPolicy',
PROFILE_PICTURE_AUTO_LOAD_POLICY: 'profilePictureAutoLoadPolicy',
SHOWN_CREATE_WALLET_GUIDE_TOAST_PUBKEYS: 'shownCreateWalletGuideToastPubkeys',
SIDEBAR_COLLAPSE: 'sidebarCollapse',
PRIMARY_COLOR: 'primaryColor',
@ -176,6 +177,12 @@ export const MEDIA_AUTO_LOAD_POLICY = {
NEVER: 'never'
} as const
export const PROFILE_PICTURE_AUTO_LOAD_POLICY = {
ALWAYS: 'always',
WIFI_ONLY: 'wifi-only',
NEVER: 'never'
} as const
export const NSFW_DISPLAY_POLICY = {
HIDE: 'hide',
HIDE_CONTENT: 'hide_content',

View file

@ -637,6 +637,7 @@ export default {
Recommended: 'موصى به',
'Enter Password': 'أدخل كلمة المرور',
Password: 'كلمة المرور',
Confirm: 'تأكيد'
Confirm: 'تأكيد',
'Auto-load profile pictures': 'تحميل صور الملف الشخصي تلقائيًا'
}
}

View file

@ -658,6 +658,7 @@ export default {
Recommended: 'Empfohlen',
'Enter Password': 'Passwort eingeben',
Password: 'Passwort',
Confirm: 'Bestätigen'
Confirm: 'Bestätigen',
'Auto-load profile pictures': 'Profilbilder automatisch laden'
}
}

View file

@ -642,6 +642,7 @@ export default {
Recommended: 'Recommended',
'Enter Password': 'Enter Password',
Password: 'Password',
Confirm: 'Confirm'
Confirm: 'Confirm',
'Auto-load profile pictures': 'Auto-load profile pictures'
}
}

View file

@ -652,6 +652,7 @@ export default {
Recommended: 'Recomendado',
'Enter Password': 'Ingresar contraseña',
Password: 'Contraseña',
Confirm: 'Confirmar'
Confirm: 'Confirmar',
'Auto-load profile pictures': 'Cargar imágenes de perfil automáticamente'
}
}

View file

@ -647,6 +647,7 @@ export default {
Recommended: 'توصیه شده',
'Enter Password': 'رمز عبور را وارد کنید',
Password: 'رمز عبور',
Confirm: 'تأیید'
Confirm: 'تأیید',
'Auto-load profile pictures': 'بارگذاری خودکار تصاویر پروفایل'
}
}

View file

@ -655,6 +655,7 @@ export default {
Recommended: 'Recommandé',
'Enter Password': 'Entrer le mot de passe',
Password: 'Mot de passe',
Confirm: 'Confirmer'
Confirm: 'Confirmer',
'Auto-load profile pictures': 'Charger les images de profil automatiquement'
}
}

View file

@ -648,6 +648,7 @@ export default {
Recommended: 'अनुशंसित',
'Enter Password': 'पासवर्ड दर्ज करें',
Password: 'पासवर्ड',
Confirm: 'पुष्टि करें'
Confirm: 'पुष्टि करें',
'Auto-load profile pictures': 'प्रोफ़ाइल चित्र स्वतः लोड करें'
}
}

View file

@ -640,6 +640,7 @@ export default {
Recommended: 'Ajánlott',
'Enter Password': 'Jelszó megadása',
Password: 'Jelszó',
Confirm: 'Megerősítés'
Confirm: 'Megerősítés',
'Auto-load profile pictures': 'Profilképek automatikus betöltése'
}
}

View file

@ -652,6 +652,7 @@ export default {
Recommended: 'Consigliato',
'Enter Password': 'Inserisci password',
Password: 'Password',
Confirm: 'Conferma'
Confirm: 'Conferma',
'Auto-load profile pictures': 'Caricamento automatico immagini di profilo'
}
}

View file

@ -646,6 +646,7 @@ export default {
Recommended: 'おすすめ',
'Enter Password': 'パスワードを入力',
Password: 'パスワード',
Confirm: '確認'
Confirm: '確認',
'Auto-load profile pictures': 'プロフィール画像を自動読み込み'
}
}

View file

@ -643,6 +643,7 @@ export default {
Recommended: '추천',
'Enter Password': '비밀번호 입력',
Password: '비밀번호',
Confirm: '확인'
Confirm: '확인',
'Auto-load profile pictures': '프로필 사진 자동 로드'
}
}

View file

@ -653,6 +653,7 @@ export default {
Recommended: 'Polecane',
'Enter Password': 'Wprowadź hasło',
Password: 'Hasło',
Confirm: 'Potwierdź'
Confirm: 'Potwierdź',
'Auto-load profile pictures': 'Automatyczne ładowanie zdjęć profilowych'
}
}

View file

@ -648,6 +648,7 @@ export default {
Recommended: 'Recomendado',
'Enter Password': 'Digite a senha',
Password: 'Senha',
Confirm: 'Confirmar'
Confirm: 'Confirmar',
'Auto-load profile pictures': 'Carregar fotos de perfil automaticamente'
}
}

View file

@ -651,6 +651,7 @@ export default {
Recommended: 'Recomendado',
'Enter Password': 'Introduza a palavra-passe',
Password: 'Palavra-passe',
Confirm: 'Confirmar'
Confirm: 'Confirmar',
'Auto-load profile pictures': 'Carregar fotos de perfil automaticamente'
}
}

View file

@ -652,6 +652,7 @@ export default {
Recommended: 'Рекомендуемые',
'Enter Password': 'Введите пароль',
Password: 'Пароль',
Confirm: 'Подтвердить'
Confirm: 'Подтвердить',
'Auto-load profile pictures': 'Автозагрузка аватаров'
}
}

View file

@ -637,6 +637,7 @@ export default {
Recommended: 'แนะนำ',
'Enter Password': 'ป้อนรหัสผ่าน',
Password: 'รหัสผ่าน',
Confirm: 'ยืนยัน'
Confirm: 'ยืนยัน',
'Auto-load profile pictures': 'โหลดรูปโปรไฟล์อัตโนมัติ'
}
}

View file

@ -623,6 +623,7 @@ export default {
Recommended: '推薦',
'Enter Password': '輸入密碼',
Password: '密碼',
Confirm: '確認'
Confirm: '確認',
'Auto-load profile pictures': '自動載入大頭照'
}
}

View file

@ -628,6 +628,7 @@ export default {
Recommended: '推荐',
'Enter Password': '输入密码',
Password: '密码',
Confirm: '确认'
Confirm: '确认',
'Auto-load profile pictures': '自动加载头像'
}
}

View file

@ -34,7 +34,9 @@ export function determineExternalContentKind(externalContent: string): string |
// Handle blockchain address format: <blockchain>:[<chainId>:]address:<address>
// Match pattern: blockchain name, optional chain ID, "address:", address
const blockchainAddressMatch = externalContent.match(/^([a-z]+):(?:[^:]+:)?address:[a-zA-Z0-9]+$/i)
const blockchainAddressMatch = externalContent.match(
/^([a-z]+):(?:[^:]+:)?address:[a-zA-Z0-9]+$/i
)
if (blockchainAddressMatch) {
const blockchain = blockchainAddressMatch[1].toLowerCase()
return `${blockchain}:address`

View file

@ -7,7 +7,8 @@ export function parseEditorJsonToText(node?: JSONContent) {
const text = _parseEditorJsonToText(node)
const regex = /(^|\s+|@)(nostr:)?(nevent|naddr|nprofile|npub)1[a-zA-Z0-9]+/g
return text.replace(regex, (match, leadingWhitespace) => {
return text
.replace(regex, (match, leadingWhitespace) => {
let bech32 = match.trim()
const whitespace = leadingWhitespace || ''
@ -25,7 +26,8 @@ export function parseEditorJsonToText(node?: JSONContent) {
} catch {
return match
}
}).trim()
})
.trim()
}
function _parseEditorJsonToText(node?: JSONContent): string {

View file

@ -4,14 +4,18 @@ import { Button } from '@/components/ui/button'
import { Label } from '@/components/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select'
import { Switch } from '@/components/ui/switch'
import { MEDIA_AUTO_LOAD_POLICY, NSFW_DISPLAY_POLICY } from '@/constants'
import {
MEDIA_AUTO_LOAD_POLICY,
NSFW_DISPLAY_POLICY,
PROFILE_PICTURE_AUTO_LOAD_POLICY
} from '@/constants'
import { LocalizedLanguageNames, TLanguage } from '@/i18n'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { cn, isSupportCheckConnectionType } from '@/lib/utils'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
import { useUserTrust } from '@/providers/UserTrustProvider'
import { TMediaAutoLoadPolicy, TNsfwDisplayPolicy } from '@/types'
import { TMediaAutoLoadPolicy, TProfilePictureAutoLoadPolicy, TNsfwDisplayPolicy } from '@/types'
import { SelectValue } from '@radix-ui/react-select'
import { RotateCcw } from 'lucide-react'
import { forwardRef, HTMLProps, useState } from 'react'
@ -28,7 +32,9 @@ const GeneralSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
hideContentMentioningMutedUsers,
setHideContentMentioningMutedUsers,
mediaAutoLoadPolicy,
setMediaAutoLoadPolicy
setMediaAutoLoadPolicy,
profilePictureAutoLoadPolicy,
setProfilePictureAutoLoadPolicy
} = useContentPolicy()
const { hideUntrustedNotes, updateHideUntrustedNotes } = useUserTrust()
const { quickReaction, updateQuickReaction, quickReactionEmoji, updateQuickReactionEmoji } =
@ -82,6 +88,31 @@ const GeneralSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
</SelectContent>
</Select>
</SettingItem>
<SettingItem>
<Label htmlFor="profile-picture-auto-load-policy" className="text-base font-normal">
{t('Auto-load profile pictures')}
</Label>
<Select
defaultValue="always"
value={profilePictureAutoLoadPolicy}
onValueChange={(value: TProfilePictureAutoLoadPolicy) =>
setProfilePictureAutoLoadPolicy(value as TProfilePictureAutoLoadPolicy)
}
>
<SelectTrigger id="profile-picture-auto-load-policy" className="w-48">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value={PROFILE_PICTURE_AUTO_LOAD_POLICY.ALWAYS}>{t('Always')}</SelectItem>
{isSupportCheckConnectionType() && (
<SelectItem value={PROFILE_PICTURE_AUTO_LOAD_POLICY.WIFI_ONLY}>
{t('Wi-Fi only')}
</SelectItem>
)}
<SelectItem value={PROFILE_PICTURE_AUTO_LOAD_POLICY.NEVER}>{t('Never')}</SelectItem>
</SelectContent>
</Select>
</SettingItem>
<SettingItem>
<Label htmlFor="autoplay" className="text-base font-normal">
<div>{t('Autoplay')}</div>

View file

@ -1,6 +1,6 @@
import { MEDIA_AUTO_LOAD_POLICY } from '@/constants'
import { MEDIA_AUTO_LOAD_POLICY, PROFILE_PICTURE_AUTO_LOAD_POLICY } from '@/constants'
import storage from '@/services/local-storage.service'
import { TMediaAutoLoadPolicy, TNsfwDisplayPolicy } from '@/types'
import { TMediaAutoLoadPolicy, TProfilePictureAutoLoadPolicy, TNsfwDisplayPolicy } from '@/types'
import { createContext, useContext, useEffect, useMemo, useState } from 'react'
type TContentPolicyContext = {
@ -17,6 +17,10 @@ type TContentPolicyContext = {
mediaAutoLoadPolicy: TMediaAutoLoadPolicy
setMediaAutoLoadPolicy: (policy: TMediaAutoLoadPolicy) => void
autoLoadProfilePicture: boolean
profilePictureAutoLoadPolicy: TProfilePictureAutoLoadPolicy
setProfilePictureAutoLoadPolicy: (policy: TProfilePictureAutoLoadPolicy) => void
faviconUrlTemplate: string
setFaviconUrlTemplate: (template: string) => void
}
@ -38,6 +42,9 @@ export function ContentPolicyProvider({ children }: { children: React.ReactNode
storage.getHideContentMentioningMutedUsers()
)
const [mediaAutoLoadPolicy, setMediaAutoLoadPolicy] = useState(storage.getMediaAutoLoadPolicy())
const [profilePictureAutoLoadPolicy, setProfilePictureAutoLoadPolicy] = useState(
storage.getProfilePictureAutoLoadPolicy()
)
const [faviconUrlTemplate, setFaviconUrlTemplate] = useState(storage.getFaviconUrlTemplate())
const [connectionType, setConnectionType] = useState((navigator as any).connection?.type)
@ -67,6 +74,17 @@ export function ContentPolicyProvider({ children }: { children: React.ReactNode
return connectionType === 'wifi' || connectionType === 'ethernet'
}, [mediaAutoLoadPolicy, connectionType])
const autoLoadProfilePicture = useMemo(() => {
if (profilePictureAutoLoadPolicy === PROFILE_PICTURE_AUTO_LOAD_POLICY.ALWAYS) {
return true
}
if (profilePictureAutoLoadPolicy === PROFILE_PICTURE_AUTO_LOAD_POLICY.NEVER) {
return false
}
// WIFI_ONLY
return connectionType === 'wifi' || connectionType === 'ethernet'
}, [profilePictureAutoLoadPolicy, connectionType])
const updateAutoplay = (autoplay: boolean) => {
storage.setAutoplay(autoplay)
setAutoplay(autoplay)
@ -87,6 +105,11 @@ export function ContentPolicyProvider({ children }: { children: React.ReactNode
setMediaAutoLoadPolicy(policy)
}
const updateProfilePictureAutoLoadPolicy = (policy: TProfilePictureAutoLoadPolicy) => {
storage.setProfilePictureAutoLoadPolicy(policy)
setProfilePictureAutoLoadPolicy(policy)
}
const updateFaviconUrlTemplate = (template: string) => {
storage.setFaviconUrlTemplate(template)
setFaviconUrlTemplate(template)
@ -104,6 +127,9 @@ export function ContentPolicyProvider({ children }: { children: React.ReactNode
autoLoadMedia,
mediaAutoLoadPolicy,
setMediaAutoLoadPolicy: updateMediaAutoLoadPolicy,
autoLoadProfilePicture,
profilePictureAutoLoadPolicy,
setProfilePictureAutoLoadPolicy: updateProfilePictureAutoLoadPolicy,
faviconUrlTemplate,
setFaviconUrlTemplate: updateFaviconUrlTemplate
}}

View file

@ -6,6 +6,7 @@ import {
MEDIA_AUTO_LOAD_POLICY,
NOTIFICATION_LIST_STYLE,
NSFW_DISPLAY_POLICY,
PROFILE_PICTURE_AUTO_LOAD_POLICY,
StorageKey,
TPrimaryColor
} from '@/constants'
@ -20,6 +21,7 @@ import {
TMediaAutoLoadPolicy,
TMediaUploadServiceConfig,
TNoteListMode,
TProfilePictureAutoLoadPolicy,
TNsfwDisplayPolicy,
TNotificationStyle,
TRelaySet,
@ -53,6 +55,8 @@ class LocalStorageService {
private hideContentMentioningMutedUsers: boolean = false
private notificationListStyle: TNotificationStyle = NOTIFICATION_LIST_STYLE.DETAILED
private mediaAutoLoadPolicy: TMediaAutoLoadPolicy = MEDIA_AUTO_LOAD_POLICY.ALWAYS
private profilePictureAutoLoadPolicy: TProfilePictureAutoLoadPolicy =
PROFILE_PICTURE_AUTO_LOAD_POLICY.ALWAYS
private shownCreateWalletGuideToastPubkeys: Set<string> = new Set()
private sidebarCollapse: boolean = false
private primaryColor: TPrimaryColor = 'DEFAULT'
@ -225,6 +229,19 @@ class LocalStorageService {
this.mediaAutoLoadPolicy = mediaAutoLoadPolicy as TMediaAutoLoadPolicy
}
const profilePictureAutoLoadPolicy = window.localStorage.getItem(
StorageKey.PROFILE_PICTURE_AUTO_LOAD_POLICY
)
if (
profilePictureAutoLoadPolicy &&
Object.values(PROFILE_PICTURE_AUTO_LOAD_POLICY).includes(
profilePictureAutoLoadPolicy as TProfilePictureAutoLoadPolicy
)
) {
this.profilePictureAutoLoadPolicy =
profilePictureAutoLoadPolicy as TProfilePictureAutoLoadPolicy
}
const shownCreateWalletGuideToastPubkeysStr = window.localStorage.getItem(
StorageKey.SHOWN_CREATE_WALLET_GUIDE_TOAST_PUBKEYS
)
@ -518,6 +535,15 @@ class LocalStorageService {
window.localStorage.setItem(StorageKey.MEDIA_AUTO_LOAD_POLICY, policy)
}
getProfilePictureAutoLoadPolicy() {
return this.profilePictureAutoLoadPolicy
}
setProfilePictureAutoLoadPolicy(policy: TProfilePictureAutoLoadPolicy) {
this.profilePictureAutoLoadPolicy = policy
window.localStorage.setItem(StorageKey.PROFILE_PICTURE_AUTO_LOAD_POLICY, policy)
}
hasShownCreateWalletGuideToast(pubkey: string) {
return this.shownCreateWalletGuideToastPubkeys.has(pubkey)
}

View file

@ -3,7 +3,8 @@ import {
MEDIA_AUTO_LOAD_POLICY,
NOTIFICATION_LIST_STYLE,
NSFW_DISPLAY_POLICY,
POLL_TYPE
POLL_TYPE,
PROFILE_PICTURE_AUTO_LOAD_POLICY
} from '../constants'
export type TSubRequestFilter = Omit<Filter, 'since' | 'until'> & { limit: number }
@ -211,4 +212,7 @@ export type TAwesomeRelayCollection = {
export type TMediaAutoLoadPolicy =
(typeof MEDIA_AUTO_LOAD_POLICY)[keyof typeof MEDIA_AUTO_LOAD_POLICY]
export type TProfilePictureAutoLoadPolicy =
(typeof PROFILE_PICTURE_AUTO_LOAD_POLICY)[keyof typeof PROFILE_PICTURE_AUTO_LOAD_POLICY]
export type TNsfwDisplayPolicy = (typeof NSFW_DISPLAY_POLICY)[keyof typeof NSFW_DISPLAY_POLICY]