feat: add toggle to show NSFW content by default

This commit is contained in:
codytseng
2025-08-14 22:56:44 +08:00
parent 352eecc416
commit cb2ad30b1d
23 changed files with 108 additions and 55 deletions

View File

@@ -4,8 +4,8 @@ import './index.css'
import { ThemeProvider } from '@/providers/ThemeProvider'
import { Toaster } from './components/ui/sonner'
import { PageManager } from './PageManager'
import { AutoplayProvider } from './providers/AutoplayProvider'
import { BookmarksProvider } from './providers/BookmarksProvider'
import { ContentPolicyProvider } from './providers/ContentPolicyProvider'
import { FavoriteRelaysProvider } from './providers/FavoriteRelaysProvider'
import { FeedProvider } from './providers/FeedProvider'
import { FollowListProvider } from './providers/FollowListProvider'
@@ -21,7 +21,7 @@ import { ZapProvider } from './providers/ZapProvider'
export default function App(): JSX.Element {
return (
<ThemeProvider>
<AutoplayProvider>
<ContentPolicyProvider>
<ScreenSizeProvider>
<NostrProvider>
<ZapProvider>
@@ -48,7 +48,7 @@ export default function App(): JSX.Element {
</ZapProvider>
</NostrProvider>
</ScreenSizeProvider>
</AutoplayProvider>
</ContentPolicyProvider>
</ThemeProvider>
)
}

View File

@@ -8,6 +8,7 @@ import {
isPictureEvent
} from '@/lib/event'
import { toNote } from '@/lib/link'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useMuteList } from '@/providers/MuteListProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { Event, kinds } from 'nostr-tools'
@@ -60,6 +61,7 @@ export default function Note({
[event]
)
const usingClient = useMemo(() => getUsingClient(event), [event])
const { defaultShowNsfw } = useContentPolicy()
const [showNsfw, setShowNsfw] = useState(false)
const { mutePubkeys } = useMuteList()
const [showMuted, setShowMuted] = useState(false)
@@ -83,7 +85,7 @@ export default function Note({
content = <UnknownNote className="mt-2" event={event} />
} else if (mutePubkeys.includes(event.pubkey) && !showMuted) {
content = <MutedNote show={() => setShowMuted(true)} />
} else if (isNsfwEvent(event) && !showNsfw) {
} else if (!defaultShowNsfw && isNsfwEvent(event) && !showNsfw) {
content = <NsfwNote show={() => setShowNsfw(true)} />
} else if (event.kind === kinds.Highlights) {
content = <Highlight className="mt-2" event={event} />

View File

@@ -1,10 +1,10 @@
import { cn, isInViewport } from '@/lib/utils'
import { useAutoplay } from '@/providers/AutoplayProvider'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import mediaManager from '@/services/media-manager.service'
import { useEffect, useRef } from 'react'
export default function VideoPlayer({ src, className }: { src: string; className?: string }) {
const { autoplay } = useAutoplay()
const { autoplay } = useContentPolicy()
const videoRef = useRef<HTMLVideoElement>(null)
const containerRef = useRef<HTMLDivElement>(null)

View File

@@ -34,6 +34,7 @@ export const StorageKey = {
TRANSLATION_SERVICE_CONFIG_MAP: 'translationServiceConfigMap',
MEDIA_UPLOAD_SERVICE_CONFIG_MAP: 'mediaUploadServiceConfigMap',
HIDE_UNTRUSTED_NOTES: 'hideUntrustedNotes',
DEFAULT_SHOW_NSFW: 'defaultShowNsfw',
MEDIA_UPLOAD_SERVICE: 'mediaUploadService', // deprecated
HIDE_UNTRUSTED_EVENTS: 'hideUntrustedEvents', // deprecated
ACCOUNT_RELAY_LIST_EVENT_MAP: 'accountRelayListEventMap', // deprecated

View File

@@ -337,6 +337,7 @@ export default {
FollowListNotFoundConfirmation:
'لم يتم العثور على قائمة المتابعة. هل تريد إنشاء واحدة جديدة؟ إذا كنت قد تابعت مستخدمين من قبل، يرجى عدم التأكيد لأن هذه العملية ستؤدي إلى فقدان قائمة المتابعة السابقة.',
MuteListNotFoundConfirmation:
'لم يتم العثور على قائمة الكتم. هل تريد إنشاء واحدة جديدة؟ إذا كنت قد كتمت مستخدمين من قبل، يرجى عدم التأكيد لأن هذه العملية ستؤدي إلى فقدان قائمة الكتم السابقة.'
'لم يتم العثور على قائمة الكتم. هل تريد إنشاء واحدة جديدة؟ إذا كنت قد كتمت مستخدمين من قبل، يرجى عدم التأكيد لأن هذه العملية ستؤدي إلى فقدان قائمة الكتم السابقة.',
'Show NSFW content by default': 'إظهار محتوى NSFW افتراضياً'
}
}

View File

@@ -344,6 +344,7 @@ export default {
FollowListNotFoundConfirmation:
'Folgeliste nicht gefunden. Möchten Sie eine neue erstellen? Wenn Sie zuvor Benutzer gefolgt haben, bestätigen Sie bitte NICHT, da diese Operation dazu führt, dass Sie Ihre vorherige Folgeliste verlieren.',
MuteListNotFoundConfirmation:
'Stummschaltungsliste nicht gefunden. Möchten Sie eine neue erstellen? Wenn Sie zuvor Benutzer stummgeschaltet haben, bestätigen Sie bitte NICHT, da diese Operation dazu führt, dass Sie Ihre vorherige Stummschaltungsliste verlieren.'
'Stummschaltungsliste nicht gefunden. Möchten Sie eine neue erstellen? Wenn Sie zuvor Benutzer stummgeschaltet haben, bestätigen Sie bitte NICHT, da diese Operation dazu führt, dass Sie Ihre vorherige Stummschaltungsliste verlieren.',
'Show NSFW content by default': 'NSFW-Inhalte standardmäßig anzeigen'
}
}

View File

@@ -338,6 +338,7 @@ export default {
FollowListNotFoundConfirmation:
'Follow list not found. Do you want to create a new one? If you have followed users before, please DO NOT confirm as this operation will cause you to lose your previous follow list.',
MuteListNotFoundConfirmation:
'Mute list not found. Do you want to create a new one? If you have muted users before, please DO NOT confirm as this operation will cause you to lose your previous mute list.'
'Mute list not found. Do you want to create a new one? If you have muted users before, please DO NOT confirm as this operation will cause you to lose your previous mute list.',
'Show NSFW content by default': 'Show NSFW content by default'
}
}

View File

@@ -343,6 +343,7 @@ export default {
FollowListNotFoundConfirmation:
'Lista de seguidos no encontrada. ¿Quieres crear una nueva? Si has seguido usuarios antes, por favor NO confirmes ya que esta operación te hará perder tu lista de seguidos anterior.',
MuteListNotFoundConfirmation:
'Lista de silenciados no encontrada. ¿Quieres crear una nueva? Si has silenciado usuarios antes, por favor NO confirmes ya que esta operación te hará perder tu lista de silenciados anterior.'
'Lista de silenciados no encontrada. ¿Quieres crear una nueva? Si has silenciado usuarios antes, por favor NO confirmes ya que esta operación te hará perder tu lista de silenciados anterior.',
'Show NSFW content by default': 'Mostrar contenido NSFW por defecto'
}
}

View File

@@ -338,6 +338,7 @@ export default {
FollowListNotFoundConfirmation:
'فهرست دنبال‌کنندگان پیدا نشد. آیا می‌خواهید یکی جدید ایجاد کنید؟ اگر قبلاً کاربرانی را دنبال کرده‌اید، لطفاً تأیید نکنید زیرا این عملیات باعث از دست رفتن فهرست دنبال‌کنندگان قبلی شما خواهد شد.',
MuteListNotFoundConfirmation:
'فهرست بی‌صدا شده‌ها پیدا نشد. آیا می‌خواهید یکی جدید ایجاد کنید؟ اگر قبلاً کاربرانی را بی‌صدا کرده‌اید، لطفاً تأیید نکنید زیرا این عملیات باعث از دست رفتن فهرست بی‌صدا شده‌های قبلی شما خواهد شد.'
'فهرست بی‌صدا شده‌ها پیدا نشد. آیا می‌خواهید یکی جدید ایجاد کنید؟ اگر قبلاً کاربرانی را بی‌صدا کرده‌اید، لطفاً تأیید نکنید زیرا این عملیات باعث از دست رفتن فهرست بی‌صدا شده‌های قبلی شما خواهد شد.',
'Show NSFW content by default': 'نمایش محتوای NSFW به صورت پیش‌فرض'
}
}

