feat: add NSFW display policy setting

This commit is contained in:
codytseng
2025-12-11 23:37:05 +08:00
parent c2b0e6f666
commit f6f974adc6
24 changed files with 185 additions and 53 deletions

View File

@@ -1,5 +1,5 @@
import { useSecondaryPage } from '@/PageManager' import { useSecondaryPage } from '@/PageManager'
import { ExtendedKind, SUPPORTED_KINDS } from '@/constants' import { ExtendedKind, NSFW_DISPLAY_POLICY, SUPPORTED_KINDS } from '@/constants'
import { getParentStuff, isNsfwEvent } from '@/lib/event' import { getParentStuff, isNsfwEvent } from '@/lib/event'
import { toExternalContent, toNote } from '@/lib/link' import { toExternalContent, toNote } from '@/lib/link'
import { useContentPolicy } from '@/providers/ContentPolicyProvider' import { useContentPolicy } from '@/providers/ContentPolicyProvider'
@@ -55,10 +55,14 @@ export default function Note({
const { parentEventId, parentExternalContent } = useMemo(() => { const { parentEventId, parentExternalContent } = useMemo(() => {
return getParentStuff(event) return getParentStuff(event)
}, [event]) }, [event])
const { defaultShowNsfw } = useContentPolicy() const { nsfwDisplayPolicy } = useContentPolicy()
const [showNsfw, setShowNsfw] = useState(false) const [showNsfw, setShowNsfw] = useState(false)
const { mutePubkeySet } = useMuteList() const { mutePubkeySet } = useMuteList()
const [showMuted, setShowMuted] = useState(false) const [showMuted, setShowMuted] = useState(false)
const isNsfw = useMemo(
() => (nsfwDisplayPolicy === NSFW_DISPLAY_POLICY.SHOW ? false : isNsfwEvent(event)),
[event, nsfwDisplayPolicy]
)
let content: React.ReactNode let content: React.ReactNode
if ( if (
@@ -72,7 +76,7 @@ export default function Note({
content = <UnknownNote className="mt-2" event={event} /> content = <UnknownNote className="mt-2" event={event} />
} else if (mutePubkeySet.has(event.pubkey) && !showMuted) { } else if (mutePubkeySet.has(event.pubkey) && !showMuted) {
content = <MutedNote show={() => setShowMuted(true)} /> content = <MutedNote show={() => setShowMuted(true)} />
} else if (!defaultShowNsfw && isNsfwEvent(event) && !showNsfw) { } else if (isNsfw && !showNsfw) {
content = <NsfwNote show={() => setShowNsfw(true)} /> content = <NsfwNote show={() => setShowNsfw(true)} />
} else if (event.kind === kinds.Highlights) { } else if (event.kind === kinds.Highlights) {
content = <Highlight className="mt-2" event={event} /> content = <Highlight className="mt-2" event={event} />

View File

@@ -1,5 +1,6 @@
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { isMentioningMutedUsers } from '@/lib/event' import { NSFW_DISPLAY_POLICY } from '@/constants'
import { isMentioningMutedUsers, isNsfwEvent } from '@/lib/event'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useContentPolicy } from '@/providers/ContentPolicyProvider' import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useMuteList } from '@/providers/MuteListProvider' import { useMuteList } from '@/providers/MuteListProvider'
@@ -22,7 +23,7 @@ export default function NoteCard({
reposters?: string[] reposters?: string[]
}) { }) {
const { mutePubkeySet } = useMuteList() const { mutePubkeySet } = useMuteList()
const { hideContentMentioningMutedUsers } = useContentPolicy() const { hideContentMentioningMutedUsers, nsfwDisplayPolicy } = useContentPolicy()
const shouldHide = useMemo(() => { const shouldHide = useMemo(() => {
if (filterMutedNotes && mutePubkeySet.has(event.pubkey)) { if (filterMutedNotes && mutePubkeySet.has(event.pubkey)) {
return true return true
@@ -30,8 +31,11 @@ export default function NoteCard({
if (hideContentMentioningMutedUsers && isMentioningMutedUsers(event, mutePubkeySet)) { if (hideContentMentioningMutedUsers && isMentioningMutedUsers(event, mutePubkeySet)) {
return true return true
} }
if (nsfwDisplayPolicy === NSFW_DISPLAY_POLICY.HIDE && isNsfwEvent(event)) {
return true
}
return false return false
}, [event, filterMutedNotes, mutePubkeySet]) }, [event, filterMutedNotes, mutePubkeySet, nsfwDisplayPolicy])
if (shouldHide) return null if (shouldHide) return null
if (event.kind === kinds.Repost || event.kind === kinds.GenericRepost) { if (event.kind === kinds.Repost || event.kind === kinds.GenericRepost) {

View File

@@ -28,7 +28,6 @@ export const StorageKey = {
TRANSLATION_SERVICE_CONFIG_MAP: 'translationServiceConfigMap', TRANSLATION_SERVICE_CONFIG_MAP: 'translationServiceConfigMap',
MEDIA_UPLOAD_SERVICE_CONFIG_MAP: 'mediaUploadServiceConfigMap', MEDIA_UPLOAD_SERVICE_CONFIG_MAP: 'mediaUploadServiceConfigMap',
HIDE_UNTRUSTED_NOTES: 'hideUntrustedNotes', HIDE_UNTRUSTED_NOTES: 'hideUntrustedNotes',
DEFAULT_SHOW_NSFW: 'defaultShowNsfw',
DISMISSED_TOO_MANY_RELAYS_ALERT: 'dismissedTooManyRelaysAlert', DISMISSED_TOO_MANY_RELAYS_ALERT: 'dismissedTooManyRelaysAlert',
SHOW_KINDS: 'showKinds', SHOW_KINDS: 'showKinds',
SHOW_KINDS_VERSION: 'showKindsVersion', SHOW_KINDS_VERSION: 'showKindsVersion',
@@ -43,6 +42,8 @@ export const StorageKey = {
FILTER_OUT_ONION_RELAYS: 'filterOutOnionRelays', FILTER_OUT_ONION_RELAYS: 'filterOutOnionRelays',
QUICK_REACTION: 'quickReaction', QUICK_REACTION: 'quickReaction',
QUICK_REACTION_EMOJI: 'quickReactionEmoji', QUICK_REACTION_EMOJI: 'quickReactionEmoji',
NSFW_DISPLAY_POLICY: 'nsfwDisplayPolicy',
DEFAULT_SHOW_NSFW: 'defaultShowNsfw', // deprecated
PINNED_PUBKEYS: 'pinnedPubkeys', // deprecated PINNED_PUBKEYS: 'pinnedPubkeys', // deprecated
MEDIA_UPLOAD_SERVICE: 'mediaUploadService', // deprecated MEDIA_UPLOAD_SERVICE: 'mediaUploadService', // deprecated
HIDE_UNTRUSTED_EVENTS: 'hideUntrustedEvents', // deprecated HIDE_UNTRUSTED_EVENTS: 'hideUntrustedEvents', // deprecated
@@ -170,6 +171,12 @@ export const MEDIA_AUTO_LOAD_POLICY = {
NEVER: 'never' NEVER: 'never'
} as const } as const
export const NSFW_DISPLAY_POLICY = {
HIDE: 'hide',
HIDE_CONTENT: 'hide_content',
SHOW: 'show'
} as const
export const MAX_PINNED_NOTES = 10 export const MAX_PINNED_NOTES = 10
export const PRIMARY_COLORS = { export const PRIMARY_COLORS = {

View File

@@ -571,6 +571,11 @@ export default {
'If enabled, you can react with a single click. Click and hold for more options': 'If enabled, you can react with a single click. Click and hold for more options':
'إذا تم التمكين، يمكنك التفاعل بنقرة واحدة. اضغط مع الاستمرار للمزيد من الخيارات', 'إذا تم التمكين، يمكنك التفاعل بنقرة واحدة. اضغط مع الاستمرار للمزيد من الخيارات',
'Quick reaction emoji': 'رمز تعبيري للرد السريع', 'Quick reaction emoji': 'رمز تعبيري للرد السريع',
'Select emoji': 'اختر رمز تعبيري' 'Select emoji': 'اختر رمز تعبيري',
'NSFW content display': 'NSFW content display',
'Hide completely': 'Hide completely',
'Show but hide content': 'Show but hide content',
'Show directly': 'Show directly',
'Click to view': 'Click to view'
} }
} }

View File

@@ -587,6 +587,11 @@ export default {
'If enabled, you can react with a single click. Click and hold for more options': 'If enabled, you can react with a single click. Click and hold for more options':
'Wenn aktiviert, können Sie mit einem Klick reagieren. Klicken und halten Sie für weitere Optionen', 'Wenn aktiviert, können Sie mit einem Klick reagieren. Klicken und halten Sie für weitere Optionen',
'Quick reaction emoji': 'Schnellreaktions-Emoji', 'Quick reaction emoji': 'Schnellreaktions-Emoji',
'Select emoji': 'Emoji auswählen' 'Select emoji': 'Emoji auswählen',
'NSFW content display': 'NSFW content display',
'Hide completely': 'Hide completely',
'Show but hide content': 'Show but hide content',
'Show directly': 'Show directly',
'Click to view': 'Click to view'
} }
} }

View File

@@ -574,6 +574,11 @@ export default {
'If enabled, you can react with a single click. Click and hold for more options': 'If enabled, you can react with a single click. Click and hold for more options':
'If enabled, you can react with a single click. Click and hold for more options', 'If enabled, you can react with a single click. Click and hold for more options',
'Quick reaction emoji': 'Quick reaction emoji', 'Quick reaction emoji': 'Quick reaction emoji',
'Select emoji': 'Select emoji' 'Select emoji': 'Select emoji',
'NSFW content display': 'NSFW content display',
'Hide completely': 'Hide completely',
'Show but hide content': 'Show but hide content',
'Show directly': 'Show directly',
'Click to view': 'Click to view'
} }
} }

View File

@@ -583,6 +583,11 @@ export default {
'If enabled, you can react with a single click. Click and hold for more options': 'If enabled, you can react with a single click. Click and hold for more options':
'Si está habilitado, puedes reaccionar con un solo clic. Mantén presionado para más opciones', 'Si está habilitado, puedes reaccionar con un solo clic. Mantén presionado para más opciones',
'Quick reaction emoji': 'Emoji de reacción rápida', 'Quick reaction emoji': 'Emoji de reacción rápida',
'Select emoji': 'Seleccionar emoji' 'Select emoji': 'Seleccionar emoji',
'NSFW content display': 'NSFW content display',
'Hide completely': 'Hide completely',
'Show but hide content': 'Show but hide content',
'Show directly': 'Show directly',
'Click to view': 'Click to view'
} }
} }

View File

@@ -577,6 +577,11 @@ export default {
'If enabled, you can react with a single click. Click and hold for more options': 'If enabled, you can react with a single click. Click and hold for more options':
'اگر فعال باشد، می‌توانید با یک کلیک واکنش نشان دهید. برای گزینه‌های بیشتر کلیک کنید و نگه دارید', 'اگر فعال باشد، می‌توانید با یک کلیک واکنش نشان دهید. برای گزینه‌های بیشتر کلیک کنید و نگه دارید',
'Quick reaction emoji': 'ایموجی واکنش سریع', 'Quick reaction emoji': 'ایموجی واکنش سریع',
'Select emoji': 'انتخاب ایموجی' 'Select emoji': 'انتخاب ایموجی',
'NSFW content display': 'NSFW content display',
'Hide completely': 'Hide completely',
'Show but hide content': 'Show but hide content',
'Show directly': 'Show directly',
'Click to view': 'Click to view'
} }
} }

