feat: support hiding indirect notifications

This commit is contained in:
codytseng 2026-01-20 22:47:42 +08:00
parent 331811f683
commit 2cd1ae481b
27 changed files with 196 additions and 38 deletions

View file

@ -1,13 +1,16 @@
import ParentNotePreview from '@/components/ParentNotePreview' import ParentNotePreview from '@/components/ParentNotePreview'
import { NOTIFICATION_LIST_STYLE } from '@/constants' import { ExtendedKind, NOTIFICATION_LIST_STYLE } from '@/constants'
import { getEmbeddedPubkeys, getParentStuff } from '@/lib/event' import { getEmbeddedPubkeys, getParentStuff, getParentTag } from '@/lib/event'
import { toExternalContent, toNote } from '@/lib/link' import { toExternalContent, toNote } from '@/lib/link'
import { generateBech32IdFromETag, tagNameEquals } from '@/lib/tag'
import { useSecondaryPage } from '@/PageManager' import { useSecondaryPage } from '@/PageManager'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { useNotificationUserPreference } from '@/providers/NotificationUserPreferenceProvider'
import { useUserPreferences } from '@/providers/UserPreferencesProvider' import { useUserPreferences } from '@/providers/UserPreferencesProvider'
import client from '@/services/client.service'
import { AtSign, MessageCircle, Quote } from 'lucide-react' import { AtSign, MessageCircle, Quote } from 'lucide-react'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useMemo } from 'react' import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Notification from './Notification' import Notification from './Notification'
@ -22,6 +25,7 @@ export function MentionNotification({
const { push } = useSecondaryPage() const { push } = useSecondaryPage()
const { pubkey } = useNostr() const { pubkey } = useNostr()
const { notificationListStyle } = useUserPreferences() const { notificationListStyle } = useUserPreferences()
const { hideIndirect } = useNotificationUserPreference()
const isMention = useMemo(() => { const isMention = useMemo(() => {
if (!pubkey) return false if (!pubkey) return false
const mentions = getEmbeddedPubkeys(notification) const mentions = getEmbeddedPubkeys(notification)
@ -30,6 +34,49 @@ export function MentionNotification({
const { parentEventId, parentExternalContent } = useMemo(() => { const { parentEventId, parentExternalContent } = useMemo(() => {
return getParentStuff(notification) return getParentStuff(notification)
}, [notification]) }, [notification])
const [isDirectMention, setIsDirectMention] = useState(false)
useEffect(() => {
const checkIsDirectMention = async () => {
if (!pubkey) return false
if (isMention) return true
if (notification.kind === ExtendedKind.POLL) return true
if (
notification.kind === ExtendedKind.VOICE_COMMENT ||
notification.kind === ExtendedKind.COMMENT
) {
const parentPTag = notification.tags.findLast(tagNameEquals('p'))
const parentPubkey = parentPTag?.[1]
return parentPubkey === pubkey
}
const parentTag = getParentTag(notification)
if (parentTag?.type === 'e') {
const [, , , , parentPubkey] = parentTag.tag
if (parentPubkey) {
return parentPubkey === pubkey
}
const parentEventId = generateBech32IdFromETag(parentTag.tag)
if (!parentEventId) return false
const parentEvent = await client.fetchEvent(parentEventId)
if (parentEvent) {
return parentEvent.pubkey === pubkey
}
return false
}
if (parentTag?.type === 'a') {
const coordinate = parentTag.tag[1]
const [, parentPubkey] = coordinate.split(':')
return parentPubkey === pubkey
}
return false
}
checkIsDirectMention().then(setIsDirectMention)
}, [pubkey, notification, isMention])
if (hideIndirect && !isDirectMention) {
return null
}
return ( return (
<Notification <Notification

View file

@ -2,6 +2,7 @@ import Image from '@/components/Image'
import { useFetchEvent } from '@/hooks' import { useFetchEvent } from '@/hooks'
import { generateBech32IdFromATag, generateBech32IdFromETag, tagNameEquals } from '@/lib/tag' import { generateBech32IdFromATag, generateBech32IdFromETag, tagNameEquals } from '@/lib/tag'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { useNotificationUserPreference } from '@/providers/NotificationUserPreferenceProvider'
import { Heart } from 'lucide-react' import { Heart } from 'lucide-react'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useMemo } from 'react' import { useMemo } from 'react'
@ -17,6 +18,7 @@ export function ReactionNotification({
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const { pubkey } = useNostr() const { pubkey } = useNostr()
const { hideIndirect } = useNotificationUserPreference()
const eventId = useMemo(() => { const eventId = useMemo(() => {
const aTag = notification.tags.findLast(tagNameEquals('a')) const aTag = notification.tags.findLast(tagNameEquals('a'))
if (aTag) { if (aTag) {
@ -56,6 +58,9 @@ export function ReactionNotification({
if (!event || !eventId || !reaction) { if (!event || !eventId || !reaction) {
return null return null
} }
if (hideIndirect && event.pubkey !== pubkey) {
return null
}
return ( return (
<Notification <Notification

View file

@ -1,3 +1,5 @@
import { useNostr } from '@/providers/NostrProvider'
import { useNotificationUserPreference } from '@/providers/NotificationUserPreferenceProvider'
import client from '@/services/client.service' import client from '@/services/client.service'
import { Repeat } from 'lucide-react' import { Repeat } from 'lucide-react'
import { Event, validateEvent } from 'nostr-tools' import { Event, validateEvent } from 'nostr-tools'
@ -13,6 +15,8 @@ export function RepostNotification({
isNew?: boolean isNew?: boolean
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const { pubkey } = useNostr()
const { hideIndirect } = useNotificationUserPreference()
const event = useMemo(() => { const event = useMemo(() => {
try { try {
const event = JSON.parse(notification.content) as Event const event = JSON.parse(notification.content) as Event
@ -25,6 +29,9 @@ export function RepostNotification({
} }
}, [notification.content]) }, [notification.content])
if (!event) return null if (!event) return null
if (hideIndirect && event.pubkey !== pubkey) {
return null
}
return ( return (
<Notification <Notification

View file

@ -1,4 +1,4 @@
import { ExtendedKind, NOTIFICATION_LIST_STYLE } from '@/constants' import { ExtendedKind, NOTIFICATION_LIST_STYLE, SPECIAL_TRUST_SCORE_FILTER_ID } from '@/constants'
import { useInfiniteScroll } from '@/hooks' import { useInfiniteScroll } from '@/hooks'
import { compareEvents } from '@/lib/event' import { compareEvents } from '@/lib/event'
import { getDefaultRelayUrls } from '@/lib/relay' import { getDefaultRelayUrls } from '@/lib/relay'
@ -28,6 +28,7 @@ import PullToRefresh from 'react-simple-pull-to-refresh'
import { LoadingBar } from '../LoadingBar' import { LoadingBar } from '../LoadingBar'
import { RefreshButton } from '../RefreshButton' import { RefreshButton } from '../RefreshButton'
import Tabs from '../Tabs' import Tabs from '../Tabs'
import TrustScoreFilter from '../TrustScoreFilter'
import { NotificationItem } from './NotificationItem' import { NotificationItem } from './NotificationItem'
import { NotificationSkeleton } from './NotificationItem/Notification' import { NotificationSkeleton } from './NotificationItem/Notification'
@ -280,7 +281,12 @@ const NotificationList = forwardRef((_, ref) => {
setShowCount(SHOW_COUNT) setShowCount(SHOW_COUNT)
setNotificationType(type as TNotificationType) setNotificationType(type as TNotificationType)
}} }}
options={!supportTouch ? <RefreshButton onClick={() => refresh()} /> : null} options={
<>
{!supportTouch ? <RefreshButton onClick={() => refresh()} /> : null}
<TrustScoreFilter filterId={SPECIAL_TRUST_SCORE_FILTER_ID.NOTIFICATIONS} />
</>
}
/> />
<div ref={topRef} className="scroll-mt-[calc(6rem+1px)]" /> <div ref={topRef} className="scroll-mt-[calc(6rem+1px)]" />
{supportTouch ? ( {supportTouch ? (

View file

@ -45,6 +45,7 @@ export const StorageKey = {
MUTED_WORDS: 'mutedWords', MUTED_WORDS: 'mutedWords',
MIN_TRUST_SCORE: 'minTrustScore', MIN_TRUST_SCORE: 'minTrustScore',
MIN_TRUST_SCORE_MAP: 'minTrustScoreMap', MIN_TRUST_SCORE_MAP: 'minTrustScoreMap',
HIDE_INDIRECT_NOTIFICATIONS: 'hideIndirectNotifications',
ENABLE_LIVE_FEED: 'enableLiveFeed', // deprecated ENABLE_LIVE_FEED: 'enableLiveFeed', // deprecated
HIDE_UNTRUSTED_NOTES: 'hideUntrustedNotes', // deprecated HIDE_UNTRUSTED_NOTES: 'hideUntrustedNotes', // deprecated
HIDE_UNTRUSTED_INTERACTIONS: 'hideUntrustedInteractions', // deprecated HIDE_UNTRUSTED_INTERACTIONS: 'hideUntrustedInteractions', // deprecated

View file

@ -665,6 +665,7 @@ export default {
'Zap Details': 'تفاصيل Zap', 'Zap Details': 'تفاصيل Zap',
'Default trust score filter threshold ({{n}}%)': 'عتبة مرشح درجة الثقة الافتراضية ({{n}}%)', 'Default trust score filter threshold ({{n}}%)': 'عتبة مرشح درجة الثقة الافتراضية ({{n}}%)',
'No notes found': 'لم يتم العثور على ملاحظات', 'No notes found': 'لم يتم العثور على ملاحظات',
'Try again later or check your connection': 'حاول مرة أخرى لاحقًا أو تحقق من اتصالك' 'Try again later or check your connection': 'حاول مرة أخرى لاحقًا أو تحقق من اتصالك',
'Hide indirect': 'إخفاء غير المباشرة'
} }
} }

View file

@ -688,6 +688,8 @@ export default {
'Default trust score filter threshold ({{n}}%)': 'Default trust score filter threshold ({{n}}%)':
'Standard-Vertrauenswert-Filter-Schwelle ({{n}}%)', 'Standard-Vertrauenswert-Filter-Schwelle ({{n}}%)',
'No notes found': 'Keine Notizen gefunden', 'No notes found': 'Keine Notizen gefunden',
'Try again later or check your connection': 'Versuchen Sie es später erneut oder überprüfen Sie Ihre Verbindung' 'Try again later or check your connection':
'Versuchen Sie es später erneut oder überprüfen Sie Ihre Verbindung',
'Hide indirect': 'Indirekte ausblenden'
} }
} }

View file

@ -668,8 +668,10 @@ export default {
'Muted words': 'Muted words', 'Muted words': 'Muted words',
'Add muted word': 'Add muted word', 'Add muted word': 'Add muted word',
'Zap Details': 'Zap Details', 'Zap Details': 'Zap Details',
'Default trust score filter threshold ({{n}}%)': 'Default trust score filter threshold ({{n}}%)', 'Default trust score filter threshold ({{n}}%)':
'Default trust score filter threshold ({{n}}%)',
'No notes found': 'No notes found', 'No notes found': 'No notes found',
'Try again later or check your connection': 'Try again later or check your connection' 'Try again later or check your connection': 'Try again later or check your connection',
'Hide indirect': 'Hide indirect'
} }
} }

View file

@ -682,6 +682,7 @@ export default {
'Default trust score filter threshold ({{n}}%)': 'Default trust score filter threshold ({{n}}%)':
'Umbral predeterminado del filtro de puntuación de confianza ({{n}}%)', 'Umbral predeterminado del filtro de puntuación de confianza ({{n}}%)',
'No notes found': 'No se encontraron notas', 'No notes found': 'No se encontraron notas',
'Try again later or check your connection': 'Inténtalo más tarde o verifica tu conexión' 'Try again later or check your connection': 'Inténtalo más tarde o verifica tu conexión',
'Hide indirect': 'Ocultar indirectas'
} }
} }

View file

@ -676,6 +676,8 @@ export default {
'Zap Details': 'جزئیات زپ', 'Zap Details': 'جزئیات زپ',
'Default trust score filter threshold ({{n}}%)': 'آستانه فیلتر امتیاز اعتماد پیش‌فرض ({{n}}%)', 'Default trust score filter threshold ({{n}}%)': 'آستانه فیلتر امتیاز اعتماد پیش‌فرض ({{n}}%)',
'No notes found': 'یادداشتی یافت نشد', 'No notes found': 'یادداشتی یافت نشد',
'Try again later or check your connection': 'بعداً دوباره امتحان کنید یا اتصال خود را بررسی کنید' 'Try again later or check your connection':
'بعداً دوباره امتحان کنید یا اتصال خود را بررسی کنید',
'Hide indirect': 'پنهان کردن غیرمستقیم'
} }
} }

View file

@ -686,6 +686,7 @@ export default {
'Default trust score filter threshold ({{n}}%)': 'Default trust score filter threshold ({{n}}%)':
'Seuil par défaut du filtre de score de confiance ({{n}}%)', 'Seuil par défaut du filtre de score de confiance ({{n}}%)',
'No notes found': 'Aucune note trouvée', 'No notes found': 'Aucune note trouvée',
'Try again later or check your connection': 'Réessayez plus tard ou vérifiez votre connexion' 'Try again later or check your connection': 'Réessayez plus tard ou vérifiez votre connexion',
'Hide indirect': 'Masquer indirects'
} }
} }

View file

@ -677,6 +677,7 @@ export default {
'Zap Details': 'जैप विवरण', 'Zap Details': 'जैप विवरण',
'Default trust score filter threshold ({{n}}%)': 'डिफ़ॉल्ट विश्वास स्कोर फ़िल्टर सीमा ({{n}}%)', 'Default trust score filter threshold ({{n}}%)': 'डिफ़ॉल्ट विश्वास स्कोर फ़िल्टर सीमा ({{n}}%)',
'No notes found': 'कोई नोट्स नहीं मिले', 'No notes found': 'कोई नोट्स नहीं मिले',
'Try again later or check your connection': 'बाद में पुनः प्रयास करें या अपना कनेक्शन जाँचें' 'Try again later or check your connection': 'बाद में पुनः प्रयास करें या अपना कनेक्शन जाँचें',
'Hide indirect': 'अप्रत्यक्ष छुपाएं'
} }
} }

View file

@ -671,6 +671,7 @@ export default {
'Default trust score filter threshold ({{n}}%)': 'Default trust score filter threshold ({{n}}%)':
'Alapértelmezett bizalmi pontszám szűrő küszöbérték ({{n}}%)', 'Alapértelmezett bizalmi pontszám szűrő küszöbérték ({{n}}%)',
'No notes found': 'Nem található jegyzet', 'No notes found': 'Nem található jegyzet',
'Try again later or check your connection': 'Próbáld újra később vagy ellenőrizd a kapcsolatot' 'Try again later or check your connection': 'Próbáld újra később vagy ellenőrizd a kapcsolatot',
'Hide indirect': 'Közvetettek elrejtése'
} }
} }

View file

@ -682,6 +682,7 @@ export default {
'Default trust score filter threshold ({{n}}%)': 'Default trust score filter threshold ({{n}}%)':
'Soglia predefinita del filtro del punteggio di fiducia ({{n}}%)', 'Soglia predefinita del filtro del punteggio di fiducia ({{n}}%)',
'No notes found': 'Nessuna nota trovata', 'No notes found': 'Nessuna nota trovata',
'Try again later or check your connection': 'Riprova più tardi o controlla la connessione' 'Try again later or check your connection': 'Riprova più tardi o controlla la connessione',
'Hide indirect': 'Nascondi indirette'
} }
} }

View file

@ -673,8 +673,11 @@ export default {
'Muted words': 'ミュートワード', 'Muted words': 'ミュートワード',
'Add muted word': 'ミュートワードを追加', 'Add muted word': 'ミュートワードを追加',
'Zap Details': 'Zapの詳細', 'Zap Details': 'Zapの詳細',
'Default trust score filter threshold ({{n}}%)': 'デフォルトの信頼スコアフィルター閾値 ({{n}}%)', 'Default trust score filter threshold ({{n}}%)':
'デフォルトの信頼スコアフィルター閾値 ({{n}}%)',
'No notes found': 'ノートが見つかりません', 'No notes found': 'ノートが見つかりません',
'Try again later or check your connection': '後でもう一度お試しいただくか、接続を確認してください' 'Try again later or check your connection':
'後でもう一度お試しいただくか、接続を確認してください',
'Hide indirect': '間接通知を非表示'
} }
} }