View File

@@ -343,6 +343,7 @@ export default {
FollowListNotFoundConfirmation:
'Liste de suivi non trouvée. Voulez-vous en créer une nouvelle ? Si vous avez suivi des utilisateurs auparavant, veuillez NE PAS confirmer car cette opération vous fera perdre votre liste de suivi précédente.',
MuteListNotFoundConfirmation:
'Liste de mise en sourdine non trouvée. Voulez-vous en créer une nouvelle ? Si vous avez mis en sourdine des utilisateurs auparavant, veuillez NE PAS confirmer car cette opération vous fera perdre votre liste de mise en sourdine précédente.'
'Liste de mise en sourdine non trouvée. Voulez-vous en créer une nouvelle ? Si vous avez mis en sourdine des utilisateurs auparavant, veuillez NE PAS confirmer car cette opération vous fera perdre votre liste de mise en sourdine précédente.',
'Show NSFW content by default': 'Afficher le contenu NSFW par défaut'
}
}

View File

@@ -342,6 +342,7 @@ export default {
FollowListNotFoundConfirmation:
'Elenco seguiti non trovato. Vuoi crearne uno nuovo? Se hai già seguito degli utenti in precedenza, per favore NON confermare poiché questa operazione causerà la perdita del tuo elenco seguiti precedente.',
MuteListNotFoundConfirmation:
'Elenco utenti silenziati non trovato. Vuoi crearne uno nuovo? Se hai già silenziato degli utenti in precedenza, per favore NON confermare poiché questa operazione causerà la perdita del tuo elenco utenti silenziati precedente.'
'Elenco utenti silenziati non trovato. Vuoi crearne uno nuovo? Se hai già silenziato degli utenti in precedenza, per favore NON confermare poiché questa operazione causerà la perdita del tuo elenco utenti silenziati precedente.',
'Show NSFW content by default': 'Mostra contenuti NSFW per impostazione predefinita'
}
}

