feat: hide notifications from untrusted users

This commit is contained in:
codytseng
2025-06-01 23:00:01 +08:00
parent 587038d51a
commit c17d1b8ab5
18 changed files with 81 additions and 33 deletions

View File

@@ -5,6 +5,7 @@ import { usePrimaryPage } from '@/PageManager'
import { useNostr } from '@/providers/NostrProvider'
import { useNoteStats } from '@/providers/NoteStatsProvider'
import { useNotification } from '@/providers/NotificationProvider'
import { useUserTrust } from '@/providers/UserTrustProvider'
import client from '@/services/client.service'
import { TNotificationType } from '@/types'
import dayjs from 'dayjs'
@@ -22,6 +23,7 @@ const NotificationList = forwardRef((_, ref) => {
const { t } = useTranslation()
const { current } = usePrimaryPage()
const { pubkey } = useNostr()
const { enabled: hideUntrustedEvents, isUserTrusted } = useUserTrust()
const { clearNewNotifications, getNotificationsSeenAt } = useNotification()
const { updateNoteStatsByEvents } = useNoteStats()
const [notificationType, setNotificationType] = useState<TNotificationType>('all')
@@ -122,7 +124,9 @@ const NotificationList = forwardRef((_, ref) => {
}, [pubkey, refreshCount, filterKinds, current])
useEffect(() => {
const visibleNotifications = notifications.slice(0, showCount)
const visibleNotifications = notifications
.slice(0, showCount)
.filter((event) => isUserTrusted(event.pubkey))
const index = visibleNotifications.findIndex((event) => event.created_at <= lastReadTime)
if (index === -1) {
setNewNotifications(visibleNotifications)
@@ -131,7 +135,7 @@ const NotificationList = forwardRef((_, ref) => {
setNewNotifications(visibleNotifications.slice(0, index))
setOldNotifications(visibleNotifications.slice(index))
}
}, [notifications, lastReadTime, showCount])
}, [notifications, lastReadTime, showCount, hideUntrustedEvents])
useEffect(() => {
const options = {

View File

@@ -14,7 +14,7 @@ export const StorageKey = {
ACCOUNT_FEED_INFO_MAP: 'accountFeedInfoMap',
MEDIA_UPLOAD_SERVICE: 'mediaUploadService',
AUTOPLAY: 'autoplay',
HIDE_UNTRUSTED_REPLIES: 'hideUntrustedReplies',
HIDE_UNTRUSTED_EVENTS: 'hideUntrustedEvents',
ACCOUNT_RELAY_LIST_EVENT_MAP: 'accountRelayListEventMap', // deprecated
ACCOUNT_FOLLOW_LIST_EVENT_MAP: 'accountFollowListEventMap', // deprecated
ACCOUNT_MUTE_LIST_EVENT_MAP: 'accountMuteListEventMap', // deprecated

View File

@@ -233,6 +233,9 @@ export default {
'Platinum Sponsors': 'الرعاة البلاتينيون',
From: 'من',
'Comment on': 'تعليق على',
'View on njump.me': 'عرض على njump.me'
'View on njump.me': 'عرض على njump.me',
'Hide content from untrusted users': 'إخفاء المحتوى من المستخدمين غير الموثوقين',
'Only show content from your followed users and the users they follow':
'فقط عرض المحتوى من المستخدمين الذين تتابعهم والمستخدمين الذين يتابعونهم'
}
}

View File

@@ -239,6 +239,10 @@ export default {
'Platinum Sponsors': 'Platin-Sponsoren',
From: 'Von',
'Comment on': 'Kommentar zu',
'View on njump.me': 'Auf njump.me ansehen'
'View on njump.me': 'Auf njump.me ansehen',
'Hide content from untrusted users':
'Inhalte von nicht vertrauenswürdigen Benutzern ausblenden',
'Only show content from your followed users and the users they follow':
'Nur Inhalte von Benutzern anzeigen, denen du folgst und die sie folgen'
}
}

View File

@@ -233,6 +233,9 @@ export default {
'Platinum Sponsors': 'Platinum Sponsors',
From: 'From',
'Comment on': 'Comment on',
'View on njump.me': 'View on njump.me'
'View on njump.me': 'View on njump.me',
'Hide content from untrusted users': 'Hide content from untrusted users',
'Only show content from your followed users and the users they follow':
'Only show content from your followed users and the users they follow'
}
}

View File

@@ -238,6 +238,9 @@ export default {
'Platinum Sponsors': 'Patrocinadores Platino',
From: 'De',
'Comment on': 'Comentar en',
'View on njump.me': 'Ver en njump.me'
'View on njump.me': 'Ver en njump.me',
'Hide content from untrusted users': 'Ocultar contenido de usuarios no confiables',
'Only show content from your followed users and the users they follow':
'Solo mostrar contenido de tus usuarios seguidos y los usuarios que ellos siguen'
}
}

View File

@@ -238,6 +238,9 @@ export default {
'Platinum Sponsors': 'Sponsors Platine',
From: 'De',
'Comment on': 'Commenter sur',
'View on njump.me': 'Voir sur njump.me'
'View on njump.me': 'Voir sur njump.me',
'Hide content from untrusted users': 'Hider le contenu des utilisateurs non fiables',
'Only show content from your followed users and the users they follow':
'Afficher uniquement le contenu de vos utilisateurs suivis et des utilisateurs quils suivent'
}
}

View File

@@ -237,6 +237,9 @@ export default {
'Platinum Sponsors': 'Sponsor Platino',
From: 'Da',
'Comment on': 'Commenta su',
'View on njump.me': 'Visualizza su njump.me'
'View on njump.me': 'Visualizza su njump.me',
'Hide content from untrusted users': 'Nascondi contenuti da utenti non fidati',
'Only show content from your followed users and the users they follow':
'Mostra solo contenuti dai tuoi utenti seguiti e dagli utenti che seguono'
}
}