View File

@@ -584,8 +584,13 @@ export default {
notes: 'notes', notes: 'notes',
'Quick reaction': 'Réaction rapide', 'Quick reaction': 'Réaction rapide',
'If enabled, you can react with a single click. Click and hold for more options': 'If enabled, you can react with a single click. Click and hold for more options':
'Si activé, vous pouvez réagir en un seul clic. Maintenez enfoncé pour plus d\'options', "Si activé, vous pouvez réagir en un seul clic. Maintenez enfoncé pour plus d'options",
'Quick reaction emoji': 'Emoji de réaction rapide', 'Quick reaction emoji': 'Emoji de réaction rapide',
'Select emoji': 'Sélectionner un emoji' 'Select emoji': 'Sélectionner un emoji',
'NSFW content display': 'NSFW content display',
'Hide completely': 'Hide completely',
'Show but hide content': 'Show but hide content',
'Show directly': 'Show directly',
'Click to view': 'Click to view'
} }
} }

View File

@@ -578,6 +578,11 @@ export default {
'If enabled, you can react with a single click. Click and hold for more options': 'If enabled, you can react with a single click. Click and hold for more options':
'यदि सक्षम है, तो आप एक क्लिक से प्रतिक्रिया दे सकते हैं। अधिक विकल्पों के लिए क्लिक करें और रोकें', 'यदि सक्षम है, तो आप एक क्लिक से प्रतिक्रिया दे सकते हैं। अधिक विकल्पों के लिए क्लिक करें और रोकें',
'Quick reaction emoji': 'त्वरित प्रतिक्रिया इमोजी', 'Quick reaction emoji': 'त्वरित प्रतिक्रिया इमोजी',
'Select emoji': 'इमोजी चुनें' 'Select emoji': 'इमोजी चुनें',
'NSFW content display': 'NSFW content display',
'Hide completely': 'Hide completely',
'Show but hide content': 'Show but hide content',
'Show directly': 'Show directly',
'Click to view': 'Click to view'
} }
} }