View File

@@ -340,6 +340,7 @@ export default {
FollowListNotFoundConfirmation:
'フォローリストが見つかりません。新しいものを作成しますか?以前にユーザーをフォローしたことがある場合は、この操作により前のフォローリストが失われるため、確認しないでください。',
MuteListNotFoundConfirmation:
'ミュートリストが見つかりません。新しいものを作成しますか?以前にユーザーをミュートしたことがある場合は、この操作により前のミュートリストが失われるため、確認しないでください。'
'ミュートリストが見つかりません。新しいものを作成しますか?以前にユーザーをミュートしたことがある場合は、この操作により前のミュートリストが失われるため、確認しないでください。',
'Show NSFW content by default': 'デフォルトでNSFWコンテンツを表示'
}
}

View File

@@ -339,6 +339,7 @@ export default {
FollowListNotFoundConfirmation:
'팔로우 목록을 찾을 수 없습니다. 새로 만드시겠습니까? 이전에 사용자를 팔로우한 적이 있다면 이 작업으로 인해 이전 팔로우 목록을 잃게 되므로 확인하지 마시기 바랍니다.',
MuteListNotFoundConfirmation:
'음소거 목록을 찾을 수 없습니다. 새로 만드시겠습니까? 이전에 사용자를 음소거한 적이 있다면 이 작업으로 인해 이전 음소거 목록을 잃게 되므로 확인하지 마시기 바랍니다.'
'음소거 목록을 찾을 수 없습니다. 새로 만드시겠습니까? 이전에 사용자를 음소거한 적이 있다면 이 작업으로 인해 이전 음소거 목록을 잃게 되므로 확인하지 마시기 바랍니다.',
'Show NSFW content by default': '기본적으로 NSFW 콘텐츠 표시'
}
}