View file

@ -671,6 +671,7 @@ export default {
'Zap Details': '잽 세부 정보', 'Zap Details': '잽 세부 정보',
'Default trust score filter threshold ({{n}}%)': '기본 신뢰 점수 필터 임계값 ({{n}}%)', 'Default trust score filter threshold ({{n}}%)': '기본 신뢰 점수 필터 임계값 ({{n}}%)',
'No notes found': '노트를 찾을 수 없습니다', 'No notes found': '노트를 찾을 수 없습니다',
'Try again later or check your connection': '나중에 다시 시도하거나 연결을 확인하세요' 'Try again later or check your connection': '나중에 다시 시도하거나 연결을 확인하세요',
'Hide indirect': '간접 숨기기'
} }
} }

View file

@ -680,8 +680,10 @@ export default {
'Muted words': 'Wyciszone słowa', 'Muted words': 'Wyciszone słowa',
'Add muted word': 'Dodaj wyciszone słowo', 'Add muted word': 'Dodaj wyciszone słowo',
'Zap Details': 'Szczegóły zapu', 'Zap Details': 'Szczegóły zapu',
'Default trust score filter threshold ({{n}}%)': 'Domyślny próg filtra wyniku zaufania ({{n}}%)', 'Default trust score filter threshold ({{n}}%)':
'Domyślny próg filtra wyniku zaufania ({{n}}%)',
'No notes found': 'Nie znaleziono notatek', 'No notes found': 'Nie znaleziono notatek',
'Try again later or check your connection': 'Spróbuj ponownie później lub sprawdź połączenie' 'Try again later or check your connection': 'Spróbuj ponownie później lub sprawdź połączenie',
'Hide indirect': 'Ukryj pośrednie'
} }
} }