View File

@@ -572,6 +572,11 @@ export default {
'If enabled, you can react with a single click. Click and hold for more options': 'If enabled, you can react with a single click. Click and hold for more options':
'Ha engedélyezve van, egy kattintással reagálhat. Tartsa lenyomva további lehetőségekért', 'Ha engedélyezve van, egy kattintással reagálhat. Tartsa lenyomva további lehetőségekért',
'Quick reaction emoji': 'Gyors reakció emoji', 'Quick reaction emoji': 'Gyors reakció emoji',
'Select emoji': 'Emoji kiválasztása' 'Select emoji': 'Emoji kiválasztása',
'NSFW content display': 'NSFW content display',
'Hide completely': 'Hide completely',
'Show but hide content': 'Show but hide content',
'Show directly': 'Show directly',
'Click to view': 'Click to view'
} }
} }

View File

@@ -582,6 +582,11 @@ export default {
'If enabled, you can react with a single click. Click and hold for more options': 'If enabled, you can react with a single click. Click and hold for more options':
'Se abilitato, puoi reagire con un solo clic. Fai clic e tieni premuto per altre opzioni', 'Se abilitato, puoi reagire con un solo clic. Fai clic e tieni premuto per altre opzioni',
'Quick reaction emoji': 'Emoji reazione rapida', 'Quick reaction emoji': 'Emoji reazione rapida',
'Select emoji': 'Seleziona emoji' 'Select emoji': 'Seleziona emoji',
'NSFW content display': 'NSFW content display',
'Hide completely': 'Hide completely',
'Show but hide content': 'Show but hide content',
'Show directly': 'Show directly',
'Click to view': 'Click to view'
} }
} }