View File

@@ -342,6 +342,7 @@ export default {
FollowListNotFoundConfirmation:
'Lista obserwowanych nie została znaleziona. Czy chcesz utworzyć nową? Jeśli wcześniej obserwowałeś użytkowników, proszę NIE potwierdzaj, ponieważ ta operacja spowoduje utratę poprzedniej listy obserwowanych.',
MuteListNotFoundConfirmation:
'Lista wyciszonych nie została znaleziona. Czy chcesz utworzyć nową? Jeśli wcześniej wyciszałeś użytkowników, proszę NIE potwierdzaj, ponieważ ta operacja spowoduje utratę poprzedniej listy wyciszonych.'
'Lista wyciszonych nie została znaleziona. Czy chcesz utworzyć nową? Jeśli wcześniej wyciszałeś użytkowników, proszę NIE potwierdzaj, ponieważ ta operacja spowoduje utratę poprzedniej listy wyciszonych.',
'Show NSFW content by default': 'Domyślnie pokazuj treści NSFW'
}
}

View File

@@ -341,6 +341,7 @@ export default {
FollowListNotFoundConfirmation:
'Lista de seguindo não encontrada. Deseja criar uma nova? Se você seguiu usuários antes, por favor NÃO confirme, pois esta operação fará você perder sua lista de seguindo anterior.',
MuteListNotFoundConfirmation:
'Lista de silenciados não encontrada. Deseja criar uma nova? Se você silenciou usuários antes, por favor NÃO confirme, pois esta operação fará você perder sua lista de silenciados anterior.'
'Lista de silenciados não encontrada. Deseja criar uma nova? Se você silenciou usuários antes, por favor NÃO confirme, pois esta operação fará você perder sua lista de silenciados anterior.',
'Show NSFW content by default': 'Mostrar conteúdo NSFW por padrão'
}
}

View File

@@ -342,6 +342,7 @@ export default {
FollowListNotFoundConfirmation:
'Lista de seguir não encontrada. Deseja criar uma nova? Se seguiu utilizadores anteriormente, por favor NÃO confirme, pois esta operação fará com que perca a sua lista de seguir anterior.',
MuteListNotFoundConfirmation:
'Lista de silenciados não encontrada. Deseja criar uma nova? Se silenciou utilizadores anteriormente, por favor NÃO confirme, pois esta operação fará com que perca a sua lista de silenciados anterior.'
'Lista de silenciados não encontrada. Deseja criar uma nova? Se silenciou utilizadores anteriormente, por favor NÃO confirme, pois esta operação fará com que perca a sua lista de silenciados anterior.',
'Show NSFW content by default': 'Mostrar conteúdo NSFW por padrão'
}
}

View File

