feat: hide content mentioning muted users (#524)
Co-authored-by: mleku <me@mleku.dev>
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)} />
|
||||||
|
|||||||
@@ -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} />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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 = []
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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} />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ export default function ProfileFeed({
|
|||||||
subRequests={subRequests}
|
subRequests={subRequests}
|
||||||
showKinds={temporaryShowKinds}
|
showKinds={temporaryShowKinds}
|
||||||
hideReplies={listMode === 'posts'}
|
hideReplies={listMode === 'posts'}
|
||||||
|
filterMutedNotes={false}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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': 'هذه الملاحظة تذكر مستخدماً قمت بكتمه'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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': 'این یادداشت به کاربری که بیصدا کردهاید اشاره میکند'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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é'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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': 'このノートはミュートしたユーザーを言及しています'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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': '이 노트는 뮤트한 사용자를 언급합니다'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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ś'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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':
|
||||||
|
'Эта заметка упоминает пользователя, которого вы заглушили'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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': 'โน้ตนี้กล่าวถึงผู้ใช้ที่คุณปิดเสียง'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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': '此笔记提及了您已屏蔽的用户'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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')}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user