View File

@@ -577,6 +577,11 @@ export default {
'If enabled, you can react with a single click. Click and hold for more options': 'If enabled, you can react with a single click. Click and hold for more options':
'有効にすると、ワンクリックでリアクションできます。長押しで他のオプションを表示', '有効にすると、ワンクリックでリアクションできます。長押しで他のオプションを表示',
'Quick reaction emoji': 'クイックリアクション絵文字', 'Quick reaction emoji': 'クイックリアクション絵文字',
'Select emoji': '絵文字を選択' 'Select emoji': '絵文字を選択',
'NSFW content display': 'NSFWコンテンツの表示',
'Hide completely': '完全に非表示',
'Show but hide content': '表示するがコンテンツを非表示',
'Show directly': '直接表示',
'Click to view': 'クリックして表示'
} }
} }

View File

@@ -576,6 +576,11 @@ export default {
'If enabled, you can react with a single click. Click and hold for more options': 'If enabled, you can react with a single click. Click and hold for more options':
'활성화하면 한 번의 클릭으로 반응할 수 있습니다. 더 많은 옵션을 보려면 길게 누르세요', '활성화하면 한 번의 클릭으로 반응할 수 있습니다. 더 많은 옵션을 보려면 길게 누르세요',
'Quick reaction emoji': '빠른 반응 이모지', 'Quick reaction emoji': '빠른 반응 이모지',
'Select emoji': '이모지 선택' 'Select emoji': '이모지 선택',
'NSFW content display': 'NSFW content display',
'Hide completely': 'Hide completely',
'Show but hide content': 'Show but hide content',
'Show directly': 'Show directly',
'Click to view': 'Click to view'
} }
} }

View File

@@ -583,6 +583,11 @@ export default {
'If enabled, you can react with a single click. Click and hold for more options': 'If enabled, you can react with a single click. Click and hold for more options':
'Jeśli włączone, możesz zareagować jednym kliknięciem. Kliknij i przytrzymaj, aby uzyskać więcej opcji', 'Jeśli włączone, możesz zareagować jednym kliknięciem. Kliknij i przytrzymaj, aby uzyskać więcej opcji',
'Quick reaction emoji': 'Emoji szybkiej reakcji', 'Quick reaction emoji': 'Emoji szybkiej reakcji',
'Select emoji': 'Wybierz emoji' 'Select emoji': 'Wybierz emoji',
'NSFW content display': 'NSFW content display',
'Hide completely': 'Hide completely',
'Show but hide content': 'Show but hide content',
'Show directly': 'Show directly',
'Click to view': 'Click to view'
} }
} }