@@ -342,6 +342,7 @@ export default {
FollowListNotFoundConfirmation:
'Список подписок не найден. Хотите создать новый? Если вы уже подписывались на пользователей ранее, пожалуйста, НЕ подтверждайте, так как эта операция приведет к потере вашего предыдущего списка подписок.',
MuteListNotFoundConfirmation:
'Список заблокированных не найден. Хотите создать новый? Если вы уже блокировали пользователей ранее, пожалуйста, НЕ подтверждайте, так как эта операция приведет к потере вашего предыдущего списка заблокированных.'
'Список заблокированных не найден. Хотите создать новый? Если вы уже блокировали пользователей ранее, пожалуйста, НЕ подтверждайте, так как эта операция приведет к потере вашего предыдущего списка заблокированных.',
'Show NSFW content by default': 'Показывать контент NSFW по умолчанию'
}
}

View File

@@ -336,6 +336,7 @@ export default {
FollowListNotFoundConfirmation:
'ไม่พบรายการติดตาม คุณต้องการสร้างรายการใหม่หรือไม่? หากคุณเคยติดตามผู้ใช้มาก่อน กรุณาอย่ายืนยัน เพราะการดำเนินการนี้จะทำให้คุณสูญเสียรายการติดตามก่อนหน้านี้',
MuteListNotFoundConfirmation:
'ไม่พบรายการปิดเสียง คุณต้องการสร้างรายการใหม่หรือไม่? หากคุณเคยปิดเสียงผู้ใช้มาก่อน กรุณาอย่ายืนยัน เพราะการดำเนินการนี้จะทำให้คุณสูญเสียรายการปิดเสียงก่อนหน้านี้'
'ไม่พบรายการปิดเสียง คุณต้องการสร้างรายการใหม่หรือไม่? หากคุณเคยปิดเสียงผู้ใช้มาก่อน กรุณาอย่ายืนยัน เพราะการดำเนินการนี้จะทำให้คุณสูญเสียรายการปิดเสียงก่อนหน้านี้',
'Show NSFW content by default': 'แสดงเนื้อหา NSFW โดยค่าเริ่มต้น'
}
}

View File

@@ -335,6 +335,7 @@ export default {
FollowListNotFoundConfirmation:
'未找到关注列表。你想创建一个新的吗?如果你之前已经关注了用户,请不要确认,因为此操作会导致你丢失之前的关注列表。',
MuteListNotFoundConfirmation:
'未找到屏蔽列表。你想创建一个新的吗?如果你之前已经屏蔽了用户,请不要确认,因为此操作会导致你丢失之前的屏蔽列表。'
'未找到屏蔽列表。你想创建一个新的吗?如果你之前已经屏蔽了用户,请不要确认,因为此操作会导致你丢失之前的屏蔽列表。',
'Show NSFW content by default': '默认显示 NSFW 内容'
}
}

View File

@@ -4,7 +4,7 @@ import { Switch } from '@/components/ui/switch'
import { LocalizedLanguageNames, TLanguage } from '@/i18n'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { cn } from '@/lib/utils'
import { useAutoplay } from '@/providers/AutoplayProvider'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useTheme } from '@/providers/ThemeProvider'
import { useUserTrust } from '@/providers/UserTrustProvider'
import { SelectValue } from '@radix-ui/react-select'
@@ -15,7 +15,7 @@ const GeneralSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
const { t, i18n } = useTranslation()
const [language, setLanguage] = useState<TLanguage>(i18n.language as TLanguage)
const { themeSetting, setThemeSetting } = useTheme()
const { autoplay, setAutoplay } = useAutoplay()
const { autoplay, setAutoplay, defaultShowNsfw, setDefaultShowNsfw } = useContentPolicy()
const { hideUntrustedNotes, updateHideUntrustedNotes } = useUserTrust()
const handleLanguageChange = (value: TLanguage) => {
@@ -75,6 +75,12 @@ const GeneralSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
onCheckedChange={updateHideUntrustedNotes}
/>
</SettingItem>
<SettingItem>
<Label htmlFor="show-nsfw" className="text-base font-normal">
{t('Show NSFW content by default')}
</Label>
<Switch id="show-nsfw" checked={defaultShowNsfw} onCheckedChange={setDefaultShowNsfw} />
</SettingItem>
</div>
</SecondaryPageLayout>
)