View file

@ -679,6 +679,8 @@ export default {
'Default trust score filter threshold ({{n}}%)': 'Default trust score filter threshold ({{n}}%)':
'Limite padrão do filtro de pontuação de confiança ({{n}}%)', 'Limite padrão do filtro de pontuação de confiança ({{n}}%)',
'No notes found': 'Nenhuma nota encontrada', 'No notes found': 'Nenhuma nota encontrada',
'Try again later or check your connection': 'Tente novamente mais tarde ou verifique sua conexão' 'Try again later or check your connection':
'Tente novamente mais tarde ou verifique sua conexão',
'Hide indirect': 'Ocultar indiretas'
} }
} }

View file

@ -682,6 +682,8 @@ export default {
'Default trust score filter threshold ({{n}}%)': 'Default trust score filter threshold ({{n}}%)':
'Limite predefinido do filtro de pontuação de confiança ({{n}}%)', 'Limite predefinido do filtro de pontuação de confiança ({{n}}%)',
'No notes found': 'Nenhuma nota encontrada', 'No notes found': 'Nenhuma nota encontrada',
'Try again later or check your connection': 'Tente novamente mais tarde ou verifique a sua ligação' 'Try again later or check your connection':
'Tente novamente mais tarde ou verifique a sua ligação',
'Hide indirect': 'Ocultar indiretas'
} }
} }