View File

@@ -578,6 +578,11 @@ export default {
'If enabled, you can react with a single click. Click and hold for more options': 'If enabled, you can react with a single click. Click and hold for more options':
'Se ativado, você pode reagir com um único clique. Clique e segure para mais opções', 'Se ativado, você pode reagir com um único clique. Clique e segure para mais opções',
'Quick reaction emoji': 'Emoji de reação rápida', 'Quick reaction emoji': 'Emoji de reação rápida',
'Select emoji': 'Selecionar emoji' 'Select emoji': 'Selecionar emoji',
'NSFW content display': 'NSFW content display',
'Hide completely': 'Hide completely',
'Show but hide content': 'Show but hide content',
'Show directly': 'Show directly',
'Click to view': 'Click to view'
} }
} }

View File

@@ -581,6 +581,11 @@ export default {
'If enabled, you can react with a single click. Click and hold for more options': 'If enabled, you can react with a single click. Click and hold for more options':
'Se ativado, pode reagir com um único clique. Clique e mantenha premido para mais opções', 'Se ativado, pode reagir com um único clique. Clique e mantenha premido para mais opções',
'Quick reaction emoji': 'Emoji de reação rápida', 'Quick reaction emoji': 'Emoji de reação rápida',
'Select emoji': 'Selecionar emoji' 'Select emoji': 'Selecionar emoji',
'NSFW content display': 'NSFW content display',
'Hide completely': 'Hide completely',
'Show but hide content': 'Show but hide content',
'Show directly': 'Show directly',
'Click to view': 'Click to view'
} }
} }

View File

@@ -583,6 +583,11 @@ export default {
'If enabled, you can react with a single click. Click and hold for more options': 'If enabled, you can react with a single click. Click and hold for more options':
'Если включено, вы можете реагировать одним щелчком. Нажмите и удерживайте для дополнительных параметров', 'Если включено, вы можете реагировать одним щелчком. Нажмите и удерживайте для дополнительных параметров',
'Quick reaction emoji': 'Эмодзи быстрой реакции', 'Quick reaction emoji': 'Эмодзи быстрой реакции',
'Select emoji': 'Выбрать эмодзи' 'Select emoji': 'Выбрать эмодзи',
'NSFW content display': 'NSFW content display',
'Hide completely': 'Hide completely',
'Show but hide content': 'Show but hide content',
'Show directly': 'Show directly',
'Click to view': 'Click to view'
} }
} }

View File

@@ -570,6 +570,11 @@ export default {
'If enabled, you can react with a single click. Click and hold for more options': 'If enabled, you can react with a single click. Click and hold for more options':
'หากเปิดใช้งาน คุณสามารถรีแอคได้ด้วยคลิกเดียว คลิกค้างไว้สำหรับตัวเลือกเพิ่มเติม', 'หากเปิดใช้งาน คุณสามารถรีแอคได้ด้วยคลิกเดียว คลิกค้างไว้สำหรับตัวเลือกเพิ่มเติม',
'Quick reaction emoji': 'อีโมจิรีแอคชั่นด่วน', 'Quick reaction emoji': 'อีโมจิรีแอคชั่นด่วน',
'Select emoji': 'เลือกอีโมจิ' 'Select emoji': 'เลือกอีโมจิ',
'NSFW content display': 'NSFW content display',
'Hide completely': 'Hide completely',
'Show but hide content': 'Show but hide content',
'Show directly': 'Show directly',
'Click to view': 'Click to view'
} }
} }

View File

@@ -563,6 +563,11 @@ export default {
'If enabled, you can react with a single click. Click and hold for more options': 'If enabled, you can react with a single click. Click and hold for more options':
'启用后,您可以通过单击进行点赞。长按以获取更多选项', '启用后,您可以通过单击进行点赞。长按以获取更多选项',
'Quick reaction emoji': '快速点赞表情', 'Quick reaction emoji': '快速点赞表情',
'Select emoji': '选择表情' 'Select emoji': '选择表情',
'NSFW content display': 'NSFW 内容显示',
'Hide completely': '完全隐藏',
'Show but hide content': '显示但隐藏内容',
'Show directly': '直接显示',
'Click to view': '点击查看'
} }
} }

