diff --git a/src/components/HideUntrustedContentButton/index.tsx b/src/components/HideUntrustedContentButton/index.tsx new file mode 100644 index 00000000..61773ed3 --- /dev/null +++ b/src/components/HideUntrustedContentButton/index.tsx @@ -0,0 +1,69 @@ +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger +} from '@/components/ui/alert-dialog' +import { Button, buttonVariants } from '@/components/ui/button' +import { useUserTrust } from '@/providers/UserTrustProvider' +import { VariantProps } from 'class-variance-authority' +import { Shield, ShieldCheck } from 'lucide-react' + +export default function HideUntrustedContentButton({ + type, + size = 'icon' +}: { + type: 'interactions' | 'notifications' + size?: VariantProps['size'] +}) { + const { + hideUntrustedInteractions, + hideUntrustedNotifications, + updateHideUntrustedInteractions, + updateHideUntrustedNotifications + } = useUserTrust() + + const enabled = type === 'interactions' ? hideUntrustedInteractions : hideUntrustedNotifications + + const updateEnabled = + type === 'interactions' ? updateHideUntrustedInteractions : updateHideUntrustedNotifications + + return ( + + + + + + + + {enabled ? 'Show' : 'Hide'} untrusted {type} + + + {enabled + ? `Currently hiding ${type} from untrusted users. ` + : `Currently showing all ${type}. `} + Trusted users include people you follow and people they follow. + {enabled + ? ` Click continue to show all ${type}.` + : ` Click continue to hide ${type} from untrusted users.`} + + + + Cancel + updateEnabled(!enabled)}>Continue + + + + ) +} diff --git a/src/components/NoteInteractions/index.tsx b/src/components/NoteInteractions/index.tsx index 131117c7..fdecccdc 100644 --- a/src/components/NoteInteractions/index.tsx +++ b/src/components/NoteInteractions/index.tsx @@ -1,6 +1,7 @@ import { Separator } from '@/components/ui/separator' import { Event } from 'nostr-tools' import { useState } from 'react' +import HideUntrustedContentButton from '../HideUntrustedContentButton' import QuoteList from '../QuoteList' import ReplyNoteList from '../ReplyNoteList' import { Tabs, TTabValue } from './Tabs' @@ -16,7 +17,10 @@ export default function NoteInteractions({ return ( <> - +
+ + +
{type === 'replies' ? ( diff --git a/src/components/NoteStats/ReplyButton.tsx b/src/components/NoteStats/ReplyButton.tsx index 50380355..9771ffbc 100644 --- a/src/components/NoteStats/ReplyButton.tsx +++ b/src/components/NoteStats/ReplyButton.tsx @@ -12,11 +12,13 @@ export default function ReplyButton({ event }: { event: Event }) { const { t } = useTranslation() const { checkLogin } = useNostr() const { repliesMap } = useReply() - const { isUserTrusted } = useUserTrust() - const replyCount = useMemo( - () => repliesMap.get(event.id)?.events.filter((evt) => isUserTrusted(evt.pubkey)).length || 0, - [repliesMap, event.id, isUserTrusted] - ) + const { hideUntrustedInteractions, isUserTrusted } = useUserTrust() + const replyCount = useMemo(() => { + if (hideUntrustedInteractions) { + return repliesMap.get(event.id)?.events.filter((evt) => isUserTrusted(evt.pubkey)).length ?? 0 + } + return repliesMap.get(event.id)?.events.length ?? 0 + }, [repliesMap, event.id, isUserTrusted]) const [open, setOpen] = useState(false) return ( diff --git a/src/components/NotificationList/index.tsx b/src/components/NotificationList/index.tsx index dcfffc51..47c0f841 100644 --- a/src/components/NotificationList/index.tsx +++ b/src/components/NotificationList/index.tsx @@ -23,7 +23,7 @@ const NotificationList = forwardRef((_, ref) => { const { t } = useTranslation() const { current } = usePrimaryPage() const { pubkey } = useNostr() - const { isUserTrusted } = useUserTrust() + const { hideUntrustedNotifications, isUserTrusted } = useUserTrust() const { clearNewNotifications, getNotificationsSeenAt } = useNotification() const { updateNoteStatsByEvents } = useNoteStats() const [notificationType, setNotificationType] = useState('all') @@ -124,9 +124,11 @@ const NotificationList = forwardRef((_, ref) => { }, [pubkey, refreshCount, filterKinds, current]) useEffect(() => { - const visibleNotifications = notifications - .slice(0, showCount) - .filter((event) => isUserTrusted(event.pubkey)) + let visibleNotifications = notifications.slice(0, showCount) + if (hideUntrustedNotifications) { + visibleNotifications = visibleNotifications.filter((event) => isUserTrusted(event.pubkey)) + } + const index = visibleNotifications.findIndex((event) => event.created_at <= lastReadTime) if (index === -1) { setNewNotifications(visibleNotifications) @@ -135,7 +137,7 @@ const NotificationList = forwardRef((_, ref) => { setNewNotifications(visibleNotifications.slice(0, index)) setOldNotifications(visibleNotifications.slice(index)) } - }, [notifications, lastReadTime, showCount, isUserTrusted]) + }, [notifications, lastReadTime, showCount, hideUntrustedNotifications, isUserTrusted]) useEffect(() => { const options = { diff --git a/src/components/QuoteList/index.tsx b/src/components/QuoteList/index.tsx index f753d112..9f427977 100644 --- a/src/components/QuoteList/index.tsx +++ b/src/components/QuoteList/index.tsx @@ -1,5 +1,6 @@ import { BIG_RELAY_URLS } from '@/constants' import { useNostr } from '@/providers/NostrProvider' +import { useUserTrust } from '@/providers/UserTrustProvider' import client from '@/services/client.service' import dayjs from 'dayjs' import { Event, kinds } from 'nostr-tools' @@ -13,6 +14,7 @@ const SHOW_COUNT = 10 export default function QuoteList({ event, className }: { event: Event; className?: string }) { const { t } = useTranslation() const { startLogin } = useNostr() + const { hideUntrustedInteractions, isUserTrusted } = useUserTrust() const [timelineKey, setTimelineKey] = useState(undefined) const [events, setEvents] = useState([]) const [showCount, setShowCount] = useState(SHOW_COUNT) @@ -124,9 +126,12 @@ export default function QuoteList({ event, className }: { event: Event; classNam
- {events.slice(0, showCount).map((event) => ( - - ))} + {events.slice(0, showCount).map((event) => { + if (hideUntrustedInteractions && !isUserTrusted(event.pubkey)) { + return null + } + return + })}
{hasMore || loading ? (
diff --git a/src/components/ReplyNoteList/index.tsx b/src/components/ReplyNoteList/index.tsx index 6ac11d74..ca65c1ea 100644 --- a/src/components/ReplyNoteList/index.tsx +++ b/src/components/ReplyNoteList/index.tsx @@ -32,7 +32,7 @@ export default function ReplyNoteList({ }) { const { t } = useTranslation() const { currentIndex } = useSecondaryPage() - const { isUserTrusted } = useUserTrust() + const { hideUntrustedInteractions, isUserTrusted } = useUserTrust() const [rootInfo, setRootInfo] = useState(undefined) const { repliesMap, addReplies } = useReply() const replies = useMemo(() => { @@ -250,7 +250,7 @@ export default function ReplyNoteList({ )}
{replies.slice(0, showCount).map((reply) => { - if (!isUserTrusted(reply.pubkey)) { + if (hideUntrustedInteractions && !isUserTrusted(reply.pubkey)) { const repliesForThisReply = repliesMap.get(reply.id) // If the reply is not trusted and there are no trusted replies for this reply, skip rendering if ( diff --git a/src/constants.ts b/src/constants.ts index 99010773..4f292a82 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -14,7 +14,9 @@ export const StorageKey = { ACCOUNT_FEED_INFO_MAP: 'accountFeedInfoMap', MEDIA_UPLOAD_SERVICE: 'mediaUploadService', AUTOPLAY: 'autoplay', - HIDE_UNTRUSTED_EVENTS: 'hideUntrustedEvents', + HIDE_UNTRUSTED_INTERACTIONS: 'hideUntrustedInteractions', + HIDE_UNTRUSTED_NOTIFICATIONS: 'hideUntrustedNotifications', + HIDE_UNTRUSTED_EVENTS: 'hideUntrustedEvents', // deprecated ACCOUNT_RELAY_LIST_EVENT_MAP: 'accountRelayListEventMap', // deprecated ACCOUNT_FOLLOW_LIST_EVENT_MAP: 'accountFollowListEventMap', // deprecated ACCOUNT_MUTE_LIST_EVENT_MAP: 'accountMuteListEventMap', // deprecated @@ -83,4 +85,4 @@ export const NIP_96_SERVICE = [ ] export const DEFAULT_NIP_96_SERVICE = 'https://nostr.build' -export const DEFAULT_NOSTRCONNECT_RELAY = ['wss://relay.nsec.app/']; \ No newline at end of file +export const DEFAULT_NOSTRCONNECT_RELAY = ['wss://relay.nsec.app/'] diff --git a/src/pages/primary/NotificationListPage/index.tsx b/src/pages/primary/NotificationListPage/index.tsx index b4a5a173..b30b70e7 100644 --- a/src/pages/primary/NotificationListPage/index.tsx +++ b/src/pages/primary/NotificationListPage/index.tsx @@ -1,3 +1,4 @@ +import HideUntrustedContentButton from '@/components/HideUntrustedContentButton' import NotificationList from '@/components/NotificationList' import PrimaryPageLayout from '@/layouts/PrimaryPageLayout' import { usePrimaryPage } from '@/PageManager' @@ -35,9 +36,12 @@ function NotificationListPageTitlebar() { const { t } = useTranslation() return ( -
- -
{t('Notifications')}
+
+
+ +
{t('Notifications')}
+
+
) } diff --git a/src/pages/secondary/GeneralSettingsPage/index.tsx b/src/pages/secondary/GeneralSettingsPage/index.tsx index 41bae265..215aae7b 100644 --- a/src/pages/secondary/GeneralSettingsPage/index.tsx +++ b/src/pages/secondary/GeneralSettingsPage/index.tsx @@ -6,7 +6,6 @@ import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' import { cn } from '@/lib/utils' import { useAutoplay } from '@/providers/AutoplayProvider' import { useTheme } from '@/providers/ThemeProvider' -import { useUserTrust } from '@/providers/UserTrustProvider' import { SelectValue } from '@radix-ui/react-select' import { forwardRef, HTMLProps, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -16,8 +15,6 @@ const GeneralSettingsPage = forwardRef(({ index }: { index?: number }, ref) => { const [language, setLanguage] = useState(i18n.language as TLanguage) const { themeSetting, setThemeSetting } = useTheme() const { autoplay, setAutoplay } = useAutoplay() - const { enabled: hideUntrustedEventsEnabled, updateEnabled: updateHideUntrustedEventsEnabled } = - useUserTrust() const handleLanguageChange = (value: TLanguage) => { i18n.changeLanguage(value) @@ -66,19 +63,6 @@ const GeneralSettingsPage = forwardRef(({ index }: { index?: number }, ref) => { - - - -
) diff --git a/src/providers/NotificationProvider.tsx b/src/providers/NotificationProvider.tsx index 0dce5e44..a56aead4 100644 --- a/src/providers/NotificationProvider.tsx +++ b/src/providers/NotificationProvider.tsx @@ -25,7 +25,7 @@ export const useNotification = () => { export function NotificationProvider({ children }: { children: React.ReactNode }) { const { pubkey, notificationsSeenAt, updateNotificationsSeenAt } = useNostr() - const { isUserTrusted } = useUserTrust() + const { hideUntrustedNotifications, isUserTrusted } = useUserTrust() const { mutePubkeys } = useMuteList() const [newNotificationIds, setNewNotificationIds] = useState(new Set()) const subCloserRef = useRef(null) @@ -66,7 +66,7 @@ export function NotificationProvider({ children }: { children: React.ReactNode } if ( evt.pubkey !== pubkey && !mutePubkeys.includes(evt.pubkey) && - isUserTrusted(evt.pubkey) + (!hideUntrustedNotifications || isUserTrusted(evt.pubkey)) ) { setNewNotificationIds((prev) => new Set([...prev, evt.id])) } diff --git a/src/providers/UserTrustProvider.tsx b/src/providers/UserTrustProvider.tsx index 82ad4826..8b2a0d92 100644 --- a/src/providers/UserTrustProvider.tsx +++ b/src/providers/UserTrustProvider.tsx @@ -1,11 +1,13 @@ import client from '@/services/client.service' +import storage from '@/services/local-storage.service' import { createContext, useCallback, useContext, useEffect, useState } from 'react' import { useNostr } from './NostrProvider' -import storage from '@/services/local-storage.service' type TUserTrustContext = { - enabled: boolean - updateEnabled: (enabled: boolean) => void + hideUntrustedInteractions: boolean + hideUntrustedNotifications: boolean + updateHideUntrustedInteractions: (hide: boolean) => void + updateHideUntrustedNotifications: (hide: boolean) => void isUserTrusted: (pubkey: string) => boolean } @@ -23,7 +25,12 @@ const wotSet = new Set() export function UserTrustProvider({ children }: { children: React.ReactNode }) { const { pubkey: currentPubkey } = useNostr() - const [enabled, setEnabled] = useState(storage.getHideUntrustedEvents()) + const [hideUntrustedInteractions, setHideUntrustedInteractions] = useState(() => + storage.getHideUntrustedInteractions() + ) + const [hideUntrustedNotifications, setHideUntrustedNotifications] = useState(() => + storage.getHideUntrustedNotifications() + ) useEffect(() => { if (!currentPubkey) return @@ -43,19 +50,32 @@ export function UserTrustProvider({ children }: { children: React.ReactNode }) { const isUserTrusted = useCallback( (pubkey: string) => { - if (!currentPubkey || !enabled) return true + if (!currentPubkey) return true return wotSet.has(pubkey) }, - [currentPubkey, enabled] + [currentPubkey] ) - const updateEnabled = (enabled: boolean) => { - setEnabled(enabled) - storage.setHideUntrustedEvents(enabled) + const updateHideUntrustedInteractions = (hide: boolean) => { + setHideUntrustedInteractions(hide) + storage.setHideUntrustedInteractions(hide) + } + + const updateHideUntrustedNotifications = (hide: boolean) => { + setHideUntrustedNotifications(hide) + storage.setHideUntrustedNotifications(hide) } return ( - + {children} ) diff --git a/src/services/local-storage.service.ts b/src/services/local-storage.service.ts index b562ec78..bce1cb5f 100644 --- a/src/services/local-storage.service.ts +++ b/src/services/local-storage.service.ts @@ -25,7 +25,8 @@ class LocalStorageService { private accountFeedInfoMap: Record = {} private mediaUploadService: string = DEFAULT_NIP_96_SERVICE private autoplay: boolean = true - private hideUntrustedEvents: boolean = true + private hideUntrustedInteractions: boolean = false + private hideUntrustedNotifications: boolean = false constructor() { if (!LocalStorageService.instance) { @@ -93,8 +94,20 @@ class LocalStorageService { this.autoplay = window.localStorage.getItem(StorageKey.AUTOPLAY) !== 'false' - this.hideUntrustedEvents = - window.localStorage.getItem(StorageKey.HIDE_UNTRUSTED_EVENTS) !== 'false' + const hideUntrustedEvents = + window.localStorage.getItem(StorageKey.HIDE_UNTRUSTED_EVENTS) === 'true' + const storedHideUntrustedInteractions = window.localStorage.getItem( + StorageKey.HIDE_UNTRUSTED_INTERACTIONS + ) + const storedHideUntrustedNotifications = window.localStorage.getItem( + StorageKey.HIDE_UNTRUSTED_NOTIFICATIONS + ) + this.hideUntrustedInteractions = storedHideUntrustedInteractions + ? storedHideUntrustedInteractions === 'true' + : hideUntrustedEvents + this.hideUntrustedNotifications = storedHideUntrustedNotifications + ? storedHideUntrustedNotifications === 'true' + : hideUntrustedEvents // Clean up deprecated data window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP) @@ -252,13 +265,28 @@ class LocalStorageService { window.localStorage.setItem(StorageKey.AUTOPLAY, autoplay.toString()) } - getHideUntrustedEvents() { - return this.hideUntrustedEvents + getHideUntrustedInteractions() { + return this.hideUntrustedInteractions } - setHideUntrustedEvents(hide: boolean) { - this.hideUntrustedEvents = hide - window.localStorage.setItem(StorageKey.HIDE_UNTRUSTED_EVENTS, hide.toString()) + setHideUntrustedInteractions(hideUntrustedInteractions: boolean) { + this.hideUntrustedInteractions = hideUntrustedInteractions + window.localStorage.setItem( + StorageKey.HIDE_UNTRUSTED_INTERACTIONS, + hideUntrustedInteractions.toString() + ) + } + + getHideUntrustedNotifications() { + return this.hideUntrustedNotifications + } + + setHideUntrustedNotifications(hideUntrustedNotifications: boolean) { + this.hideUntrustedNotifications = hideUntrustedNotifications + window.localStorage.setItem( + StorageKey.HIDE_UNTRUSTED_NOTIFICATIONS, + hideUntrustedNotifications.toString() + ) } }