View file

@ -682,6 +682,7 @@ export default {
'Default trust score filter threshold ({{n}}%)': 'Default trust score filter threshold ({{n}}%)':
'Порог фильтра рейтинга доверия по умолчанию ({{n}}%)', 'Порог фильтра рейтинга доверия по умолчанию ({{n}}%)',
'No notes found': 'Заметки не найдены', 'No notes found': 'Заметки не найдены',
'Try again later or check your connection': 'Попробуйте позже или проверьте подключение' 'Try again later or check your connection': 'Попробуйте позже или проверьте подключение',
'Hide indirect': 'Скрыть косвенные'
} }
} }

View file

@ -667,6 +667,7 @@ export default {
'Default trust score filter threshold ({{n}}%)': 'Default trust score filter threshold ({{n}}%)':
'เกณฑ์ตัวกรองคะแนนความไว้วางใจเริ่มต้น ({{n}}%)', 'เกณฑ์ตัวกรองคะแนนความไว้วางใจเริ่มต้น ({{n}}%)',
'No notes found': 'ไม่พบโน้ต', 'No notes found': 'ไม่พบโน้ต',
'Try again later or check your connection': 'ลองใหม่ภายหลังหรือตรวจสอบการเชื่อมต่อของคุณ' 'Try again later or check your connection': 'ลองใหม่ภายหลังหรือตรวจสอบการเชื่อมต่อของคุณ',
'Hide indirect': 'ซ่อนทางอ้อม'
} }
} }