View File

@@ -1,32 +0,0 @@
import { createContext, useContext, useState } from 'react'
import storage from '@/services/local-storage.service'
type TAutoplayContext = {
autoplay: boolean
setAutoplay: (autoplay: boolean) => void
}
const AutoplayContext = createContext<TAutoplayContext | undefined>(undefined)
export const useAutoplay = () => {
const context = useContext(AutoplayContext)
if (!context) {
throw new Error('useAutoplay must be used within an AutoplayProvider')
}
return context
}
export function AutoplayProvider({ children }: { children: React.ReactNode }) {
const [autoplay, setAutoplay] = useState<boolean>(storage.getAutoplay())
const updateAutoplay = (autoplay: boolean) => {
storage.setAutoplay(autoplay)
setAutoplay(autoplay)
}
return (
<AutoplayContext.Provider value={{ autoplay, setAutoplay: updateAutoplay }}>
{children}
</AutoplayContext.Provider>
)
}

View File

@@ -0,0 +1,48 @@
import storage from '@/services/local-storage.service'
import { createContext, useContext, useState } from 'react'
type TContentPolicyContext = {
autoplay: boolean
setAutoplay: (autoplay: boolean) => void
defaultShowNsfw: boolean
setDefaultShowNsfw: (showNsfw: boolean) => void
}
const ContentPolicyContext = createContext<TContentPolicyContext | undefined>(undefined)
export const useContentPolicy = () => {
const context = useContext(ContentPolicyContext)
if (!context) {
throw new Error('useContentPolicy must be used within an ContentPolicyProvider')
}
return context
}
export function ContentPolicyProvider({ children }: { children: React.ReactNode }) {
const [autoplay, setAutoplay] = useState<boolean>(storage.getAutoplay())
const [defaultShowNsfw, setDefaultShowNsfw] = useState<boolean>(storage.getDefaultShowNsfw())
const updateAutoplay = (autoplay: boolean) => {
storage.setAutoplay(autoplay)
setAutoplay(autoplay)
}
const updateDefaultShowNsfw = (defaultShowNsfw: boolean) => {
storage.setDefaultShowNsfw(defaultShowNsfw)
setDefaultShowNsfw(defaultShowNsfw)
}
return (
<ContentPolicyContext.Provider
value={{
autoplay,
setAutoplay: updateAutoplay,
defaultShowNsfw,
setDefaultShowNsfw: updateDefaultShowNsfw
}}
>
{children}
</ContentPolicyContext.Provider>
)
}

View File

@@ -32,6 +32,7 @@ class LocalStorageService {
private hideUntrustedNotes: boolean = false
private translationServiceConfigMap: Record<string, TTranslationServiceConfig> = {}
private mediaUploadServiceConfigMap: Record<string, TMediaUploadServiceConfig> = {}
private defaultShowNsfw: boolean = false
constructor() {
if (!LocalStorageService.instance) {
@@ -133,6 +134,9 @@ class LocalStorageService {
this.mediaUploadServiceConfigMap = JSON.parse(mediaUploadServiceConfigMapStr)
}
const defaultShowNsfwStr = window.localStorage.getItem(StorageKey.DEFAULT_SHOW_NSFW)
this.defaultShowNsfw = defaultShowNsfwStr === 'true'
// Clean up deprecated data
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)
window.localStorage.removeItem(StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP)
@@ -347,6 +351,15 @@ class LocalStorageService {
)
return config
}
getDefaultShowNsfw() {
return this.defaultShowNsfw
}
setDefaultShowNsfw(defaultShowNsfw: boolean) {
this.defaultShowNsfw = defaultShowNsfw
window.localStorage.setItem(StorageKey.DEFAULT_SHOW_NSFW, defaultShowNsfw.toString())
}
}
const instance = new LocalStorageService()