View File

@@ -4,14 +4,14 @@ import { Button } from '@/components/ui/button'
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 { MEDIA_AUTO_LOAD_POLICY } from '@/constants' import { MEDIA_AUTO_LOAD_POLICY, NSFW_DISPLAY_POLICY } from '@/constants'
import { LocalizedLanguageNames, TLanguage } from '@/i18n' import { LocalizedLanguageNames, TLanguage } from '@/i18n'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { cn, isSupportCheckConnectionType } from '@/lib/utils' import { cn, isSupportCheckConnectionType } from '@/lib/utils'
import { useContentPolicy } from '@/providers/ContentPolicyProvider' import { useContentPolicy } from '@/providers/ContentPolicyProvider'
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 { TMediaAutoLoadPolicy, TNsfwDisplayPolicy } from '@/types'
import { SelectValue } from '@radix-ui/react-select' import { SelectValue } from '@radix-ui/react-select'
import { RotateCcw } from 'lucide-react' import { RotateCcw } from 'lucide-react'
import { forwardRef, HTMLProps, useState } from 'react' import { forwardRef, HTMLProps, useState } from 'react'
@@ -23,8 +23,8 @@ const GeneralSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
const { const {
autoplay, autoplay,
setAutoplay, setAutoplay,
defaultShowNsfw, nsfwDisplayPolicy,
setDefaultShowNsfw, setNsfwDisplayPolicy,
hideContentMentioningMutedUsers, hideContentMentioningMutedUsers,
setHideContentMentioningMutedUsers, setHideContentMentioningMutedUsers,
mediaAutoLoadPolicy, mediaAutoLoadPolicy,
@@ -110,10 +110,24 @@ const GeneralSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
/> />
</SettingItem> </SettingItem>
<SettingItem> <SettingItem>
<Label htmlFor="show-nsfw" className="text-base font-normal"> <Label htmlFor="nsfw-display-policy" className="text-base font-normal">
{t('Show NSFW content by default')} {t('NSFW content display')}
</Label> </Label>
<Switch id="show-nsfw" checked={defaultShowNsfw} onCheckedChange={setDefaultShowNsfw} /> <Select
value={nsfwDisplayPolicy}
onValueChange={(value: TNsfwDisplayPolicy) => setNsfwDisplayPolicy(value)}
>
<SelectTrigger id="nsfw-display-policy" className="w-48">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value={NSFW_DISPLAY_POLICY.HIDE}>{t('Hide completely')}</SelectItem>
<SelectItem value={NSFW_DISPLAY_POLICY.HIDE_CONTENT}>
{t('Show but hide content')}
</SelectItem>
<SelectItem value={NSFW_DISPLAY_POLICY.SHOW}>{t('Show directly')}</SelectItem>
</SelectContent>
</Select>
</SettingItem> </SettingItem>
<SettingItem> <SettingItem>
<Label htmlFor="quick-reaction" className="text-base font-normal"> <Label htmlFor="quick-reaction" className="text-base font-normal">

View File

@@ -1,14 +1,14 @@
import { MEDIA_AUTO_LOAD_POLICY } from '@/constants' import { MEDIA_AUTO_LOAD_POLICY } from '@/constants'
import storage from '@/services/local-storage.service' import storage from '@/services/local-storage.service'
import { TMediaAutoLoadPolicy } from '@/types' import { TMediaAutoLoadPolicy, TNsfwDisplayPolicy } from '@/types'
import { createContext, useContext, useEffect, useMemo, useState } from 'react' import { createContext, useContext, useEffect, useMemo, useState } from 'react'
type TContentPolicyContext = { type TContentPolicyContext = {
autoplay: boolean autoplay: boolean
setAutoplay: (autoplay: boolean) => void setAutoplay: (autoplay: boolean) => void
defaultShowNsfw: boolean nsfwDisplayPolicy: TNsfwDisplayPolicy
setDefaultShowNsfw: (showNsfw: boolean) => void setNsfwDisplayPolicy: (policy: TNsfwDisplayPolicy) => void
hideContentMentioningMutedUsers?: boolean hideContentMentioningMutedUsers?: boolean
setHideContentMentioningMutedUsers?: (hide: boolean) => void setHideContentMentioningMutedUsers?: (hide: boolean) => void
@@ -33,7 +33,7 @@ export const useContentPolicy = () => {
export function ContentPolicyProvider({ children }: { children: React.ReactNode }) { export function ContentPolicyProvider({ children }: { children: React.ReactNode }) {
const [autoplay, setAutoplay] = useState(storage.getAutoplay()) const [autoplay, setAutoplay] = useState(storage.getAutoplay())
const [defaultShowNsfw, setDefaultShowNsfw] = useState(storage.getDefaultShowNsfw()) const [nsfwDisplayPolicy, setNsfwDisplayPolicy] = useState(storage.getNsfwDisplayPolicy())
const [hideContentMentioningMutedUsers, setHideContentMentioningMutedUsers] = useState( const [hideContentMentioningMutedUsers, setHideContentMentioningMutedUsers] = useState(
storage.getHideContentMentioningMutedUsers() storage.getHideContentMentioningMutedUsers()
) )
@@ -72,9 +72,9 @@ export function ContentPolicyProvider({ children }: { children: React.ReactNode
setAutoplay(autoplay) setAutoplay(autoplay)
} }
const updateDefaultShowNsfw = (defaultShowNsfw: boolean) => { const updateNsfwDisplayPolicy = (policy: TNsfwDisplayPolicy) => {
storage.setDefaultShowNsfw(defaultShowNsfw) storage.setNsfwDisplayPolicy(policy)
setDefaultShowNsfw(defaultShowNsfw) setNsfwDisplayPolicy(policy)
} }
const updateHideContentMentioningMutedUsers = (hide: boolean) => { const updateHideContentMentioningMutedUsers = (hide: boolean) => {
@@ -97,8 +97,8 @@ export function ContentPolicyProvider({ children }: { children: React.ReactNode
value={{ value={{
autoplay, autoplay,
setAutoplay: updateAutoplay, setAutoplay: updateAutoplay,
defaultShowNsfw, nsfwDisplayPolicy,
setDefaultShowNsfw: updateDefaultShowNsfw, setNsfwDisplayPolicy: updateNsfwDisplayPolicy,
hideContentMentioningMutedUsers, hideContentMentioningMutedUsers,
setHideContentMentioningMutedUsers: updateHideContentMentioningMutedUsers, setHideContentMentioningMutedUsers: updateHideContentMentioningMutedUsers,
autoLoadMedia, autoLoadMedia,

View File

@@ -5,6 +5,7 @@ import {
ExtendedKind, ExtendedKind,
MEDIA_AUTO_LOAD_POLICY, MEDIA_AUTO_LOAD_POLICY,
NOTIFICATION_LIST_STYLE, NOTIFICATION_LIST_STYLE,
NSFW_DISPLAY_POLICY,
StorageKey, StorageKey,
TPrimaryColor TPrimaryColor
} from '@/constants' } from '@/constants'
@@ -19,6 +20,7 @@ import {
TMediaAutoLoadPolicy, TMediaAutoLoadPolicy,
TMediaUploadServiceConfig, TMediaUploadServiceConfig,
TNoteListMode, TNoteListMode,
TNsfwDisplayPolicy,
TNotificationStyle, TNotificationStyle,
TRelaySet, TRelaySet,
TThemeSetting, TThemeSetting,
@@ -46,7 +48,6 @@ class LocalStorageService {
private hideUntrustedNotes: boolean = false private hideUntrustedNotes: boolean = false
private translationServiceConfigMap: Record<string, TTranslationServiceConfig> = {} private translationServiceConfigMap: Record<string, TTranslationServiceConfig> = {}
private mediaUploadServiceConfigMap: Record<string, TMediaUploadServiceConfig> = {} private mediaUploadServiceConfigMap: Record<string, TMediaUploadServiceConfig> = {}
private defaultShowNsfw: boolean = false
private dismissedTooManyRelaysAlert: boolean = false private dismissedTooManyRelaysAlert: boolean = false
private showKinds: number[] = [] private showKinds: number[] = []
private hideContentMentioningMutedUsers: boolean = false private hideContentMentioningMutedUsers: boolean = false
@@ -60,6 +61,7 @@ class LocalStorageService {
private filterOutOnionRelays: boolean = !isTorBrowser() private filterOutOnionRelays: boolean = !isTorBrowser()
private quickReaction: boolean = false private quickReaction: boolean = false
private quickReactionEmoji: string | TEmoji = '+' private quickReactionEmoji: string | TEmoji = '+'
private nsfwDisplayPolicy: TNsfwDisplayPolicy = NSFW_DISPLAY_POLICY.HIDE_CONTENT
constructor() { constructor() {
if (!LocalStorageService.instance) { if (!LocalStorageService.instance) {
@@ -161,7 +163,20 @@ class LocalStorageService {
this.mediaUploadServiceConfigMap = JSON.parse(mediaUploadServiceConfigMapStr) this.mediaUploadServiceConfigMap = JSON.parse(mediaUploadServiceConfigMapStr)
} }
this.defaultShowNsfw = window.localStorage.getItem(StorageKey.DEFAULT_SHOW_NSFW) === 'true' // Migrate old boolean setting to new policy
const nsfwDisplayPolicyStr = window.localStorage.getItem(StorageKey.NSFW_DISPLAY_POLICY)
if (
nsfwDisplayPolicyStr &&
Object.values(NSFW_DISPLAY_POLICY).includes(nsfwDisplayPolicyStr as TNsfwDisplayPolicy)
) {
this.nsfwDisplayPolicy = nsfwDisplayPolicyStr as TNsfwDisplayPolicy
} else {
// Migration: convert old boolean to new policy
const defaultShowNsfwStr = window.localStorage.getItem(StorageKey.DEFAULT_SHOW_NSFW)
this.nsfwDisplayPolicy =
defaultShowNsfwStr === 'true' ? NSFW_DISPLAY_POLICY.SHOW : NSFW_DISPLAY_POLICY.HIDE_CONTENT
window.localStorage.setItem(StorageKey.NSFW_DISPLAY_POLICY, this.nsfwDisplayPolicy)
}
this.dismissedTooManyRelaysAlert = this.dismissedTooManyRelaysAlert =
window.localStorage.getItem(StorageKey.DISMISSED_TOO_MANY_RELAYS_ALERT) === 'true' window.localStorage.getItem(StorageKey.DISMISSED_TOO_MANY_RELAYS_ALERT) === 'true'
@@ -458,15 +473,6 @@ class LocalStorageService {
return config return config
} }
getDefaultShowNsfw() {
return this.defaultShowNsfw
}
setDefaultShowNsfw(defaultShowNsfw: boolean) {
this.defaultShowNsfw = defaultShowNsfw
window.localStorage.setItem(StorageKey.DEFAULT_SHOW_NSFW, defaultShowNsfw.toString())
}
getDismissedTooManyRelaysAlert() { getDismissedTooManyRelaysAlert() {
return this.dismissedTooManyRelaysAlert return this.dismissedTooManyRelaysAlert
} }
@@ -592,6 +598,15 @@ class LocalStorageService {
typeof emoji === 'string' ? emoji : JSON.stringify(emoji) typeof emoji === 'string' ? emoji : JSON.stringify(emoji)
) )
} }
getNsfwDisplayPolicy() {
return this.nsfwDisplayPolicy
}
setNsfwDisplayPolicy(policy: TNsfwDisplayPolicy) {
this.nsfwDisplayPolicy = policy
window.localStorage.setItem(StorageKey.NSFW_DISPLAY_POLICY, policy)
}
} }
const instance = new LocalStorageService() const instance = new LocalStorageService()

View File

@@ -1,5 +1,5 @@
import { Event, Filter, VerifiedEvent } from 'nostr-tools' import { Event, Filter, VerifiedEvent } from 'nostr-tools'
import { MEDIA_AUTO_LOAD_POLICY, NOTIFICATION_LIST_STYLE, POLL_TYPE } from '../constants' import { MEDIA_AUTO_LOAD_POLICY, NOTIFICATION_LIST_STYLE, NSFW_DISPLAY_POLICY, POLL_TYPE } from '../constants'
export type TSubRequestFilter = Omit<Filter, 'since' | 'until'> & { limit: number } export type TSubRequestFilter = Omit<Filter, 'since' | 'until'> & { limit: number }
@@ -205,3 +205,6 @@ export type TAwesomeRelayCollection = {
export type TMediaAutoLoadPolicy = export type TMediaAutoLoadPolicy =
(typeof MEDIA_AUTO_LOAD_POLICY)[keyof typeof MEDIA_AUTO_LOAD_POLICY] (typeof MEDIA_AUTO_LOAD_POLICY)[keyof typeof MEDIA_AUTO_LOAD_POLICY]
export type TNsfwDisplayPolicy =
(typeof NSFW_DISPLAY_POLICY)[keyof typeof NSFW_DISPLAY_POLICY]