View file

@ -649,6 +649,7 @@ export default {
'Zap Details': '打閃詳情', 'Zap Details': '打閃詳情',
'Default trust score filter threshold ({{n}}%)': '預設信任分數過濾閾值 ({{n}}%)', 'Default trust score filter threshold ({{n}}%)': '預設信任分數過濾閾值 ({{n}}%)',
'No notes found': '沒有找到筆記', 'No notes found': '沒有找到筆記',
'Try again later or check your connection': '請稍後重試或檢查網路連接' 'Try again later or check your connection': '請稍後重試或檢查網路連接',
'Hide indirect': '隱藏間接通知'
} }
} }

View file

@ -654,6 +654,7 @@ export default {
'Zap Details': '打闪详情', 'Zap Details': '打闪详情',
'Default trust score filter threshold ({{n}}%)': '默认信任分数过滤阈值 ({{n}}%)', 'Default trust score filter threshold ({{n}}%)': '默认信任分数过滤阈值 ({{n}}%)',
'No notes found': '没有找到笔记', 'No notes found': '没有找到笔记',
'Try again later or check your connection': '请稍后重试或检查网络连接' 'Try again later or check your connection': '请稍后重试或检查网络连接',
'Hide indirect': '隐藏间接通知'
} }
} }

View file

@ -127,7 +127,7 @@ function NoteListPageTitlebar({
layoutRef?.current?.scrollToTop('smooth') layoutRef?.current?.scrollToTop('smooth')
} }
}} }}
className={showRelayDetails ? 'bg-accent/50' : ''} className={showRelayDetails ? 'bg-muted/40' : ''}
> >
<Info /> <Info />
</Button> </Button>