View File

@@ -234,6 +234,9 @@ export default {
'Platinum Sponsors': 'プラチナスポンサー',
From: 'から',
'Comment on': 'にコメント',
'View on njump.me': 'njump.meで表示'
'View on njump.me': 'njump.meで表示',
'Hide content from untrusted users': '信頼できないユーザーのコンテンツを非表示',
'Only show content from your followed users and the users they follow':
'フォローしているユーザーとそのユーザーがフォローしているユーザーのコンテンツのみを表示'
}
}

View File

@@ -236,6 +236,9 @@ export default {
'Platinum Sponsors': 'Sponsorzy Platynowi',
From: 'Od',
'Comment on': 'Komentarz do',
'View on njump.me': 'Zobacz na njump.me'
'View on njump.me': 'Zobacz na njump.me',
'Hide content from untrusted users': 'Ukryj treści od nieznanych użytkowników',
'Only show content from your followed users and the users they follow':
'Pokaż tylko treści od użytkowników, których obserwujesz i ich obserwowanych'
}
}

View File

@@ -236,6 +236,9 @@ export default {
'Platinum Sponsors': 'Patrocinadores Platinum',
From: 'Fonte',
'Comment on': 'Comentando',
'View on njump.me': 'Ver em njump.me'
'View on njump.me': 'Ver em njump.me',
'Hide content from untrusted users': 'Ocultar conteúdo de usuários não confiáveis',
'Only show content from your followed users and the users they follow':
'Mostrar apenas conteúdo dos usuários que você segue e dos usuários que eles seguem'
}
}

View File

@@ -237,6 +237,9 @@ export default {
'Platinum Sponsors': 'Patrocinadores Platinum',
From: 'De',
'Comment on': 'Comentar em',
'View on njump.me': 'Ver em njump.me'
'View on njump.me': 'Ver em njump.me',
'Hide content from untrusted users': 'Esconder conteúdo de usuários não confiáveis',
'Only show content from your followed users and the users they follow':
'Mostrar apenas conteúdo dos usuários que você segue e dos usuários que eles seguem'
}
}

View File

@@ -237,6 +237,9 @@ export default {
'Platinum Sponsors': 'Платиновые спонсоры',
From: 'От',
'Comment on': 'Прокомментировать',
'View on njump.me': 'Посмотреть на njump.me'
'View on njump.me': 'Посмотреть на njump.me',
'Hide content from untrusted users': 'Скрыть контент от недоверенных пользователей',
'Only show content from your followed users and the users they follow':
'Показывать только контент от пользователей, на которых вы подписаны, и от пользователей, на которых они подписаны'
}
}

View File

@@ -234,6 +234,9 @@ export default {
'Platinum Sponsors': '白金赞助商',
From: '来自',
'Comment on': '评论于',
'View on njump.me': '在 njump.me 上查看'
'View on njump.me': '在 njump.me 上查看',
'Hide content from untrusted users': '隐藏不受信任用户的内容',
'Only show content from your followed users and the users they follow':
'仅显示您关注的用户及其关注的用户的内容'
}
}

View File

