feat: hide content mentioning muted users (#524)

Co-authored-by: mleku <me@mleku.dev>
This commit is contained in:
Cody Tseng
2025-09-02 22:18:34 +08:00
committed by GitHub
parent d3578184fb
commit 3c657dfa8c
37 changed files with 289 additions and 83 deletions

View File

@@ -1,5 +1,7 @@
import { ExtendedKind } from '@/constants' import { ExtendedKind } from '@/constants'
import { isMentioningMutedUsers } from '@/lib/event'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useMuteList } from '@/providers/MuteListProvider' import { useMuteList } from '@/providers/MuteListProvider'
import { Event, kinds } from 'nostr-tools' import { Event, kinds } from 'nostr-tools'
import { useMemo } from 'react' import { useMemo } from 'react'
@@ -22,10 +24,18 @@ export default function ContentPreview({
className?: string className?: string
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const { mutePubkeys } = useMuteList() const { mutePubkeySet } = useMuteList()
const { hideContentMentioningMutedUsers } = useContentPolicy()
const isMuted = useMemo( const isMuted = useMemo(
() => (event ? mutePubkeys.includes(event.pubkey) : false), () => (event ? mutePubkeySet.has(event.pubkey) : false),
[mutePubkeys, event] [mutePubkeySet, event]
)
const isMentioningMuted = useMemo(
() =>
hideContentMentioningMutedUsers && event
? isMentioningMutedUsers(event, mutePubkeySet)
: false,
[event, mutePubkeySet]
) )
if (!event) { if (!event) {
@@ -38,6 +48,14 @@ export default function ContentPreview({
) )
} }
if (isMentioningMuted) {
return (
<div className={cn('pointer-events-none', className)}>
[{t('This note mentions a user you muted')}]
</div>
)
}
if ( if (
[ [
kinds.ShortTextNote, kinds.ShortTextNote,

View File

@@ -18,10 +18,10 @@ export default function MuteButton({ pubkey }: { pubkey: string }) {
const { t } = useTranslation() const { t } = useTranslation()
const { isSmallScreen } = useScreenSize() const { isSmallScreen } = useScreenSize()
const { pubkey: accountPubkey, checkLogin } = useNostr() const { pubkey: accountPubkey, checkLogin } = useNostr()
const { mutePubkeys, changing, mutePubkeyPrivately, mutePubkeyPublicly, unmutePubkey } = const { mutePubkeySet, changing, mutePubkeyPrivately, mutePubkeyPublicly, unmutePubkey } =
useMuteList() useMuteList()
const [updating, setUpdating] = useState(false) const [updating, setUpdating] = useState(false)
const isMuted = useMemo(() => mutePubkeys.includes(pubkey), [mutePubkeys, pubkey]) const isMuted = useMemo(() => mutePubkeySet.has(pubkey), [mutePubkeySet, pubkey])
if (!accountPubkey || (pubkey && pubkey === accountPubkey)) return null if (!accountPubkey || (pubkey && pubkey === accountPubkey)) return null

View File

@@ -54,7 +54,7 @@ export default function Note({
const usingClient = useMemo(() => getUsingClient(event), [event]) const usingClient = useMemo(() => getUsingClient(event), [event])
const { defaultShowNsfw } = useContentPolicy() const { defaultShowNsfw } = useContentPolicy()
const [showNsfw, setShowNsfw] = useState(false) const [showNsfw, setShowNsfw] = useState(false)
const { mutePubkeys } = useMuteList() const { mutePubkeySet } = useMuteList()
const [showMuted, setShowMuted] = useState(false) const [showMuted, setShowMuted] = useState(false)
let content: React.ReactNode let content: React.ReactNode
@@ -67,7 +67,7 @@ export default function Note({
].includes(event.kind) ].includes(event.kind)
) { ) {
content = <UnknownNote className="mt-2" event={event} /> content = <UnknownNote className="mt-2" event={event} />
} else if (mutePubkeys.includes(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 (!defaultShowNsfw && isNsfwEvent(event) && !showNsfw) {
content = <NsfwNote show={() => setShowNsfw(true)} /> content = <NsfwNote show={() => setShowNsfw(true)} />

View File

@@ -1,8 +1,10 @@
import { isMentioningMutedUsers } from '@/lib/event'
import { tagNameEquals } from '@/lib/tag' import { tagNameEquals } from '@/lib/tag'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useMuteList } from '@/providers/MuteListProvider' import { useMuteList } from '@/providers/MuteListProvider'
import client from '@/services/client.service' import client from '@/services/client.service'
import { Event, kinds, nip19, verifyEvent } from 'nostr-tools' import { Event, kinds, nip19, verifyEvent } from 'nostr-tools'
import { useEffect, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import MainNoteCard from './MainNoteCard' import MainNoteCard from './MainNoteCard'
export default function RepostNoteCard({ export default function RepostNoteCard({
@@ -14,8 +16,19 @@ export default function RepostNoteCard({
className?: string className?: string
filterMutedNotes?: boolean filterMutedNotes?: boolean
}) { }) {
const { mutePubkeys } = useMuteList() const { mutePubkeySet } = useMuteList()
const { hideContentMentioningMutedUsers } = useContentPolicy()
const [targetEvent, setTargetEvent] = useState<Event | null>(null) const [targetEvent, setTargetEvent] = useState<Event | null>(null)
const shouldHide = useMemo(() => {
if (!targetEvent) return true
if (filterMutedNotes && mutePubkeySet.has(targetEvent.pubkey)) {
return true
}
if (hideContentMentioningMutedUsers && isMentioningMutedUsers(targetEvent, mutePubkeySet)) {
return true
}
return false
}, [targetEvent, filterMutedNotes, hideContentMentioningMutedUsers, mutePubkeySet])
useEffect(() => { useEffect(() => {
const fetch = async () => { const fetch = async () => {
try { try {
@@ -56,10 +69,7 @@ export default function RepostNoteCard({
fetch() fetch()
}, [event]) }, [event])
if (!targetEvent) return null if (!targetEvent || shouldHide) return null
if (filterMutedNotes && mutePubkeys.includes(targetEvent.pubkey)) {
return null
}
return <MainNoteCard className={className} reposter={event.pubkey} event={targetEvent} /> return <MainNoteCard className={className} reposter={event.pubkey} event={targetEvent} />
} }

View File

@@ -1,6 +1,9 @@
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { isMentioningMutedUsers } from '@/lib/event'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useMuteList } from '@/providers/MuteListProvider' import { useMuteList } from '@/providers/MuteListProvider'
import { Event, kinds } from 'nostr-tools' import { Event, kinds } from 'nostr-tools'
import { useMemo } from 'react'
import MainNoteCard from './MainNoteCard' import MainNoteCard from './MainNoteCard'
import RepostNoteCard from './RepostNoteCard' import RepostNoteCard from './RepostNoteCard'
@@ -13,10 +16,18 @@ export default function NoteCard({
className?: string className?: string
filterMutedNotes?: boolean filterMutedNotes?: boolean
}) { }) {
const { mutePubkeys } = useMuteList() const { mutePubkeySet } = useMuteList()
if (filterMutedNotes && mutePubkeys.includes(event.pubkey)) { const { hideContentMentioningMutedUsers } = useContentPolicy()
return null const shouldHide = useMemo(() => {
if (filterMutedNotes && mutePubkeySet.has(event.pubkey)) {
return true
} }
if (hideContentMentioningMutedUsers && isMentioningMutedUsers(event, mutePubkeySet)) {
return true
}
return false
}, [event, filterMutedNotes, mutePubkeySet])
if (shouldHide) return null
if (event.kind === kinds.Repost) { if (event.kind === kinds.Repost) {
return ( return (

View File

@@ -2,9 +2,11 @@ import NewNotesButton from '@/components/NewNotesButton'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
getReplaceableCoordinateFromEvent, getReplaceableCoordinateFromEvent,
isMentioningMutedUsers,
isReplaceableEvent, isReplaceableEvent,
isReplyNoteEvent isReplyNoteEvent
} from '@/lib/event' } from '@/lib/event'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useDeletedEvent } from '@/providers/DeletedEventProvider' import { useDeletedEvent } from '@/providers/DeletedEventProvider'
import { useMuteList } from '@/providers/MuteListProvider' import { useMuteList } from '@/providers/MuteListProvider'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
@@ -13,7 +15,15 @@ import client from '@/services/client.service'
import { TFeedSubRequest } from '@/types' import { TFeedSubRequest } from '@/types'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react' import {
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState
} from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import PullToRefresh from 'react-simple-pull-to-refresh' import PullToRefresh from 'react-simple-pull-to-refresh'
import NoteCard, { NoteCardLoadingSkeleton } from '../NoteCard' import NoteCard, { NoteCardLoadingSkeleton } from '../NoteCard'
@@ -44,7 +54,8 @@ const NoteList = forwardRef(
const { t } = useTranslation() const { t } = useTranslation()
const { startLogin } = useNostr() const { startLogin } = useNostr()
const { isUserTrusted } = useUserTrust() const { isUserTrusted } = useUserTrust()
const { mutePubkeys } = useMuteList() const { mutePubkeySet } = useMuteList()
const { hideContentMentioningMutedUsers } = useContentPolicy()
const { isEventDeleted } = useDeletedEvent() const { isEventDeleted } = useDeletedEvent()
const [events, setEvents] = useState<Event[]>([]) const [events, setEvents] = useState<Event[]>([])
const [newEvents, setNewEvents] = useState<Event[]>([]) const [newEvents, setNewEvents] = useState<Event[]>([])
@@ -56,13 +67,30 @@ const NoteList = forwardRef(
const bottomRef = useRef<HTMLDivElement | null>(null) const bottomRef = useRef<HTMLDivElement | null>(null)
const topRef = useRef<HTMLDivElement | null>(null) const topRef = useRef<HTMLDivElement | null>(null)
const shouldHideEvent = useCallback(
(evt: Event) => {
if (isEventDeleted(evt)) return true
if (hideReplies && isReplyNoteEvent(evt)) return true
if (hideUntrustedNotes && !isUserTrusted(evt.pubkey)) return true
if (filterMutedNotes && mutePubkeySet.has(evt.pubkey)) return true
if (
filterMutedNotes &&
hideContentMentioningMutedUsers &&
isMentioningMutedUsers(evt, mutePubkeySet)
) {
return true
}
return false
},
[hideReplies, hideUntrustedNotes, mutePubkeySet, isEventDeleted]
)
const filteredEvents = useMemo(() => { const filteredEvents = useMemo(() => {
const idSet = new Set<string>() const idSet = new Set<string>()
return events.slice(0, showCount).filter((evt) => { return events.slice(0, showCount).filter((evt) => {
if (isEventDeleted(evt)) return false if (shouldHideEvent(evt)) return false
if (hideReplies && isReplyNoteEvent(evt)) return false
if (hideUntrustedNotes && !isUserTrusted(evt.pubkey)) return false
const id = isReplaceableEvent(evt.kind) ? getReplaceableCoordinateFromEvent(evt) : evt.id const id = isReplaceableEvent(evt.kind) ? getReplaceableCoordinateFromEvent(evt) : evt.id
if (idSet.has(id)) { if (idSet.has(id)) {
@@ -71,16 +99,13 @@ const NoteList = forwardRef(
idSet.add(id) idSet.add(id)
return true return true
}) })
}, [events, hideReplies, hideUntrustedNotes, showCount, isEventDeleted]) }, [events, showCount, shouldHideEvent])
const filteredNewEvents = useMemo(() => { const filteredNewEvents = useMemo(() => {
const idSet = new Set<string>() const idSet = new Set<string>()
return newEvents.filter((event: Event) => { return newEvents.filter((event: Event) => {
if (isEventDeleted(event)) return false if (shouldHideEvent(event)) return false
if (hideReplies && isReplyNoteEvent(event)) return false
if (hideUntrustedNotes && !isUserTrusted(event.pubkey)) return false
if (filterMutedNotes && mutePubkeys.includes(event.pubkey)) return false
const id = isReplaceableEvent(event.kind) const id = isReplaceableEvent(event.kind)
? getReplaceableCoordinateFromEvent(event) ? getReplaceableCoordinateFromEvent(event)
@@ -91,7 +116,7 @@ const NoteList = forwardRef(
idSet.add(id) idSet.add(id)
return true return true
}) })
}, [newEvents, hideReplies, hideUntrustedNotes, filterMutedNotes, mutePubkeys, isEventDeleted]) }, [events, showCount, shouldHideEvent])
const scrollToTop = (behavior: ScrollBehavior = 'instant') => { const scrollToTop = (behavior: ScrollBehavior = 'instant') => {
setTimeout(() => { setTimeout(() => {

View File

@@ -47,8 +47,8 @@ export function useMenuActions({
const { t } = useTranslation() const { t } = useTranslation()
const { pubkey, attemptDelete } = useNostr() const { pubkey, attemptDelete } = useNostr()
const { relaySets, favoriteRelays } = useFavoriteRelays() const { relaySets, favoriteRelays } = useFavoriteRelays()
const { mutePubkeyPublicly, mutePubkeyPrivately, unmutePubkey, mutePubkeys } = useMuteList() const { mutePubkeyPublicly, mutePubkeyPrivately, unmutePubkey, mutePubkeySet } = useMuteList()
const isMuted = useMemo(() => mutePubkeys.includes(event.pubkey), [mutePubkeys, event]) const isMuted = useMemo(() => mutePubkeySet.has(event.pubkey), [mutePubkeySet, event])
const broadcastSubMenu: SubMenuAction[] = useMemo(() => { const broadcastSubMenu: SubMenuAction[] = useMemo(() => {
const items = [] const items = []

View File

@@ -1,4 +1,7 @@
import { isMentioningMutedUsers } from '@/lib/event'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useMuteList } from '@/providers/MuteListProvider'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { useReply } from '@/providers/ReplyProvider' import { useReply } from '@/providers/ReplyProvider'
import { useUserTrust } from '@/providers/UserTrustProvider' import { useUserTrust } from '@/providers/UserTrustProvider'
@@ -14,18 +17,29 @@ export default function ReplyButton({ event }: { event: Event }) {
const { pubkey, checkLogin } = useNostr() const { pubkey, checkLogin } = useNostr()
const { repliesMap } = useReply() const { repliesMap } = useReply()
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust() const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
const { mutePubkeySet } = useMuteList()
const { hideContentMentioningMutedUsers } = useContentPolicy()
const { replyCount, hasReplied } = useMemo(() => { const { replyCount, hasReplied } = useMemo(() => {
const hasReplied = pubkey const hasReplied = pubkey
? repliesMap.get(event.id)?.events.some((evt) => evt.pubkey === pubkey) ? repliesMap.get(event.id)?.events.some((evt) => evt.pubkey === pubkey)
: false : false
if (hideUntrustedInteractions) {
return { return {
replyCount: replyCount:
repliesMap.get(event.id)?.events.filter((evt) => isUserTrusted(evt.pubkey)).length ?? 0, repliesMap.get(event.id)?.events.filter((evt) => {
if (hideUntrustedInteractions && !isUserTrusted(evt.pubkey)) {
return false
}
if (mutePubkeySet.has(evt.pubkey)) {
return false
}
if (hideContentMentioningMutedUsers && isMentioningMutedUsers(evt, mutePubkeySet)) {
return false
}
return true
}).length ?? 0,
hasReplied hasReplied
} }
}
return { replyCount: repliesMap.get(event.id)?.events.length ?? 0, hasReplied }
}, [repliesMap, event.id, hideUntrustedInteractions]) }, [repliesMap, event.id, hideUntrustedInteractions])
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)

View File

@@ -1,6 +1,9 @@
import { ExtendedKind } from '@/constants' import { ExtendedKind } from '@/constants'
import { isMentioningMutedUsers } from '@/lib/event'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useMuteList } from '@/providers/MuteListProvider' import { useMuteList } from '@/providers/MuteListProvider'
import { Event, kinds } from 'nostr-tools' import { Event, kinds } from 'nostr-tools'
import { useMemo } from 'react'
import { MentionNotification } from './MentionNotification' import { MentionNotification } from './MentionNotification'
import { PollResponseNotification } from './PollResponseNotification' import { PollResponseNotification } from './PollResponseNotification'
import { ReactionNotification } from './ReactionNotification' import { ReactionNotification } from './ReactionNotification'
@@ -14,10 +17,19 @@ export function NotificationItem({
notification: Event notification: Event
isNew?: boolean isNew?: boolean
}) { }) {
const { mutePubkeys } = useMuteList() const { mutePubkeySet } = useMuteList()
if (mutePubkeys.includes(notification.pubkey)) { const { hideContentMentioningMutedUsers } = useContentPolicy()
return null const shouldHide = useMemo(() => {
if (mutePubkeySet.has(notification.pubkey)) {
return true
} }
if (hideContentMentioningMutedUsers && isMentioningMutedUsers(notification, mutePubkeySet)) {
return true
}
return false
}, [])
if (shouldHide) return null
if (notification.kind === kinds.Reaction) { if (notification.kind === kinds.Reaction) {
return <ReactionNotification notification={notification} isNew={isNew} /> return <ReactionNotification notification={notification} isNew={isNew} />
} }

View File

@@ -23,7 +23,7 @@ export default function Mentions({
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const { pubkey } = useNostr() const { pubkey } = useNostr()
const { mutePubkeys } = useMuteList() const { mutePubkeySet } = useMuteList()
const [potentialMentions, setPotentialMentions] = useState<string[]>([]) const [potentialMentions, setPotentialMentions] = useState<string[]>([])
const [parentEventPubkey, setParentEventPubkey] = useState<string | undefined>() const [parentEventPubkey, setParentEventPubkey] = useState<string | undefined>()
const [removedPubkeys, setRemovedPubkeys] = useState<string[]>([]) const [removedPubkeys, setRemovedPubkeys] = useState<string[]>([])
@@ -43,13 +43,13 @@ export default function Mentions({
pubkeys pubkeys
.filter((p) => potentialMentions.includes(p)) .filter((p) => potentialMentions.includes(p))
.concat( .concat(
potentialMentions.filter((p) => mutePubkeys.includes(p) && p !== _parentEventPubkey) potentialMentions.filter((p) => mutePubkeySet.has(p) && p !== _parentEventPubkey)
) )
) )
) )
}) })
}) })
}, [content, parentEvent, pubkey]) }, [content, parentEvent, pubkey, mutePubkeySet])
useEffect(() => { useEffect(() => {
const newMentions = potentialMentions.filter((pubkey) => !removedPubkeys.includes(pubkey)) const newMentions = potentialMentions.filter((pubkey) => !removedPubkeys.includes(pubkey))

View File

@@ -108,6 +108,7 @@ export default function ProfileFeed({
subRequests={subRequests} subRequests={subRequests}
showKinds={temporaryShowKinds} showKinds={temporaryShowKinds}
hideReplies={listMode === 'posts'} hideReplies={listMode === 'posts'}
filterMutedNotes={false}
/> />
</> </>
) )

View File

@@ -31,7 +31,7 @@ export default function Profile({ id }: { id?: string }) {
const { push } = useSecondaryPage() const { push } = useSecondaryPage()
const { profile, isFetching } = useFetchProfile(id) const { profile, isFetching } = useFetchProfile(id)
const { pubkey: accountPubkey } = useNostr() const { pubkey: accountPubkey } = useNostr()
const { mutePubkeys } = useMuteList() const { mutePubkeySet } = useMuteList()
const { followings } = useFetchFollowings(profile?.pubkey) const { followings } = useFetchFollowings(profile?.pubkey)
const isFollowingYou = useMemo(() => { const isFollowingYou = useMemo(() => {
return ( return (
@@ -176,7 +176,7 @@ export default function Profile({ id }: { id?: string }) {
<Relays pubkey={pubkey} /> <Relays pubkey={pubkey} />
{isSelf && ( {isSelf && (
<SecondaryPageLink to={toMuteList()} className="flex gap-1 hover:underline w-fit"> <SecondaryPageLink to={toMuteList()} className="flex gap-1 hover:underline w-fit">
{mutePubkeys.length} {mutePubkeySet.size}
<div className="text-muted-foreground">{t('Muted')}</div> <div className="text-muted-foreground">{t('Muted')}</div>
</SecondaryPageLink> </SecondaryPageLink>
)} )}

View File

@@ -9,17 +9,17 @@ import { pubkeyToNpub } from '@/lib/pubkey'
import { useMuteList } from '@/providers/MuteListProvider' import { useMuteList } from '@/providers/MuteListProvider'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { Bell, BellOff, Copy, Ellipsis } from 'lucide-react' import { Bell, BellOff, Copy, Ellipsis } from 'lucide-react'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
export default function ProfileOptions({ pubkey }: { pubkey: string }) { export default function ProfileOptions({ pubkey }: { pubkey: string }) {
const { t } = useTranslation() const { t } = useTranslation()
const { pubkey: accountPubkey } = useNostr() const { pubkey: accountPubkey } = useNostr()
const { mutePubkeys, mutePubkeyPrivately, mutePubkeyPublicly, unmutePubkey } = useMuteList() const { mutePubkeySet, mutePubkeyPrivately, mutePubkeyPublicly, unmutePubkey } = useMuteList()
const isMuted = useMemo(() => mutePubkeySet.has(pubkey), [mutePubkeySet, pubkey])
if (pubkey === accountPubkey) return null if (pubkey === accountPubkey) return null
const isMuted = mutePubkeys.includes(pubkey)
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>

View File

@@ -1,8 +1,9 @@
import { useSecondaryPage } from '@/PageManager' import { useSecondaryPage } from '@/PageManager'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { getUsingClient } from '@/lib/event' import { getUsingClient, isMentioningMutedUsers } from '@/lib/event'
import { toNote } from '@/lib/link' import { toNote } from '@/lib/link'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useMuteList } from '@/providers/MuteListProvider' import { useMuteList } from '@/providers/MuteListProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
@@ -33,12 +34,21 @@ export default function ReplyNote({
const { t } = useTranslation() const { t } = useTranslation()
const { isSmallScreen } = useScreenSize() const { isSmallScreen } = useScreenSize()
const { push } = useSecondaryPage() const { push } = useSecondaryPage()
const { mutePubkeys } = useMuteList() const { mutePubkeySet } = useMuteList()
const { hideContentMentioningMutedUsers } = useContentPolicy()
const [showMuted, setShowMuted] = useState(false) const [showMuted, setShowMuted] = useState(false)
const show = useMemo( const show = useMemo(() => {
() => showMuted || !mutePubkeys.includes(event.pubkey), if (showMuted) {
[showMuted, mutePubkeys, event] return true
) }
if (mutePubkeySet.has(event.pubkey)) {
return false
}
if (hideContentMentioningMutedUsers && isMentioningMutedUsers(event, mutePubkeySet)) {
return false
}
return true
}, [showMuted, mutePubkeySet, event, hideContentMentioningMutedUsers])
const usingClient = useMemo(() => getUsingClient(event), [event]) const usingClient = useMemo(() => getUsingClient(event), [event])
return ( return (

View File

@@ -5,11 +5,15 @@ import {
getRootATag, getRootATag,
getRootETag, getRootETag,
getRootEventHexId, getRootEventHexId,
isMentioningMutedUsers,
isReplaceableEvent, isReplaceableEvent,
isReplyNoteEvent isReplyNoteEvent
} from '@/lib/event' } from '@/lib/event'
import { toNote } from '@/lib/link'
import { generateBech32IdFromETag, tagNameEquals } from '@/lib/tag' import { generateBech32IdFromETag, tagNameEquals } from '@/lib/tag'
import { useSecondaryPage } from '@/PageManager' import { useSecondaryPage } from '@/PageManager'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useMuteList } from '@/providers/MuteListProvider'
import { useReply } from '@/providers/ReplyProvider' import { useReply } from '@/providers/ReplyProvider'
import { useUserTrust } from '@/providers/UserTrustProvider' import { useUserTrust } from '@/providers/UserTrustProvider'
import client from '@/services/client.service' import client from '@/services/client.service'
@@ -29,8 +33,10 @@ const SHOW_COUNT = 10
export default function ReplyNoteList({ index, event }: { index?: number; event: NEvent }) { export default function ReplyNoteList({ index, event }: { index?: number; event: NEvent }) {
const { t } = useTranslation() const { t } = useTranslation()
const { currentIndex } = useSecondaryPage() const { push, currentIndex } = useSecondaryPage()
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust() const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
const { mutePubkeySet } = useMuteList()
const { hideContentMentioningMutedUsers } = useContentPolicy()
const [rootInfo, setRootInfo] = useState<TRootInfo | undefined>(undefined) const [rootInfo, setRootInfo] = useState<TRootInfo | undefined>(undefined)
const { repliesMap, addReplies } = useReply() const { repliesMap, addReplies } = useReply()
const replies = useMemo(() => { const replies = useMemo(() => {
@@ -44,6 +50,9 @@ export default function ReplyNoteList({ index, event }: { index?: number; event:
const events = parentEventKeys.flatMap((id) => repliesMap.get(id)?.events || []) const events = parentEventKeys.flatMap((id) => repliesMap.get(id)?.events || [])
events.forEach((evt) => { events.forEach((evt) => {
if (replyIdSet.has(evt.id)) return if (replyIdSet.has(evt.id)) return
if (mutePubkeySet.has(evt.pubkey)) return
if (hideContentMentioningMutedUsers && isMentioningMutedUsers(evt, mutePubkeySet)) return
replyIdSet.add(evt.id) replyIdSet.add(evt.id)
replyEvents.push(evt) replyEvents.push(evt)
}) })
@@ -309,7 +318,14 @@ export default function ReplyNoteList({ index, event }: { index?: number; event:
<ReplyNote <ReplyNote
event={reply} event={reply}
parentEventId={event.id !== parentEventHexId ? parentEventId : undefined} parentEventId={event.id !== parentEventHexId ? parentEventId : undefined}
onClickParent={() => parentEventHexId && highlightReply(parentEventHexId)} onClickParent={() => {
if (!parentEventHexId) return
if (replies.every((r) => r.id !== parentEventHexId)) {
push(toNote(parentEventId ?? parentEventHexId))
return
}
highlightReply(parentEventHexId)
}}
highlight={highlightReplyId === reply.id} highlight={highlightReplyId === reply.id}
/> />
</div> </div>

View File

@@ -40,6 +40,7 @@ export const StorageKey = {
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',
HIDE_CONTENT_MENTIONING_MUTED_USERS: 'hideContentMentioningMutedUsers',
MEDIA_UPLOAD_SERVICE: 'mediaUploadService', // deprecated MEDIA_UPLOAD_SERVICE: 'mediaUploadService', // deprecated
HIDE_UNTRUSTED_EVENTS: 'hideUntrustedEvents', // deprecated HIDE_UNTRUSTED_EVENTS: 'hideUntrustedEvents', // deprecated
ACCOUNT_RELAY_LIST_EVENT_MAP: 'accountRelayListEventMap', // deprecated ACCOUNT_RELAY_LIST_EVENT_MAP: 'accountRelayListEventMap', // deprecated

View File

@@ -372,6 +372,8 @@ export default {
'Deletion request sent to {{count}} relays': 'تم إرسال طلب الحذف إلى {{count}} ريلايات', 'Deletion request sent to {{count}} relays': 'تم إرسال طلب الحذف إلى {{count}} ريلايات',
'Suitable Relays': 'الريلايات المناسبة', 'Suitable Relays': 'الريلايات المناسبة',
'Type searching for people, keywords, or relays': 'Type searching for people, keywords, or relays':
'اكتب للبحث عن أشخاص، كلمات مفتاحية، أو ريلايات' 'اكتب للبحث عن أشخاص، كلمات مفتاحية، أو ريلايات',
'Hide content mentioning muted users': 'إخفاء المحتوى الذي يذكر المستخدمين المكتومين',
'This note mentions a user you muted': 'هذه الملاحظة تذكر مستخدماً قمت بكتمه'
} }
} }

View File

@@ -380,6 +380,9 @@ export default {
'Deletion request sent to {{count}} relays': 'Löschanfrage an {{count}} Relays gesendet', 'Deletion request sent to {{count}} relays': 'Löschanfrage an {{count}} Relays gesendet',
'Suitable Relays': 'Geeignete Relays', 'Suitable Relays': 'Geeignete Relays',
'Type searching for people, keywords, or relays': 'Type searching for people, keywords, or relays':
'Gib ein, um nach Personen, Schlüsselwörtern oder Relays zu suchen' 'Gib ein, um nach Personen, Schlüsselwörtern oder Relays zu suchen',
'Hide content mentioning muted users': 'Inhalte ausblenden, die stumme Benutzer erwähnen',
'This note mentions a user you muted':
'Diese Notiz erwähnt einen Benutzer, den Sie stumm geschaltet haben'
} }
} }

View File

@@ -371,6 +371,8 @@ export default {
'Deletion request sent to {{count}} relays': 'Deletion request sent to {{count}} relays', 'Deletion request sent to {{count}} relays': 'Deletion request sent to {{count}} relays',
'Suitable Relays': 'Suitable Relays', 'Suitable Relays': 'Suitable Relays',
'Type searching for people, keywords, or relays': 'Type searching for people, keywords, or relays':
'Type searching for people, keywords, or relays' 'Type searching for people, keywords, or relays',
'Hide content mentioning muted users': 'Hide content mentioning muted users',
'This note mentions a user you muted': 'This note mentions a user you muted'
} }
} }

View File

@@ -377,6 +377,8 @@ export default {
'Solicitud de eliminación enviada a {{count}} relés', 'Solicitud de eliminación enviada a {{count}} relés',
'Suitable Relays': 'Relés adecuados', 'Suitable Relays': 'Relés adecuados',
'Type searching for people, keywords, or relays': 'Type searching for people, keywords, or relays':
'Escribe para buscar personas, palabras clave o relés' 'Escribe para buscar personas, palabras clave o relés',
'Hide content mentioning muted users': 'Ocultar contenido que mencione usuarios silenciados',
'This note mentions a user you muted': 'Esta nota menciona a un usuario que silenciaste'
} }
} }

View File

@@ -373,6 +373,8 @@ export default {
'Deletion request sent to {{count}} relays': 'درخواست حذف به {{count}} رله ارسال شد', 'Deletion request sent to {{count}} relays': 'درخواست حذف به {{count}} رله ارسال شد',
'Suitable Relays': 'رله‌های مناسب', 'Suitable Relays': 'رله‌های مناسب',
'Type searching for people, keywords, or relays': 'Type searching for people, keywords, or relays':
'برای جستجو افراد، کلمات کلیدی یا رله‌ها تایپ کنید' 'برای جستجو افراد، کلمات کلیدی یا رله‌ها تایپ کنید',
'Hide content mentioning muted users': 'مخفی کردن محتوای اشاره کننده به کاربران بی‌صدا شده',
'This note mentions a user you muted': 'این یادداشت به کاربری که بی‌صدا کرده‌اید اشاره می‌کند'
} }
} }

View File

@@ -379,6 +379,10 @@ export default {
'Demande de suppression envoyée à {{count}} relais', 'Demande de suppression envoyée à {{count}} relais',
'Suitable Relays': 'Relais adaptés', 'Suitable Relays': 'Relais adaptés',
'Type searching for people, keywords, or relays': 'Type searching for people, keywords, or relays':
'Tapez pour rechercher des personnes, des mots-clés ou des relais' 'Tapez pour rechercher des personnes, des mots-clés ou des relais',
'Hide content mentioning muted users':
'Masquer le contenu mentionnant des utilisateurs masqués',
'This note mentions a user you muted':
'Cette note mentionne un utilisateur que vous avez masqué'
} }
} }

View File

@@ -377,6 +377,8 @@ export default {
'Richiesta di eliminazione inviata a {{count}} relays', 'Richiesta di eliminazione inviata a {{count}} relays',
'Suitable Relays': 'Relays adatti', 'Suitable Relays': 'Relays adatti',
'Type searching for people, keywords, or relays': 'Type searching for people, keywords, or relays':
'Digita per cercare persone, parole chiave o relays' 'Digita per cercare persone, parole chiave o relays',
'Hide content mentioning muted users': 'Nascondi contenuto che menziona utenti silenziati',
'This note mentions a user you muted': 'Questa nota menziona un utente che hai silenziato'
} }
} }

View File

@@ -374,6 +374,8 @@ export default {
'削除リクエストが{{count}}個のリレーに送信されました', '削除リクエストが{{count}}個のリレーに送信されました',
'Suitable Relays': '適切なリレー', 'Suitable Relays': '適切なリレー',
'Type searching for people, keywords, or relays': 'Type searching for people, keywords, or relays':
'人、キーワード、またはリレーを検索するために入力してください' '人、キーワード、またはリレーを検索するために入力してください',
'Hide content mentioning muted users': 'ミュートしたユーザーを言及するコンテンツを非表示',
'This note mentions a user you muted': 'このノートはミュートしたユーザーを言及しています'
} }
} }

View File

@@ -374,6 +374,8 @@ export default {
'삭제 요청이 {{count}}개의 릴레이로 전송되었습니다', '삭제 요청이 {{count}}개의 릴레이로 전송되었습니다',
'Suitable Relays': '적합한 릴레이', 'Suitable Relays': '적합한 릴레이',
'Type searching for people, keywords, or relays': 'Type searching for people, keywords, or relays':
'사람, 키워드 또는 릴레이를 검색하려면 입력하세요' '사람, 키워드 또는 릴레이를 검색하려면 입력하세요',
'Hide content mentioning muted users': '뮤트된 사용자를 언급하는 콘텐츠 숨기기',
'This note mentions a user you muted': '이 노트는 뮤트한 사용자를 언급합니다'
} }
} }

View File

@@ -378,6 +378,8 @@ export default {
'Żądanie usunięcia wysłane do {{count}} przekaźników', 'Żądanie usunięcia wysłane do {{count}} przekaźników',
'Suitable Relays': 'Odpowiednie przekaźniki', 'Suitable Relays': 'Odpowiednie przekaźniki',
'Type searching for people, keywords, or relays': 'Type searching for people, keywords, or relays':
'Wpisz, aby wyszukać osoby, słowa kluczowe lub przekaźniki' 'Wpisz, aby wyszukać osoby, słowa kluczowe lub przekaźniki',
'Hide content mentioning muted users': 'Ukryj treści wspominające wyciszonych użytkowników',
'This note mentions a user you muted': 'Ten wpis wspomina użytkownika, którego wyciszyłeś'
} }
} }

View File

@@ -374,6 +374,8 @@ export default {
'Deletion request sent to {{count}} relays': 'Pedido de exclusão enviado para {{count}} relays', 'Deletion request sent to {{count}} relays': 'Pedido de exclusão enviado para {{count}} relays',
'Suitable Relays': 'Relays adequados', 'Suitable Relays': 'Relays adequados',
'Type searching for people, keywords, or relays': 'Type searching for people, keywords, or relays':
'Digite para buscar pessoas, palavras-chave ou relays' 'Digite para buscar pessoas, palavras-chave ou relays',
'Hide content mentioning muted users': 'Ocultar conteúdo que menciona usuários silenciados',
'This note mentions a user you muted': 'Esta nota menciona um usuário que você silenciou'
} }
} }

View File

@@ -377,6 +377,8 @@ export default {
'Pedido de eliminação enviado para {{count}} relays', 'Pedido de eliminação enviado para {{count}} relays',
'Suitable Relays': 'Relays adequados', 'Suitable Relays': 'Relays adequados',
'Type searching for people, keywords, or relays': 'Type searching for people, keywords, or relays':
'Digite para buscar pessoas, palavras-chave ou relays' 'Digite para buscar pessoas, palavras-chave ou relays',
'Hide content mentioning muted users': 'Ocultar conteúdo que menciona utilizadores silenciados',
'This note mentions a user you muted': 'Esta nota menciona um utilizador que silenciou'
} }
} }

View File

@@ -377,6 +377,9 @@ export default {
'Deletion request sent to {{count}} relays': 'Запрос на удаление отправлен на {{count}} релеев', 'Deletion request sent to {{count}} relays': 'Запрос на удаление отправлен на {{count}} релеев',
'Suitable Relays': 'Подходящие релея', 'Suitable Relays': 'Подходящие релея',
'Type searching for people, keywords, or relays': 'Type searching for people, keywords, or relays':
'Начните ввод для поиска людей, ключевых слов или релеев' 'Начните ввод для поиска людей, ключевых слов или релеев',
'Hide content mentioning muted users': 'Скрыть контент, упоминающий заглушённых пользователей',
'This note mentions a user you muted':
'Эта заметка упоминает пользователя, которого вы заглушили'
} }
} }

View File

@@ -369,6 +369,8 @@ export default {
'Try deleting this note': 'ลองลบโน้ตนี้ดู', 'Try deleting this note': 'ลองลบโน้ตนี้ดู',
'Deletion request sent to {{count}} relays': 'คำขอลบถูกส่งไปยังรีเลย์ {{count}} รายการ', 'Deletion request sent to {{count}} relays': 'คำขอลบถูกส่งไปยังรีเลย์ {{count}} รายการ',
'Suitable Relays': 'รีเลย์ที่เหมาะสม', 'Suitable Relays': 'รีเลย์ที่เหมาะสม',
'Type searching for people, keywords, or relays': 'พิมพ์เพื่อค้นหาผู้คน คีย์เวิร์ด หรือรีเลย์' 'Type searching for people, keywords, or relays': 'พิมพ์เพื่อค้นหาผู้คน คีย์เวิร์ด หรือรีเลย์',
'Hide content mentioning muted users': 'ซ่อนเนื้อหาที่กล่าวถึงผู้ใช้ที่ปิดเสียง',
'This note mentions a user you muted': 'โน้ตนี้กล่าวถึงผู้ใช้ที่คุณปิดเสียง'
} }
} }

View File

@@ -367,6 +367,8 @@ export default {
'Try deleting this note': '尝试删除此笔记', 'Try deleting this note': '尝试删除此笔记',
'Deletion request sent to {{count}} relays': '删除请求已发送到 {{count}} 个服务器', 'Deletion request sent to {{count}} relays': '删除请求已发送到 {{count}} 个服务器',
'Suitable Relays': '适合的服务器', 'Suitable Relays': '适合的服务器',
'Type searching for people, keywords, or relays': '输入以搜索用户、关键词或服务器' 'Type searching for people, keywords, or relays': '输入以搜索用户、关键词或服务器',
'Hide content mentioning muted users': '隐藏提及已屏蔽用户的内容',
'This note mentions a user you muted': '此笔记提及了您已屏蔽的用户'
} }
} }

View File

@@ -47,6 +47,15 @@ export function isProtectedEvent(event: Event) {
return event.tags.some(([tagName]) => tagName === '-') return event.tags.some(([tagName]) => tagName === '-')
} }
export function isMentioningMutedUsers(event: Event, mutePubkeySet: Set<string>) {
for (const [tagName, pubkey] of event.tags) {
if (tagName === 'p' && mutePubkeySet.has(pubkey)) {
return true
}
}
return false
}
export function getParentETag(event?: Event) { export function getParentETag(event?: Event) {
if (!event) return undefined if (!event) return undefined

View File

@@ -16,7 +16,14 @@ const GeneralSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
const { t, i18n } = useTranslation() const { t, i18n } = useTranslation()
const [language, setLanguage] = useState<TLanguage>(i18n.language as TLanguage) const [language, setLanguage] = useState<TLanguage>(i18n.language as TLanguage)
const { themeSetting, setThemeSetting } = useTheme() const { themeSetting, setThemeSetting } = useTheme()
const { autoplay, setAutoplay, defaultShowNsfw, setDefaultShowNsfw } = useContentPolicy() const {
autoplay,
setAutoplay,
defaultShowNsfw,
setDefaultShowNsfw,
hideContentMentioningMutedUsers,
setHideContentMentioningMutedUsers
} = useContentPolicy()
const { hideUntrustedNotes, updateHideUntrustedNotes } = useUserTrust() const { hideUntrustedNotes, updateHideUntrustedNotes } = useUserTrust()
const handleLanguageChange = (value: TLanguage) => { const handleLanguageChange = (value: TLanguage) => {
@@ -76,6 +83,16 @@ const GeneralSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
onCheckedChange={updateHideUntrustedNotes} onCheckedChange={updateHideUntrustedNotes}
/> />
</SettingItem> </SettingItem>
<SettingItem>
<Label htmlFor="hide-content-mentioning-muted-users" className="text-base font-normal">
{t('Hide content mentioning muted users')}
</Label>
<Switch
id="hide-content-mentioning-muted-users"
checked={hideContentMentioningMutedUsers}
onCheckedChange={setHideContentMentioningMutedUsers}
/>
</SettingItem>
<SettingItem> <SettingItem>
<Label htmlFor="show-nsfw" className="text-base font-normal"> <Label htmlFor="show-nsfw" className="text-base font-normal">
{t('Show NSFW content by default')} {t('Show NSFW content by default')}

View File

@@ -7,6 +7,9 @@ type TContentPolicyContext = {
defaultShowNsfw: boolean defaultShowNsfw: boolean
setDefaultShowNsfw: (showNsfw: boolean) => void setDefaultShowNsfw: (showNsfw: boolean) => void
hideContentMentioningMutedUsers?: boolean
setHideContentMentioningMutedUsers?: (hide: boolean) => void
} }
const ContentPolicyContext = createContext<TContentPolicyContext | undefined>(undefined) const ContentPolicyContext = createContext<TContentPolicyContext | undefined>(undefined)
@@ -22,6 +25,9 @@ export const useContentPolicy = () => {
export function ContentPolicyProvider({ children }: { children: React.ReactNode }) { export function ContentPolicyProvider({ children }: { children: React.ReactNode }) {
const [autoplay, setAutoplay] = useState<boolean>(storage.getAutoplay()) const [autoplay, setAutoplay] = useState<boolean>(storage.getAutoplay())
const [defaultShowNsfw, setDefaultShowNsfw] = useState<boolean>(storage.getDefaultShowNsfw()) const [defaultShowNsfw, setDefaultShowNsfw] = useState<boolean>(storage.getDefaultShowNsfw())
const [hideContentMentioningMutedUsers, setHideContentMentioningMutedUsers] = useState<boolean>(
storage.getHideContentMentioningMutedUsers()
)
const updateAutoplay = (autoplay: boolean) => { const updateAutoplay = (autoplay: boolean) => {
storage.setAutoplay(autoplay) storage.setAutoplay(autoplay)
@@ -33,13 +39,20 @@ export function ContentPolicyProvider({ children }: { children: React.ReactNode
setDefaultShowNsfw(defaultShowNsfw) setDefaultShowNsfw(defaultShowNsfw)
} }
const updateHideContentMentioningMutedUsers = (hide: boolean) => {
storage.setHideContentMentioningMutedUsers(hide)
setHideContentMentioningMutedUsers(hide)
}
return ( return (
<ContentPolicyContext.Provider <ContentPolicyContext.Provider
value={{ value={{
autoplay, autoplay,
setAutoplay: updateAutoplay, setAutoplay: updateAutoplay,
defaultShowNsfw, defaultShowNsfw,
setDefaultShowNsfw: updateDefaultShowNsfw setDefaultShowNsfw: updateDefaultShowNsfw,
hideContentMentioningMutedUsers,
setHideContentMentioningMutedUsers: updateHideContentMentioningMutedUsers
}} }}
> >
{children} {children}

View File

@@ -11,7 +11,7 @@ import { z } from 'zod'
import { useNostr } from './NostrProvider' import { useNostr } from './NostrProvider'
type TMuteListContext = { type TMuteListContext = {
mutePubkeys: string[] mutePubkeySet: Set<string>
changing: boolean changing: boolean
getMutePubkeys: () => string[] getMutePubkeys: () => string[]
getMuteType: (pubkey: string) => 'public' | 'private' | null getMuteType: (pubkey: string) => 'public' | 'private' | null
@@ -49,10 +49,8 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
() => new Set(getPubkeysFromPTags(privateTags)), () => new Set(getPubkeysFromPTags(privateTags)),
[privateTags] [privateTags]
) )
const mutePubkeys = useMemo(() => { const mutePubkeySet = useMemo(() => {
return Array.from( return new Set([...Array.from(privateMutePubkeySet), ...Array.from(publicMutePubkeySet)])
new Set([...Array.from(privateMutePubkeySet), ...Array.from(publicMutePubkeySet)])
)
}, [publicMutePubkeySet, privateMutePubkeySet]) }, [publicMutePubkeySet, privateMutePubkeySet])
const [changing, setChanging] = useState(false) const [changing, setChanging] = useState(false)
@@ -94,7 +92,7 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
}, [muteListEvent]) }, [muteListEvent])
const getMutePubkeys = () => { const getMutePubkeys = () => {
return mutePubkeys return Array.from(mutePubkeySet)
} }
const getMuteType = useCallback( const getMuteType = useCallback(
@@ -253,7 +251,7 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
return ( return (
<MuteListContext.Provider <MuteListContext.Provider
value={{ value={{
mutePubkeys, mutePubkeySet,
changing, changing,
getMutePubkeys, getMutePubkeys,
getMuteType, getMuteType,

View File

@@ -1,8 +1,10 @@
import { BIG_RELAY_URLS, ExtendedKind } from '@/constants' import { BIG_RELAY_URLS, ExtendedKind } from '@/constants'
import { isMentioningMutedUsers } from '@/lib/event'
import client from '@/services/client.service' import client from '@/services/client.service'
import { kinds } from 'nostr-tools' import { kinds } from 'nostr-tools'
import { SubCloser } from 'nostr-tools/abstract-pool' import { SubCloser } from 'nostr-tools/abstract-pool'
import { createContext, useContext, useEffect, useRef, useState } from 'react' import { createContext, useContext, useEffect, useRef, useState } from 'react'
import { useContentPolicy } from './ContentPolicyProvider'
import { useMuteList } from './MuteListProvider' import { useMuteList } from './MuteListProvider'
import { useNostr } from './NostrProvider' import { useNostr } from './NostrProvider'
import { useUserTrust } from './UserTrustProvider' import { useUserTrust } from './UserTrustProvider'
@@ -26,7 +28,8 @@ export const useNotification = () => {
export function NotificationProvider({ children }: { children: React.ReactNode }) { export function NotificationProvider({ children }: { children: React.ReactNode }) {
const { pubkey, notificationsSeenAt, updateNotificationsSeenAt } = useNostr() const { pubkey, notificationsSeenAt, updateNotificationsSeenAt } = useNostr()
const { hideUntrustedNotifications, isUserTrusted } = useUserTrust() const { hideUntrustedNotifications, isUserTrusted } = useUserTrust()
const { mutePubkeys } = useMuteList() const { mutePubkeySet } = useMuteList()
const { hideContentMentioningMutedUsers } = useContentPolicy()
const [newNotificationIds, setNewNotificationIds] = useState(new Set<string>()) const [newNotificationIds, setNewNotificationIds] = useState(new Set<string>())
const subCloserRef = useRef<SubCloser | null>(null) const subCloserRef = useRef<SubCloser | null>(null)
@@ -67,7 +70,8 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
// Only show notification if not from self and not muted // Only show notification if not from self and not muted
if ( if (
evt.pubkey !== pubkey && evt.pubkey !== pubkey &&
!mutePubkeys.includes(evt.pubkey) && !mutePubkeySet.has(evt.pubkey) &&
(!hideContentMentioningMutedUsers || !isMentioningMutedUsers(evt, mutePubkeySet)) &&
(!hideUntrustedNotifications || isUserTrusted(evt.pubkey)) (!hideUntrustedNotifications || isUserTrusted(evt.pubkey))
) { ) {
setNewNotificationIds((prev) => { setNewNotificationIds((prev) => {

View File

@@ -35,6 +35,7 @@ class LocalStorageService {
private defaultShowNsfw: boolean = false private defaultShowNsfw: boolean = false
private dismissedTooManyRelaysAlert: boolean = false private dismissedTooManyRelaysAlert: boolean = false
private showKinds: number[] = [] private showKinds: number[] = []
private hideContentMentioningMutedUsers: boolean = false
constructor() { constructor() {
if (!LocalStorageService.instance) { if (!LocalStorageService.instance) {
@@ -156,6 +157,9 @@ class LocalStorageService {
window.localStorage.setItem(StorageKey.SHOW_KINDS, JSON.stringify(this.showKinds)) window.localStorage.setItem(StorageKey.SHOW_KINDS, JSON.stringify(this.showKinds))
window.localStorage.setItem(StorageKey.SHOW_KINDS_VERSION, '1') window.localStorage.setItem(StorageKey.SHOW_KINDS_VERSION, '1')
this.hideContentMentioningMutedUsers =
window.localStorage.getItem(StorageKey.HIDE_CONTENT_MENTIONING_MUTED_USERS) === 'true'
// Clean up deprecated data // Clean up deprecated data
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP) window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)
window.localStorage.removeItem(StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP) window.localStorage.removeItem(StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP)
@@ -397,6 +401,15 @@ class LocalStorageService {
this.showKinds = kinds this.showKinds = kinds
window.localStorage.setItem(StorageKey.SHOW_KINDS, JSON.stringify(kinds)) window.localStorage.setItem(StorageKey.SHOW_KINDS, JSON.stringify(kinds))
} }
getHideContentMentioningMutedUsers() {
return this.hideContentMentioningMutedUsers
}
setHideContentMentioningMutedUsers(hide: boolean) {
this.hideContentMentioningMutedUsers = hide
window.localStorage.setItem(StorageKey.HIDE_CONTENT_MENTIONING_MUTED_USERS, hide.toString())
}
} }
const instance = new LocalStorageService() const instance = new LocalStorageService()