View file

@ -1,15 +1,21 @@
import NotificationList from '@/components/NotificationList' import NotificationList from '@/components/NotificationList'
import TrustScoreFilter from '@/components/TrustScoreFilter' import { Button } from '@/components/ui/button'
import { SPECIAL_TRUST_SCORE_FILTER_ID } from '@/constants'
import PrimaryPageLayout from '@/layouts/PrimaryPageLayout' import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
import { cn } from '@/lib/utils'
import { usePrimaryPage } from '@/PageManager' import { usePrimaryPage } from '@/PageManager'
import {
NotificationUserPreferenceContext,
useNotificationUserPreference
} from '@/providers/NotificationUserPreferenceProvider'
import localStorage from '@/services/local-storage.service'
import { TPageRef } from '@/types' import { TPageRef } from '@/types'
import { Bell } from 'lucide-react' import { Bell } from 'lucide-react'
import { forwardRef, useEffect, useRef } from 'react' import { forwardRef, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
const NotificationListPage = forwardRef<TPageRef>((_, ref) => { const NotificationListPage = forwardRef<TPageRef>((_, ref) => {
const { current } = usePrimaryPage() const { current } = usePrimaryPage()
const [hideIndirect, setHideIndirect] = useState(localStorage.getHideIndirectNotifications())
const firstRenderRef = useRef(true) const firstRenderRef = useRef(true)
const notificationListRef = useRef<{ refresh: () => void }>(null) const notificationListRef = useRef<{ refresh: () => void }>(null)
@ -20,15 +26,30 @@ const NotificationListPage = forwardRef<TPageRef>((_, ref) => {
firstRenderRef.current = false firstRenderRef.current = false
}, [current]) }, [current])
const updateHideIndirect = useCallback(
(enable: boolean) => {
setHideIndirect(enable)
localStorage.setHideIndirectNotifications(enable)
},
[setHideIndirect]
)
return ( return (
<PrimaryPageLayout <NotificationUserPreferenceContext.Provider
ref={ref} value={{
pageName="notifications" hideIndirect,
titlebar={<NotificationListPageTitlebar />} updateHideIndirect
displayScrollToTopButton }}
> >
<NotificationList ref={notificationListRef} /> <PrimaryPageLayout
</PrimaryPageLayout> ref={ref}
pageName="notifications"
titlebar={<NotificationListPageTitlebar />}
displayScrollToTopButton
>
<NotificationList ref={notificationListRef} />
</PrimaryPageLayout>
</NotificationUserPreferenceContext.Provider>
) )
}) })
NotificationListPage.displayName = 'NotificationListPage' NotificationListPage.displayName = 'NotificationListPage'
@ -43,7 +64,25 @@ function NotificationListPageTitlebar() {
<Bell /> <Bell />
<div className="text-lg font-semibold">{t('Notifications')}</div> <div className="text-lg font-semibold">{t('Notifications')}</div>
</div> </div>
<TrustScoreFilter filterId={SPECIAL_TRUST_SCORE_FILTER_ID.NOTIFICATIONS} /> <HideUnrelatedNotificationsToggle />
</div> </div>
) )
} }
function HideUnrelatedNotificationsToggle() {
const { t } = useTranslation()
const { hideIndirect, updateHideIndirect } = useNotificationUserPreference()
return (
<Button
variant="ghost"
className={cn(
'h-10 px-3 shrink-0 rounded-xl [&_svg]:size-5',
hideIndirect ? 'text-foreground bg-muted/40' : 'text-muted-foreground'
)}
onClick={() => updateHideIndirect(!hideIndirect)}
>
{t('Hide indirect')}
</Button>
)
}