@@ -16,7 +16,7 @@ const GeneralSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
const [language, setLanguage] = useState<TLanguage>(i18n.language as TLanguage)
const { themeSetting, setThemeSetting } = useTheme()
const { autoplay, setAutoplay } = useAutoplay()
const { enabled: hideUntrustedRepliesEnabled, updateEnabled: updateHideUntrustedRepliesEnabled } =
const { enabled: hideUntrustedEventsEnabled, updateEnabled: updateHideUntrustedEventsEnabled } =
useUserTrust()
const handleLanguageChange = (value: TLanguage) => {
@@ -67,16 +67,16 @@ const GeneralSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
<Switch id="autoplay" checked={autoplay} onCheckedChange={setAutoplay} />
</SettingItem>
<SettingItem>
<Label htmlFor="hide-untrusted-replies" className="text-base font-normal">
{t('Hide replies from untrusted users')}
<Label htmlFor="hide-untrusted-events" className="text-base font-normal">
{t('Hide content from untrusted users')}
<div className="text-muted-foreground">
{t('Only show replies from your followed users and the users they follow')}
{t('Only show content from your followed users and the users they follow')}
</div>
</Label>
<Switch
id="hide-untrusted-replies"
checked={hideUntrustedRepliesEnabled}
onCheckedChange={updateHideUntrustedRepliesEnabled}
id="hide-untrusted-events"
checked={hideUntrustedEventsEnabled}
onCheckedChange={updateHideUntrustedEventsEnabled}
/>
</SettingItem>
</div>

View File

@@ -5,6 +5,7 @@ import { SubCloser } from 'nostr-tools/abstract-pool'
import { createContext, useContext, useEffect, useRef, useState } from 'react'
import { useMuteList } from './MuteListProvider'
import { useNostr } from './NostrProvider'
import { useUserTrust } from './UserTrustProvider'
type TNotificationContext = {
hasNewNotification: boolean
@@ -24,6 +25,7 @@ export const useNotification = () => {
export function NotificationProvider({ children }: { children: React.ReactNode }) {
const { pubkey, notificationsSeenAt, updateNotificationsSeenAt } = useNostr()
const { isUserTrusted } = useUserTrust()
const { mutePubkeys } = useMuteList()
const [newNotificationIds, setNewNotificationIds] = useState(new Set<string>())
const subCloserRef = useRef<SubCloser | null>(null)
@@ -61,7 +63,11 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
{
onevent: (evt) => {
// Only show notification if not from self and not muted
if (evt.pubkey !== pubkey && !mutePubkeys.includes(evt.pubkey)) {
if (
evt.pubkey !== pubkey &&
!mutePubkeys.includes(evt.pubkey) &&
isUserTrusted(evt.pubkey)
) {
setNewNotificationIds((prev) => new Set([...prev, evt.id]))
}
},

View File

@@ -23,7 +23,7 @@ const wotSet = new Set<string>()
export function UserTrustProvider({ children }: { children: React.ReactNode }) {
const { pubkey: currentPubkey } = useNostr()
const [enabled, setEnabled] = useState(storage.getHideUntrustedReplies())
const [enabled, setEnabled] = useState(storage.getHideUntrustedEvents())
useEffect(() => {
if (!currentPubkey) return
@@ -43,7 +43,7 @@ export function UserTrustProvider({ children }: { children: React.ReactNode }) {
const updateEnabled = (enabled: boolean) => {
setEnabled(enabled)
storage.setHideUntrustedReplies(enabled)
storage.setHideUntrustedEvents(enabled)
}
const isUserTrusted = (pubkey: string) => {

View File

@@ -25,7 +25,7 @@ class LocalStorageService {
private accountFeedInfoMap: Record<string, TFeedInfo | undefined> = {}
private mediaUploadService: string = DEFAULT_NIP_96_SERVICE
private autoplay: boolean = true
private hideUntrustedReplies: boolean = true
private hideUntrustedEvents: boolean = true
constructor() {
if (!LocalStorageService.instance) {
@@ -93,8 +93,8 @@ class LocalStorageService {
this.autoplay = window.localStorage.getItem(StorageKey.AUTOPLAY) !== 'false'
this.hideUntrustedReplies =
window.localStorage.getItem(StorageKey.HIDE_UNTRUSTED_REPLIES) !== 'false'
this.hideUntrustedEvents =
window.localStorage.getItem(StorageKey.HIDE_UNTRUSTED_EVENTS) !== 'false'
// Clean up deprecated data
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)
@@ -252,12 +252,13 @@ class LocalStorageService {
window.localStorage.setItem(StorageKey.AUTOPLAY, autoplay.toString())
}
getHideUntrustedReplies() {
return this.hideUntrustedReplies
getHideUntrustedEvents() {
return this.hideUntrustedEvents
}
setHideUntrustedReplies(hide: boolean) {
this.hideUntrustedReplies = hide
window.localStorage.setItem(StorageKey.HIDE_UNTRUSTED_REPLIES, hide.toString())
setHideUntrustedEvents(hide: boolean) {
this.hideUntrustedEvents = hide
window.localStorage.setItem(StorageKey.HIDE_UNTRUSTED_EVENTS, hide.toString())
}
}