View file

@ -0,0 +1,14 @@
import { createContext, useContext } from 'react'
type TNotificationUserPreferenceContext = {
hideIndirect: boolean
updateHideIndirect: (enable: boolean) => void
}
export const NotificationUserPreferenceContext =
createContext<TNotificationUserPreferenceContext | null>(null)
export function useNotificationUserPreference() {
const ctx = useContext(NotificationUserPreferenceContext)
return ctx ?? { hideIndirect: false, updateHideIndirect: () => {} }
}

View file

@ -68,6 +68,7 @@ class LocalStorageService {
private mutedWords: string[] = [] private mutedWords: string[] = []
private minTrustScore: number = 0 private minTrustScore: number = 0
private minTrustScoreMap: Record<string, number> = {} private minTrustScoreMap: Record<string, number> = {}
private hideIndirectNotifications: boolean = false
constructor() { constructor() {
if (!LocalStorageService.instance) { if (!LocalStorageService.instance) {
@ -319,6 +320,9 @@ class LocalStorageService {
} }
} }
this.hideIndirectNotifications =
window.localStorage.getItem(StorageKey.HIDE_INDIRECT_NOTIFICATIONS) === 'true'
// Clean up deprecated data // Clean up deprecated data
window.localStorage.removeItem(StorageKey.PINNED_PUBKEYS) window.localStorage.removeItem(StorageKey.PINNED_PUBKEYS)
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP) window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)
@ -684,6 +688,15 @@ class LocalStorageService {
this.mutedWords = words this.mutedWords = words
window.localStorage.setItem(StorageKey.MUTED_WORDS, JSON.stringify(this.mutedWords)) window.localStorage.setItem(StorageKey.MUTED_WORDS, JSON.stringify(this.mutedWords))
} }
getHideIndirectNotifications() {
return this.hideIndirectNotifications
}
setHideIndirectNotifications(onlyShow: boolean) {
this.hideIndirectNotifications = onlyShow
window.localStorage.setItem(StorageKey.HIDE_INDIRECT_NOTIFICATIONS, onlyShow.toString())
}
} }
const instance = new LocalStorageService() const instance = new LocalStorageService()