feat: trust score filter
This commit is contained in:
parent
43f4c34fb3
commit
5f785e5553
48 changed files with 974 additions and 480 deletions
|
|
@ -1,10 +1,10 @@
|
|||
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { useState } from 'react'
|
||||
import HideUntrustedContentButton from '../HideUntrustedContentButton'
|
||||
import QuoteList from '../QuoteList'
|
||||
import ReactionList from '../ReactionList'
|
||||
import ReplyNoteList from '../ReplyNoteList'
|
||||
import TrustScoreFilter from '../TrustScoreFilter'
|
||||
import { Tabs, TTabValue } from './Tabs'
|
||||
|
||||
export default function ExternalContentInteractions({
|
||||
|
|
@ -37,7 +37,7 @@ export default function ExternalContentInteractions({
|
|||
</ScrollArea>
|
||||
<Separator orientation="vertical" className="h-6" />
|
||||
<div className="size-10 flex items-center justify-center">
|
||||
<HideUntrustedContentButton type="interactions" />
|
||||
<TrustScoreFilter />
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
|
|
|
|||
|
|
@ -1,77 +0,0 @@
|
|||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Button, buttonVariants } from '@/components/ui/button'
|
||||
import { useUserTrust } from '@/providers/UserTrustProvider'
|
||||
import { VariantProps } from 'class-variance-authority'
|
||||
import { Shield, ShieldCheck } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function HideUntrustedContentButton({
|
||||
type,
|
||||
size = 'icon'
|
||||
}: {
|
||||
type: 'interactions' | 'notifications'
|
||||
size?: VariantProps<typeof buttonVariants>['size']
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
hideUntrustedInteractions,
|
||||
hideUntrustedNotifications,
|
||||
updateHideUntrustedInteractions,
|
||||
updateHideUntrustedNotifications
|
||||
} = useUserTrust()
|
||||
|
||||
const enabled = type === 'interactions' ? hideUntrustedInteractions : hideUntrustedNotifications
|
||||
|
||||
const updateEnabled =
|
||||
type === 'interactions' ? updateHideUntrustedInteractions : updateHideUntrustedNotifications
|
||||
|
||||
const typeText = t(type)
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="ghost" size={size}>
|
||||
{enabled ? (
|
||||
<ShieldCheck className="text-green-400" />
|
||||
) : (
|
||||
<Shield className="text-muted-foreground" />
|
||||
)}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{enabled
|
||||
? t('Show untrusted {type}', { type: typeText })
|
||||
: t('Hide untrusted {type}', { type: typeText })}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{enabled
|
||||
? t('Currently hiding {type} from untrusted users.', { type: typeText })
|
||||
: t('Currently showing all {type}.', { type: typeText })}{' '}
|
||||
{t('Trusted users include people you follow and people they follow.')}{' '}
|
||||
{enabled
|
||||
? t('Click continue to show all {type}.', { type: typeText })
|
||||
: t('Click continue to hide {type} from untrusted users.', { type: typeText })}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t('Cancel')}</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={() => updateEnabled(!enabled)}>
|
||||
{t('Continue')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import NoteList, { TNoteListRef } from '@/components/NoteList'
|
||||
import Tabs from '@/components/Tabs'
|
||||
import TrustScoreFilter from '@/components/TrustScoreFilter'
|
||||
import UserAggregationList, { TUserAggregationListRef } from '@/components/UserAggregationList'
|
||||
import { isTouchDevice } from '@/lib/utils'
|
||||
import { useKindFilter } from '@/providers/KindFilterProvider'
|
||||
import { useUserTrust } from '@/providers/UserTrustProvider'
|
||||
import storage from '@/services/local-storage.service'
|
||||
import { TFeedSubRequest, TNoteListMode } from '@/types'
|
||||
import { useMemo, useRef, useState } from 'react'
|
||||
|
|
@ -25,7 +25,6 @@ export default function NormalFeed({
|
|||
disable24hMode?: boolean
|
||||
onRefresh?: () => void
|
||||
}) {
|
||||
const { hideUntrustedNotes } = useUserTrust()
|
||||
const { showKinds } = useKindFilter()
|
||||
const [temporaryShowKinds, setTemporaryShowKinds] = useState(showKinds)
|
||||
const [listMode, setListMode] = useState<TNoteListMode>(() => storage.getNoteListMode())
|
||||
|
|
@ -79,6 +78,7 @@ export default function NormalFeed({
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
<TrustScoreFilter />
|
||||
{showKindsFilter && (
|
||||
<KindFilter
|
||||
showKinds={temporaryShowKinds}
|
||||
|
|
@ -103,7 +103,6 @@ export default function NormalFeed({
|
|||
showKinds={temporaryShowKinds}
|
||||
subRequests={subRequests}
|
||||
hideReplies={listMode === 'posts'}
|
||||
hideUntrustedNotes={hideUntrustedNotes}
|
||||
areAlgoRelays={areAlgoRelays}
|
||||
showRelayCloseReason={showRelayCloseReason}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -2,16 +2,17 @@ import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
|
|||
import { Separator } from '@/components/ui/separator'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { useState } from 'react'
|
||||
import HideUntrustedContentButton from '../HideUntrustedContentButton'
|
||||
import QuoteList from '../QuoteList'
|
||||
import ReactionList from '../ReactionList'
|
||||
import ReplyNoteList from '../ReplyNoteList'
|
||||
import RepostList from '../RepostList'
|
||||
import TrustScoreFilter from '../TrustScoreFilter'
|
||||
import ZapList from '../ZapList'
|
||||
import { Tabs, TTabValue } from './Tabs'
|
||||
|
||||
export default function NoteInteractions({ event }: { event: Event }) {
|
||||
const [type, setType] = useState<TTabValue>('replies')
|
||||
|
||||
let list
|
||||
switch (type) {
|
||||
case 'replies':
|
||||
|
|
@ -41,9 +42,7 @@ export default function NoteInteractions({ event }: { event: Event }) {
|
|||
<ScrollBar orientation="horizontal" className="opacity-0 pointer-events-none" />
|
||||
</ScrollArea>
|
||||
<Separator orientation="vertical" className="h-6" />
|
||||
<div className="size-10 flex items-center justify-center">
|
||||
<HideUntrustedContentButton type="interactions" />
|
||||
</div>
|
||||
<TrustScoreFilter />
|
||||
</div>
|
||||
<Separator />
|
||||
{list}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import NewNotesButton from '@/components/NewNotesButton'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { SPAMMER_PERCENTILE_THRESHOLD } from '@/constants'
|
||||
import { useInfiniteScroll } from '@/hooks/useInfiniteScroll'
|
||||
import { getEventKey, getKeyFromTag, isMentioningMutedUsers, isReplyNoteEvent } from '@/lib/event'
|
||||
import { tagNameEquals } from '@/lib/tag'
|
||||
|
|
@ -46,7 +47,6 @@ const NoteList = forwardRef<
|
|||
showKinds?: number[]
|
||||
filterMutedNotes?: boolean
|
||||
hideReplies?: boolean
|
||||
hideUntrustedNotes?: boolean
|
||||
hideSpam?: boolean
|
||||
areAlgoRelays?: boolean
|
||||
showRelayCloseReason?: boolean
|
||||
|
|
@ -61,7 +61,6 @@ const NoteList = forwardRef<
|
|||
showKinds,
|
||||
filterMutedNotes = true,
|
||||
hideReplies = false,
|
||||
hideUntrustedNotes = false,
|
||||
hideSpam = false,
|
||||
areAlgoRelays = false,
|
||||
showRelayCloseReason = false,
|
||||
|
|
@ -73,7 +72,7 @@ const NoteList = forwardRef<
|
|||
) => {
|
||||
const { t } = useTranslation()
|
||||
const { startLogin } = useNostr()
|
||||
const { isUserTrusted, isSpammer } = useUserTrust()
|
||||
const { isSpammer, meetsMinTrustScore } = useUserTrust()
|
||||
const { mutePubkeySet } = useMuteList()
|
||||
const { hideContentMentioningMutedUsers } = useContentPolicy()
|
||||
const { isEventDeleted } = useDeletedEvent()
|
||||
|
|
@ -106,7 +105,6 @@ const NoteList = forwardRef<
|
|||
|
||||
if (pinnedEventHexIdSet.has(evt.id)) return true
|
||||
if (isEventDeleted(evt)) return true
|
||||
if (hideUntrustedNotes && !isUserTrusted(evt.pubkey)) return true
|
||||
if (filterMutedNotes && mutePubkeySet.has(evt.pubkey)) return true
|
||||
if (
|
||||
filterMutedNotes &&
|
||||
|
|
@ -121,7 +119,7 @@ const NoteList = forwardRef<
|
|||
|
||||
return false
|
||||
},
|
||||
[hideUntrustedNotes, mutePubkeySet, JSON.stringify(pinnedEventIds), isEventDeleted, filterFn]
|
||||
[mutePubkeySet, JSON.stringify(pinnedEventIds), isEventDeleted, filterFn]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -195,7 +193,13 @@ const NoteList = forwardRef<
|
|||
const _filteredNotes = (
|
||||
await Promise.all(
|
||||
filteredEvents.map(async (evt, i) => {
|
||||
if (hideSpam && (await isSpammer(evt.pubkey))) {
|
||||
// Check trust score filter
|
||||
if (
|
||||
!(await meetsMinTrustScore(
|
||||
evt.pubkey,
|
||||
hideSpam ? SPAMMER_PERCENTILE_THRESHOLD : undefined
|
||||
))
|
||||
) {
|
||||
return null
|
||||
}
|
||||
const key = keys[i]
|
||||
|
|
@ -213,7 +217,7 @@ const NoteList = forwardRef<
|
|||
|
||||
setFiltering(true)
|
||||
processEvents().finally(() => setFiltering(false))
|
||||
}, [events, shouldHideEvent, hideReplies, isSpammer, hideSpam])
|
||||
}, [events, shouldHideEvent, hideReplies, hideSpam, meetsMinTrustScore])
|
||||
|
||||
useEffect(() => {
|
||||
const processNewEvents = async () => {
|
||||
|
|
@ -238,6 +242,10 @@ const NoteList = forwardRef<
|
|||
if (hideSpam && (await isSpammer(evt.pubkey))) {
|
||||
return null
|
||||
}
|
||||
// Check trust score filter
|
||||
if (!(await meetsMinTrustScore(evt.pubkey))) {
|
||||
return null
|
||||
}
|
||||
return evt
|
||||
})
|
||||
)
|
||||
|
|
@ -245,7 +253,7 @@ const NoteList = forwardRef<
|
|||
setFilteredNewEvents(_filteredNotes)
|
||||
}
|
||||
processNewEvents()
|
||||
}, [newEvents, shouldHideEvent, isSpammer, hideSpam])
|
||||
}, [newEvents, shouldHideEvent, isSpammer, hideSpam, meetsMinTrustScore])
|
||||
|
||||
const scrollToTop = (behavior: ScrollBehavior = 'instant') => {
|
||||
setTimeout(() => {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { ExtendedKind } from '@/constants'
|
||||
import { notificationFilter } from '@/lib/notification'
|
||||
import { isMentioningMutedUsers } from '@/lib/event'
|
||||
import { tagNameEquals } from '@/lib/tag'
|
||||
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||
import { useMuteList } from '@/providers/MuteListProvider'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { useUserTrust } from '@/providers/UserTrustProvider'
|
||||
import { Event, kinds } from 'nostr-tools'
|
||||
import { useMemo } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { HighlightNotification } from './HighlightNotification'
|
||||
import { MentionNotification } from './MentionNotification'
|
||||
import { PollResponseNotification } from './PollResponseNotification'
|
||||
|
|
@ -23,22 +24,51 @@ export function NotificationItem({
|
|||
const { pubkey } = useNostr()
|
||||
const { mutePubkeySet } = useMuteList()
|
||||
const { hideContentMentioningMutedUsers } = useContentPolicy()
|
||||
const { hideUntrustedNotifications, isUserTrusted } = useUserTrust()
|
||||
const canShow = useMemo(() => {
|
||||
return notificationFilter(notification, {
|
||||
pubkey,
|
||||
mutePubkeySet,
|
||||
hideContentMentioningMutedUsers,
|
||||
hideUntrustedNotifications,
|
||||
isUserTrusted
|
||||
})
|
||||
const { minTrustScore, meetsMinTrustScore } = useUserTrust()
|
||||
const [canShow, setCanShow] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const checkCanShow = async () => {
|
||||
// Check muted users
|
||||
if (mutePubkeySet.has(notification.pubkey)) {
|
||||
setCanShow(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Check content mentioning muted users
|
||||
if (hideContentMentioningMutedUsers && isMentioningMutedUsers(notification, mutePubkeySet)) {
|
||||
setCanShow(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Check trust score
|
||||
if (!(await meetsMinTrustScore(notification.pubkey))) {
|
||||
setCanShow(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Check reaction target for kind 7
|
||||
if (pubkey && notification.kind === kinds.Reaction) {
|
||||
const targetPubkey = notification.tags.findLast(tagNameEquals('p'))?.[1]
|
||||
if (targetPubkey !== pubkey) {
|
||||
setCanShow(false)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
setCanShow(true)
|
||||
}
|
||||
|
||||
checkCanShow()
|
||||
}, [
|
||||
notification,
|
||||
pubkey,
|
||||
mutePubkeySet,
|
||||
hideContentMentioningMutedUsers,
|
||||
hideUntrustedNotifications,
|
||||
isUserTrusted
|
||||
minTrustScore,
|
||||
meetsMinTrustScore
|
||||
])
|
||||
|
||||
if (!canShow) return null
|
||||
|
||||
if (notification.kind === kinds.Reaction) {
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
import { useSecondaryPage } from '@/PageManager'
|
||||
import { useStuff } from '@/hooks/useStuff'
|
||||
import { useStuffStatsById } from '@/hooks/useStuffStatsById'
|
||||
import { toProfile } from '@/lib/link'
|
||||
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||
import { useUserTrust } from '@/providers/UserTrustProvider'
|
||||
import { TEmoji } from '@/types'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Emoji from '../Emoji'
|
||||
import { FormattedTimestamp } from '../FormattedTimestamp'
|
||||
import Nip05 from '../Nip05'
|
||||
import UserAvatar from '../UserAvatar'
|
||||
import Username from '../Username'
|
||||
import { useStuff } from '@/hooks/useStuff'
|
||||
|
||||
const SHOW_COUNT = 20
|
||||
|
||||
|
|
@ -19,14 +20,39 @@ export default function ReactionList({ stuff }: { stuff: Event | string }) {
|
|||
const { t } = useTranslation()
|
||||
const { push } = useSecondaryPage()
|
||||
const { isSmallScreen } = useScreenSize()
|
||||
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
|
||||
const { minTrustScore, meetsMinTrustScore } = useUserTrust()
|
||||
const { stuffKey } = useStuff(stuff)
|
||||
const noteStats = useStuffStatsById(stuffKey)
|
||||
const filteredLikes = useMemo(() => {
|
||||
return (noteStats?.likes ?? [])
|
||||
.filter((like) => !hideUntrustedInteractions || isUserTrusted(like.pubkey))
|
||||
.sort((a, b) => b.created_at - a.created_at)
|
||||
}, [noteStats, stuffKey, hideUntrustedInteractions, isUserTrusted])
|
||||
const [filteredLikes, setFilteredLikes] = useState<
|
||||
Array<{
|
||||
id: string
|
||||
pubkey: string
|
||||
emoji: string | TEmoji
|
||||
created_at: number
|
||||
}>
|
||||
>([])
|
||||
|
||||
useEffect(() => {
|
||||
const filterLikes = async () => {
|
||||
const likes = noteStats?.likes ?? []
|
||||
const filtered: {
|
||||
id: string
|
||||
pubkey: string
|
||||
created_at: number
|
||||
emoji: string | TEmoji
|
||||
}[] = []
|
||||
await Promise.all(
|
||||
likes.map(async (like) => {
|
||||
if (await meetsMinTrustScore(like.pubkey)) {
|
||||
filtered.push(like)
|
||||
}
|
||||
})
|
||||
)
|
||||
filtered.sort((a, b) => b.created_at - a.created_at)
|
||||
setFilteredLikes(filtered)
|
||||
}
|
||||
filterLikes()
|
||||
}, [noteStats, stuffKey, minTrustScore, meetsMinTrustScore])
|
||||
|
||||
const [showCount, setShowCount] = useState(SHOW_COUNT)
|
||||
const bottomRef = useRef<HTMLDivElement | null>(null)
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export default function RelayReviewsPreview({ relayUrl }: { relayUrl: string })
|
|||
const { t } = useTranslation()
|
||||
const { push } = useSecondaryPage()
|
||||
const { pubkey, checkLogin } = useNostr()
|
||||
const { hideUntrustedNotes, isUserTrusted, isSpammer } = useUserTrust()
|
||||
const { isSpammer } = useUserTrust()
|
||||
const { mutePubkeySet } = useMuteList()
|
||||
const [showEditor, setShowEditor] = useState(false)
|
||||
const [myReview, setMyReview] = useState<NostrEvent | null>(null)
|
||||
|
|
@ -103,7 +103,7 @@ export default function RelayReviewsPreview({ relayUrl }: { relayUrl: string })
|
|||
setInitialized(true)
|
||||
}
|
||||
init()
|
||||
}, [relayUrl, pubkey, mutePubkeySet, hideUntrustedNotes, isUserTrusted])
|
||||
}, [relayUrl, pubkey, mutePubkeySet])
|
||||
|
||||
const handleReviewed = (evt: NostrEvent) => {
|
||||
setMyReview(evt)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { useMuteList } from '@/providers/MuteListProvider'
|
|||
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||
import { useUserTrust } from '@/providers/UserTrustProvider'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ClientTag from '../ClientTag'
|
||||
import Collapsible from '../Collapsible'
|
||||
|
|
@ -42,11 +42,13 @@ export default function ReplyNote({
|
|||
const { isSmallScreen } = useScreenSize()
|
||||
const { push } = useSecondaryPage()
|
||||
const { mutePubkeySet } = useMuteList()
|
||||
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
|
||||
const { minTrustScore, meetsMinTrustScore } = useUserTrust()
|
||||
const { hideContentMentioningMutedUsers } = useContentPolicy()
|
||||
const eventKey = useMemo(() => getEventKey(event), [event])
|
||||
const replies = useThread(eventKey)
|
||||
const [showMuted, setShowMuted] = useState(false)
|
||||
const [hasReplies, setHasReplies] = useState(false)
|
||||
|
||||
const show = useMemo(() => {
|
||||
if (showMuted) {
|
||||
return true
|
||||
|
|
@ -59,24 +61,32 @@ export default function ReplyNote({
|
|||
}
|
||||
return true
|
||||
}, [showMuted, mutePubkeySet, event, hideContentMentioningMutedUsers])
|
||||
const hasReplies = useMemo(() => {
|
||||
if (!replies || replies.length === 0) {
|
||||
return false
|
||||
|
||||
useEffect(() => {
|
||||
const checkHasReplies = async () => {
|
||||
if (!replies || replies.length === 0) {
|
||||
setHasReplies(false)
|
||||
return
|
||||
}
|
||||
|
||||
for (const reply of replies) {
|
||||
if (mutePubkeySet.has(reply.pubkey)) {
|
||||
continue
|
||||
}
|
||||
if (hideContentMentioningMutedUsers && isMentioningMutedUsers(reply, mutePubkeySet)) {
|
||||
continue
|
||||
}
|
||||
if (!(await meetsMinTrustScore(reply.pubkey))) {
|
||||
continue
|
||||
}
|
||||
setHasReplies(true)
|
||||
return
|
||||
}
|
||||
setHasReplies(false)
|
||||
}
|
||||
|
||||
for (const reply of replies) {
|
||||
if (hideUntrustedInteractions && !isUserTrusted(reply.pubkey)) {
|
||||
continue
|
||||
}
|
||||
if (mutePubkeySet.has(reply.pubkey)) {
|
||||
continue
|
||||
}
|
||||
if (hideContentMentioningMutedUsers && isMentioningMutedUsers(reply, mutePubkeySet)) {
|
||||
continue
|
||||
}
|
||||
return true
|
||||
}
|
||||
}, [replies])
|
||||
checkHasReplies()
|
||||
}, [replies, minTrustScore, meetsMinTrustScore, mutePubkeySet, hideContentMentioningMutedUsers])
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,63 +1,19 @@
|
|||
import { useSecondaryPage } from '@/PageManager'
|
||||
import { useAllDescendantThreads } from '@/hooks/useThread'
|
||||
import { getEventKey, getKeyFromTag, getParentTag, isMentioningMutedUsers } from '@/lib/event'
|
||||
import { useFilteredAllReplies } from '@/hooks'
|
||||
import { getEventKey, getKeyFromTag, getParentTag } from '@/lib/event'
|
||||
import { toNote } from '@/lib/link'
|
||||
import { generateBech32IdFromETag } from '@/lib/tag'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||
import { useMuteList } from '@/providers/MuteListProvider'
|
||||
import { useUserTrust } from '@/providers/UserTrustProvider'
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react'
|
||||
import { NostrEvent } from 'nostr-tools'
|
||||
import { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ReplyNote from '../ReplyNote'
|
||||
|
||||
export default function SubReplies({ parentKey }: { parentKey: string }) {
|
||||
const { t } = useTranslation()
|
||||
const { push } = useSecondaryPage()
|
||||
const allThreads = useAllDescendantThreads(parentKey)
|
||||
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
|
||||
const { mutePubkeySet } = useMuteList()
|
||||
const { hideContentMentioningMutedUsers } = useContentPolicy()
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const replies = useMemo(() => {
|
||||
const replyKeySet = new Set<string>()
|
||||
const replyEvents: NostrEvent[] = []
|
||||
|
||||
let parentKeys = [parentKey]
|
||||
while (parentKeys.length > 0) {
|
||||
const events = parentKeys.flatMap((key) => allThreads.get(key) ?? [])
|
||||
events.forEach((evt) => {
|
||||
const key = getEventKey(evt)
|
||||
if (replyKeySet.has(key)) return
|
||||
if (mutePubkeySet.has(evt.pubkey)) return
|
||||
if (hideContentMentioningMutedUsers && isMentioningMutedUsers(evt, mutePubkeySet)) return
|
||||
if (hideUntrustedInteractions && !isUserTrusted(evt.pubkey)) {
|
||||
const replyKey = getEventKey(evt)
|
||||
const repliesForThisReply = allThreads.get(replyKey)
|
||||
// If the reply is not trusted and there are no trusted replies for this reply, skip rendering
|
||||
if (
|
||||
!repliesForThisReply ||
|
||||
repliesForThisReply.every((evt) => !isUserTrusted(evt.pubkey))
|
||||
) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
replyKeySet.add(key)
|
||||
replyEvents.push(evt)
|
||||
})
|
||||
parentKeys = events.map((evt) => getEventKey(evt))
|
||||
}
|
||||
return replyEvents.sort((a, b) => a.created_at - b.created_at)
|
||||
}, [
|
||||
parentKey,
|
||||
allThreads,
|
||||
mutePubkeySet,
|
||||
hideContentMentioningMutedUsers,
|
||||
hideUntrustedInteractions
|
||||
])
|
||||
const { replies } = useFilteredAllReplies(parentKey)
|
||||
const [highlightReplyKey, setHighlightReplyKey] = useState<string | undefined>(undefined)
|
||||
const replyRefs = useRef<Record<string, HTMLDivElement | null>>({})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
import { useFilteredReplies } from '@/hooks'
|
||||
import { useInfiniteScroll } from '@/hooks/useInfiniteScroll'
|
||||
import { useStuff } from '@/hooks/useStuff'
|
||||
import { useAllDescendantThreads } from '@/hooks/useThread'
|
||||
import { getEventKey, isMentioningMutedUsers } from '@/lib/event'
|
||||
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||
import { useMuteList } from '@/providers/MuteListProvider'
|
||||
import { useUserTrust } from '@/providers/UserTrustProvider'
|
||||
import { getEventKey } from '@/lib/event'
|
||||
import threadService from '@/services/thread.service'
|
||||
import { Event as NEvent } from 'nostr-tools'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
|
|
@ -18,47 +15,9 @@ const SHOW_COUNT = 10
|
|||
|
||||
export default function ReplyNoteList({ stuff }: { stuff: NEvent | string }) {
|
||||
const { t } = useTranslation()
|
||||
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
|
||||
const { mutePubkeySet } = useMuteList()
|
||||
const { hideContentMentioningMutedUsers } = useContentPolicy()
|
||||
const { stuffKey } = useStuff(stuff)
|
||||
const allThreads = useAllDescendantThreads(stuffKey)
|
||||
const [initialLoading, setInitialLoading] = useState(false)
|
||||
|
||||
const replies = useMemo(() => {
|
||||
const replyKeySet = new Set<string>()
|
||||
const thread = allThreads.get(stuffKey) || []
|
||||
const replyEvents = thread.filter((evt) => {
|
||||
const key = getEventKey(evt)
|
||||
if (replyKeySet.has(key)) return false
|
||||
if (mutePubkeySet.has(evt.pubkey)) return false
|
||||
if (hideContentMentioningMutedUsers && isMentioningMutedUsers(evt, mutePubkeySet)) {
|
||||
return false
|
||||
}
|
||||
if (hideUntrustedInteractions && !isUserTrusted(evt.pubkey)) {
|
||||
const replyKey = getEventKey(evt)
|
||||
const repliesForThisReply = allThreads.get(replyKey)
|
||||
// If the reply is not trusted and there are no trusted replies for this reply, skip rendering
|
||||
if (
|
||||
!repliesForThisReply ||
|
||||
repliesForThisReply.every((evt) => !isUserTrusted(evt.pubkey))
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
replyKeySet.add(key)
|
||||
return true
|
||||
})
|
||||
return replyEvents.sort((a, b) => b.created_at - a.created_at)
|
||||
}, [
|
||||
stuffKey,
|
||||
allThreads,
|
||||
mutePubkeySet,
|
||||
hideContentMentioningMutedUsers,
|
||||
hideUntrustedInteractions,
|
||||
isUserTrusted
|
||||
])
|
||||
const { replies } = useFilteredReplies(stuffKey)
|
||||
|
||||
// Initial subscription
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
|||
import { useUserTrust } from '@/providers/UserTrustProvider'
|
||||
import { Repeat } from 'lucide-react'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FormattedTimestamp } from '../FormattedTimestamp'
|
||||
import Nip05 from '../Nip05'
|
||||
|
|
@ -19,13 +19,33 @@ export default function RepostList({ event }: { event: Event }) {
|
|||
const { t } = useTranslation()
|
||||
const { push } = useSecondaryPage()
|
||||
const { isSmallScreen } = useScreenSize()
|
||||
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
|
||||
const { minTrustScore, meetsMinTrustScore } = useUserTrust()
|
||||
const noteStats = useStuffStatsById(getEventKey(event))
|
||||
const filteredReposts = useMemo(() => {
|
||||
return (noteStats?.reposts ?? [])
|
||||
.filter((repost) => !hideUntrustedInteractions || isUserTrusted(repost.pubkey))
|
||||
.sort((a, b) => b.created_at - a.created_at)
|
||||
}, [noteStats, event.id, hideUntrustedInteractions, isUserTrusted])
|
||||
const [filteredReposts, setFilteredReposts] = useState<
|
||||
Array<{ id: string; pubkey: string; created_at: number }>
|
||||
>([])
|
||||
|
||||
useEffect(() => {
|
||||
const filterReposts = async () => {
|
||||
const reposts = noteStats?.reposts ?? []
|
||||
const filtered = (
|
||||
await Promise.all(
|
||||
reposts.map(async (repost) => {
|
||||
if (await meetsMinTrustScore(repost.pubkey)) {
|
||||
return repost
|
||||
}
|
||||
})
|
||||
)
|
||||
).filter(Boolean) as {
|
||||
id: string
|
||||
pubkey: string
|
||||
created_at: number
|
||||
}[]
|
||||
filtered.sort((a, b) => b.created_at - a.created_at)
|
||||
setFilteredReposts(filtered)
|
||||
}
|
||||
filterReposts()
|
||||
}, [noteStats, event.id, minTrustScore, meetsMinTrustScore])
|
||||
|
||||
const [showCount, setShowCount] = useState(SHOW_COUNT)
|
||||
const bottomRef = useRef<HTMLDivElement | null>(null)
|
||||
|
|
|
|||
|
|
@ -27,23 +27,38 @@ export default function LikeButton({ stuff }: { stuff: Event | string }) {
|
|||
const { t } = useTranslation()
|
||||
const { isSmallScreen } = useScreenSize()
|
||||
const { pubkey, publish, checkLogin } = useNostr()
|
||||
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
|
||||
const { meetsMinTrustScore } = useUserTrust()
|
||||
const { quickReaction, quickReactionEmoji } = useUserPreferences()
|
||||
const { event, externalContent, stuffKey } = useStuff(stuff)
|
||||
const [liking, setLiking] = useState(false)
|
||||
const [isEmojiReactionsOpen, setIsEmojiReactionsOpen] = useState(false)
|
||||
const [isPickerOpen, setIsPickerOpen] = useState(false)
|
||||
const [likeCount, setLikeCount] = useState(0)
|
||||
const longPressTimerRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const isLongPressRef = useRef(false)
|
||||
const noteStats = useStuffStatsById(stuffKey)
|
||||
const { myLastEmoji, likeCount } = useMemo(() => {
|
||||
const myLastEmoji = useMemo(() => {
|
||||
const stats = noteStats || {}
|
||||
const myLike = stats.likes?.find((like) => like.pubkey === pubkey)
|
||||
const likes = hideUntrustedInteractions
|
||||
? stats.likes?.filter((like) => isUserTrusted(like.pubkey))
|
||||
: stats.likes
|
||||
return { myLastEmoji: myLike?.emoji, likeCount: likes?.length }
|
||||
}, [noteStats, pubkey, hideUntrustedInteractions])
|
||||
return myLike?.emoji
|
||||
}, [noteStats, pubkey])
|
||||
|
||||
useEffect(() => {
|
||||
const filterLikes = async () => {
|
||||
const stats = noteStats || {}
|
||||
const likes = stats.likes || []
|
||||
let count = 0
|
||||
await Promise.all(
|
||||
likes.map(async (like) => {
|
||||
if (await meetsMinTrustScore(like.pubkey)) {
|
||||
count++
|
||||
}
|
||||
})
|
||||
)
|
||||
setLikeCount(count)
|
||||
}
|
||||
filterLikes()
|
||||
}, [noteStats, meetsMinTrustScore])
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => setIsPickerOpen(false), 100)
|
||||
|
|
|
|||
|
|
@ -1,55 +1,19 @@
|
|||
import { useFilteredAllReplies } from '@/hooks'
|
||||
import { useStuff } from '@/hooks/useStuff'
|
||||
import { useAllDescendantThreads } from '@/hooks/useThread'
|
||||
import { getEventKey, isMentioningMutedUsers } from '@/lib/event'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||
import { useMuteList } from '@/providers/MuteListProvider'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { useUserTrust } from '@/providers/UserTrustProvider'
|
||||
import { MessageCircle } from 'lucide-react'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PostEditor from '../PostEditor'
|
||||
import { formatCount } from './utils'
|
||||
|
||||
export default function ReplyButton({ stuff }: { stuff: Event | string }) {
|
||||
const { t } = useTranslation()
|
||||
const { pubkey, checkLogin } = useNostr()
|
||||
const { event, stuffKey } = useStuff(stuff)
|
||||
const allThreads = useAllDescendantThreads(stuffKey)
|
||||
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
|
||||
const { mutePubkeySet } = useMuteList()
|
||||
const { hideContentMentioningMutedUsers } = useContentPolicy()
|
||||
const { replyCount, hasReplied } = useMemo(() => {
|
||||
const hasReplied = pubkey
|
||||
? allThreads.get(stuffKey)?.some((evt) => evt.pubkey === pubkey)
|
||||
: false
|
||||
|
||||
let replyCount = 0
|
||||
const replies = [...(allThreads.get(stuffKey) ?? [])]
|
||||
while (replies.length > 0) {
|
||||
const reply = replies.pop()
|
||||
if (!reply) break
|
||||
|
||||
const replyKey = getEventKey(reply)
|
||||
const nestedReplies = allThreads.get(replyKey) ?? []
|
||||
replies.push(...nestedReplies)
|
||||
|
||||
if (hideUntrustedInteractions && !isUserTrusted(reply.pubkey)) {
|
||||
continue
|
||||
}
|
||||
if (mutePubkeySet.has(reply.pubkey)) {
|
||||
continue
|
||||
}
|
||||
if (hideContentMentioningMutedUsers && isMentioningMutedUsers(reply, mutePubkeySet)) {
|
||||
continue
|
||||
}
|
||||
replyCount++
|
||||
}
|
||||
|
||||
return { replyCount, hasReplied }
|
||||
}, [allThreads, event, stuffKey, hideUntrustedInteractions])
|
||||
const { checkLogin } = useNostr()
|
||||
const { stuffKey } = useStuff(stuff)
|
||||
const { replies, hasReplied } = useFilteredAllReplies(stuffKey)
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
|
|
@ -68,7 +32,7 @@ export default function ReplyButton({ stuff }: { stuff: Event | string }) {
|
|||
title={t('Reply')}
|
||||
>
|
||||
<MessageCircle />
|
||||
{!!replyCount && <div className="text-sm">{formatCount(replyCount)}</div>}
|
||||
{!!replies.length && <div className="text-sm">{formatCount(replies.length)}</div>}
|
||||
</button>
|
||||
<PostEditor parentStuff={stuff} open={open} setOpen={setOpen} />
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import { useUserTrust } from '@/providers/UserTrustProvider'
|
|||
import stuffStatsService from '@/services/stuff-stats.service'
|
||||
import { Loader, PencilLine, Repeat } from 'lucide-react'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PostEditor from '../PostEditor'
|
||||
import { formatCount } from './utils'
|
||||
|
|
@ -25,24 +25,38 @@ import { formatCount } from './utils'
|
|||
export default function RepostButton({ stuff }: { stuff: Event | string }) {
|
||||
const { t } = useTranslation()
|
||||
const { isSmallScreen } = useScreenSize()
|
||||
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
|
||||
const { meetsMinTrustScore } = useUserTrust()
|
||||
const { publish, checkLogin, pubkey } = useNostr()
|
||||
const { event, stuffKey } = useStuff(stuff)
|
||||
const noteStats = useStuffStatsById(stuffKey)
|
||||
const [reposting, setReposting] = useState(false)
|
||||
const [isPostDialogOpen, setIsPostDialogOpen] = useState(false)
|
||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
|
||||
const { repostCount, hasReposted } = useMemo(() => {
|
||||
// external content
|
||||
if (!event) return { repostCount: 0, hasReposted: false }
|
||||
const [repostCount, setRepostCount] = useState(0)
|
||||
const hasReposted = useMemo(() => {
|
||||
return pubkey ? noteStats?.repostPubkeySet?.has(pubkey) : false
|
||||
}, [noteStats, pubkey])
|
||||
|
||||
return {
|
||||
repostCount: hideUntrustedInteractions
|
||||
? noteStats?.reposts?.filter((repost) => isUserTrusted(repost.pubkey)).length
|
||||
: noteStats?.reposts?.length,
|
||||
hasReposted: pubkey ? noteStats?.repostPubkeySet?.has(pubkey) : false
|
||||
useEffect(() => {
|
||||
const filterReposts = async () => {
|
||||
if (!event) {
|
||||
setRepostCount(0)
|
||||
return
|
||||
}
|
||||
|
||||
const reposts = noteStats?.reposts || []
|
||||
let count = 0
|
||||
await Promise.all(
|
||||
reposts.map(async (repost) => {
|
||||
if (await meetsMinTrustScore(repost.pubkey)) {
|
||||
count++
|
||||
}
|
||||
})
|
||||
)
|
||||
setRepostCount(count)
|
||||
}
|
||||
}, [noteStats, event, hideUntrustedInteractions])
|
||||
filterReposts()
|
||||
}, [noteStats, event, meetsMinTrustScore])
|
||||
const canRepost = !hasReposted && !reposting && !!event
|
||||
|
||||
const repost = async () => {
|
||||
|
|
|
|||
171
src/components/TrustScoreFilter/index.tsx
Normal file
171
src/components/TrustScoreFilter/index.tsx
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
import { Button } from '@/components/ui/button'
|
||||
import { Drawer, DrawerContent, DrawerHeader, DrawerTrigger } from '@/components/ui/drawer'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { Slider } from '@/components/ui/slider'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||
import { useUserTrust } from '@/providers/UserTrustProvider'
|
||||
import { Shield, ShieldCheck } from 'lucide-react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const TRUST_LEVELS = [
|
||||
{ value: 0, label: 'trust-filter.off' },
|
||||
{ value: 60, label: 'trust-filter.low' },
|
||||
{ value: 80, label: 'trust-filter.medium' },
|
||||
{ value: 90, label: 'trust-filter.high' },
|
||||
{ value: 100, label: 'trust-filter.wot' }
|
||||
]
|
||||
|
||||
function getDescription(score: number, t: (key: string, options?: any) => string) {
|
||||
if (score === 0) {
|
||||
return t('trust-filter.show-all-content')
|
||||
} else if (score === 100) {
|
||||
return t('trust-filter.only-show-wot')
|
||||
} else {
|
||||
return t('trust-filter.hide-bottom-percent', { score })
|
||||
}
|
||||
}
|
||||
|
||||
export default function TrustScoreFilter() {
|
||||
const { t } = useTranslation()
|
||||
const { isSmallScreen } = useScreenSize()
|
||||
const { minTrustScore, updateMinTrustScore } = useUserTrust()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [temporaryScore, setTemporaryScore] = useState(minTrustScore)
|
||||
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setTemporaryScore(minTrustScore)
|
||||
}, [minTrustScore])
|
||||
|
||||
// Debounced update function
|
||||
const handleScoreChange = (newScore: number) => {
|
||||
setTemporaryScore(newScore)
|
||||
|
||||
// Clear existing timer
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current)
|
||||
}
|
||||
|
||||
// Set new timer for debounced update
|
||||
debounceTimerRef.current = setTimeout(() => {
|
||||
updateMinTrustScore(newScore)
|
||||
}, 300) // 300ms debounce delay
|
||||
}
|
||||
|
||||
// Cleanup timer on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const description = getDescription(temporaryScore, t)
|
||||
|
||||
const trigger = (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="titlebar-icon"
|
||||
className={cn(
|
||||
'relative w-fit px-3',
|
||||
minTrustScore === 0
|
||||
? 'text-muted-foreground hover:text-foreground'
|
||||
: 'text-primary hover:text-primary-hover'
|
||||
)}
|
||||
onClick={() => {
|
||||
if (isSmallScreen) {
|
||||
setOpen(true)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{minTrustScore < 100 ? <Shield size={16} /> : <ShieldCheck size={16} />}
|
||||
{minTrustScore > 0 && minTrustScore < 100 && (
|
||||
<div className="absolute inset-0 w-full h-full flex flex-col items-center justify-center text-[0.55rem] font-mono font-bold">
|
||||
{minTrustScore}
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
|
||||
const content = (
|
||||
<>
|
||||
{/* Slider */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-baseline justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t('trust-filter.filter-threshold')}
|
||||
</span>
|
||||
<span className="text-lg font-semibold text-primary">
|
||||
{temporaryScore === 0 ? t('trust-filter.off') : `${temporaryScore}%`}
|
||||
</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[temporaryScore]}
|
||||
onValueChange={([value]) => handleScoreChange(value)}
|
||||
min={0}
|
||||
max={100}
|
||||
step={5}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Quick Presets */}
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-xs text-muted-foreground">{t('trust-filter.quick-presets')}</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{TRUST_LEVELS.map((level) => (
|
||||
<button
|
||||
key={level.value}
|
||||
onClick={() => handleScoreChange(level.value)}
|
||||
className={cn(
|
||||
'text-center py-1.5 px-2 flex-1 rounded text-xs transition-all duration-200',
|
||||
temporaryScore === level.value
|
||||
? 'bg-primary text-primary-foreground font-medium shadow-sm'
|
||||
: 'bg-secondary hover:bg-secondary/80 hover:shadow-sm hover:scale-[1.02]'
|
||||
)}
|
||||
>
|
||||
{t(level.label)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="space-y-1 pt-2 border-t">
|
||||
<div className="text-sm font-medium text-foreground">{description}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t('trust-filter.trust-score-description')}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
if (isSmallScreen) {
|
||||
return (
|
||||
<>
|
||||
{trigger}
|
||||
<Drawer open={open} onOpenChange={setOpen}>
|
||||
<DrawerTrigger asChild></DrawerTrigger>
|
||||
<DrawerContent className="px-4 pb-4">
|
||||
<DrawerHeader className="text-base font-semibold">
|
||||
{t('trust-filter.title')}
|
||||
</DrawerHeader>
|
||||
<div className="space-y-4 pb-4">{content}</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
||||
<PopoverContent className="p-4 space-y-4 w-96" collisionPadding={16} sideOffset={0}>
|
||||
{content}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
|
@ -34,6 +34,7 @@ import PullToRefresh from 'react-simple-pull-to-refresh'
|
|||
import { toast } from 'sonner'
|
||||
import { LoadingBar } from '../LoadingBar'
|
||||
import NewNotesButton from '../NewNotesButton'
|
||||
import TrustScoreBadge from '../TrustScoreBadge'
|
||||
|
||||
const LIMIT = 500
|
||||
const SHOW_COUNT = 20
|
||||
|
|
@ -66,14 +67,16 @@ const UserAggregationList = forwardRef<
|
|||
const { t } = useTranslation()
|
||||
const { pubkey: currentPubkey, startLogin } = useNostr()
|
||||
const { push } = useSecondaryPage()
|
||||
const { hideUntrustedNotes, isUserTrusted } = useUserTrust()
|
||||
const { mutePubkeySet } = useMuteList()
|
||||
const { pinnedPubkeySet } = usePinnedUsers()
|
||||
const { meetsMinTrustScore } = useUserTrust()
|
||||
const { hideContentMentioningMutedUsers } = useContentPolicy()
|
||||
const { isEventDeleted } = useDeletedEvent()
|
||||
const [since, setSince] = useState(() => dayjs().subtract(1, 'day').unix())
|
||||
const [events, setEvents] = useState<Event[]>([])
|
||||
const [filteredEvents, setFilteredEvents] = useState<Event[]>([])
|
||||
const [newEvents, setNewEvents] = useState<Event[]>([])
|
||||
const [filteredNewEvents, setFilteredNewEvents] = useState<Event[]>([])
|
||||
const [newEventPubkeys, setNewEventPubkeys] = useState<Set<string>>(new Set())
|
||||
const [timelineKey, setTimelineKey] = useState<string | undefined>(undefined)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
|
@ -233,31 +236,40 @@ const UserAggregationList = forwardRef<
|
|||
return () => clearTimeout(timeout)
|
||||
}, [loading])
|
||||
|
||||
const shouldHideEvent = useCallback(
|
||||
(evt: Event) => {
|
||||
if (evt.pubkey === currentPubkey) return true
|
||||
if (isEventDeleted(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
|
||||
}
|
||||
const filterEvents = useCallback(
|
||||
async (events: Event[]) => {
|
||||
const results = await Promise.allSettled(
|
||||
events.map(async (evt) => {
|
||||
if (evt.pubkey === currentPubkey) return null
|
||||
if (evt.created_at < since) return null
|
||||
if (isEventDeleted(evt)) return null
|
||||
if (filterMutedNotes && mutePubkeySet.has(evt.pubkey)) return null
|
||||
if (
|
||||
filterMutedNotes &&
|
||||
hideContentMentioningMutedUsers &&
|
||||
isMentioningMutedUsers(evt, mutePubkeySet)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
if (!(await meetsMinTrustScore(evt.pubkey))) {
|
||||
return null
|
||||
}
|
||||
|
||||
return false
|
||||
return evt
|
||||
})
|
||||
)
|
||||
return results
|
||||
.filter((res) => res.status === 'fulfilled' && res.value !== null)
|
||||
.map((res) => (res as PromiseFulfilledResult<Event>).value)
|
||||
},
|
||||
[
|
||||
hideUntrustedNotes,
|
||||
mutePubkeySet,
|
||||
isEventDeleted,
|
||||
currentPubkey,
|
||||
filterMutedNotes,
|
||||
isUserTrusted,
|
||||
hideContentMentioningMutedUsers,
|
||||
isMentioningMutedUsers
|
||||
isMentioningMutedUsers,
|
||||
meetsMinTrustScore
|
||||
]
|
||||
)
|
||||
|
||||
|
|
@ -265,13 +277,17 @@ const UserAggregationList = forwardRef<
|
|||
return dayjs().diff(dayjs.unix(since), 'day')
|
||||
}, [since])
|
||||
|
||||
const filteredEvents = useMemo(() => {
|
||||
return events.filter((evt) => evt.created_at >= since && !shouldHideEvent(evt))
|
||||
}, [events, since, shouldHideEvent])
|
||||
useEffect(() => {
|
||||
filterEvents(events).then((filtered) => {
|
||||
setFilteredEvents(filtered)
|
||||
})
|
||||
}, [events, filterEvents])
|
||||
|
||||
const filteredNewEvents = useMemo(() => {
|
||||
return newEvents.filter((evt) => evt.created_at >= since && !shouldHideEvent(evt))
|
||||
}, [newEvents, since, shouldHideEvent])
|
||||
useEffect(() => {
|
||||
filterEvents(newEvents).then((filtered) => {
|
||||
setFilteredNewEvents(filtered)
|
||||
})
|
||||
}, [newEvents, filterEvents])
|
||||
|
||||
const aggregations = useMemo(() => {
|
||||
const aggs = userAggregationService.aggregateByUser(filteredEvents)
|
||||
|
|
@ -528,25 +544,28 @@ function UserAggregationItem({
|
|||
)}
|
||||
|
||||
<div className="flex-1 min-w-0 flex flex-col">
|
||||
{supportTouch ? (
|
||||
<SimpleUsername
|
||||
userId={aggregation.pubkey}
|
||||
className={cn(
|
||||
'font-semibold text-base truncate max-w-fit',
|
||||
!hasNewEvents && 'text-muted-foreground'
|
||||
)}
|
||||
skeletonClassName="h-4"
|
||||
/>
|
||||
) : (
|
||||
<Username
|
||||
userId={aggregation.pubkey}
|
||||
className={cn(
|
||||
'font-semibold text-base truncate max-w-fit',
|
||||
!hasNewEvents && 'text-muted-foreground'
|
||||
)}
|
||||
skeletonClassName="h-4"
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
{supportTouch ? (
|
||||
<SimpleUsername
|
||||
userId={aggregation.pubkey}
|
||||
className={cn(
|
||||
'font-semibold text-base truncate max-w-fit',
|
||||
!hasNewEvents && 'text-muted-foreground'
|
||||
)}
|
||||
skeletonClassName="h-4"
|
||||
/>
|
||||
) : (
|
||||
<Username
|
||||
userId={aggregation.pubkey}
|
||||
className={cn(
|
||||
'font-semibold text-base truncate max-w-fit',
|
||||
!hasNewEvents && 'text-muted-foreground'
|
||||
)}
|
||||
skeletonClassName="h-4"
|
||||
/>
|
||||
)}
|
||||
<TrustScoreBadge pubkey={aggregation.pubkey} />
|
||||
</div>
|
||||
<FormattedTimestamp
|
||||
timestamp={aggregation.lastEventTime}
|
||||
className="text-sm text-muted-foreground"
|
||||
|
|
|
|||
|
|
@ -23,11 +23,8 @@ export const StorageKey = {
|
|||
LAST_READ_NOTIFICATION_TIME_MAP: 'lastReadNotificationTimeMap',
|
||||
ACCOUNT_FEED_INFO_MAP: 'accountFeedInfoMap',
|
||||
AUTOPLAY: 'autoplay',
|
||||
HIDE_UNTRUSTED_INTERACTIONS: 'hideUntrustedInteractions',
|
||||
HIDE_UNTRUSTED_NOTIFICATIONS: 'hideUntrustedNotifications',
|
||||
TRANSLATION_SERVICE_CONFIG_MAP: 'translationServiceConfigMap',
|
||||
MEDIA_UPLOAD_SERVICE_CONFIG_MAP: 'mediaUploadServiceConfigMap',
|
||||
HIDE_UNTRUSTED_NOTES: 'hideUntrustedNotes',
|
||||
DISMISSED_TOO_MANY_RELAYS_ALERT: 'dismissedTooManyRelaysAlert',
|
||||
SHOW_KINDS: 'showKinds',
|
||||
SHOW_KINDS_VERSION: 'showKindsVersion',
|
||||
|
|
@ -44,6 +41,10 @@ export const StorageKey = {
|
|||
QUICK_REACTION: 'quickReaction',
|
||||
QUICK_REACTION_EMOJI: 'quickReactionEmoji',
|
||||
NSFW_DISPLAY_POLICY: 'nsfwDisplayPolicy',
|
||||
MIN_TRUST_SCORE: 'minTrustScore',
|
||||
HIDE_UNTRUSTED_NOTES: 'hideUntrustedNotes', // deprecated
|
||||
HIDE_UNTRUSTED_INTERACTIONS: 'hideUntrustedInteractions', // deprecated
|
||||
HIDE_UNTRUSTED_NOTIFICATIONS: 'hideUntrustedNotifications', // deprecated
|
||||
DEFAULT_SHOW_NSFW: 'defaultShowNsfw', // deprecated
|
||||
PINNED_PUBKEYS: 'pinnedPubkeys', // deprecated
|
||||
MEDIA_UPLOAD_SERVICE: 'mediaUploadService', // deprecated
|
||||
|
|
@ -466,3 +467,5 @@ export const PRIMARY_COLORS = {
|
|||
export type TPrimaryColor = keyof typeof PRIMARY_COLORS
|
||||
|
||||
export const LONG_PRESS_THRESHOLD = 400
|
||||
|
||||
export const SPAMMER_PERCENTILE_THRESHOLD = 60
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export * from './useFetchProfile'
|
|||
export * from './useFetchRelayInfo'
|
||||
export * from './useFetchRelayInfos'
|
||||
export * from './useFetchRelayList'
|
||||
export * from './useFilteredReplies'
|
||||
export * from './useInfiniteScroll'
|
||||
export * from './useSearchProfiles'
|
||||
export * from './useTranslatedEvent'
|
||||
|
|
|
|||
160
src/hooks/useFilteredReplies.tsx
Normal file
160
src/hooks/useFilteredReplies.tsx
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
import { getEventKey, isMentioningMutedUsers } from '@/lib/event'
|
||||
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||
import { useMuteList } from '@/providers/MuteListProvider'
|
||||
import { useNostr } from '@/providers/NostrProvider'
|
||||
import { useUserTrust } from '@/providers/UserTrustProvider'
|
||||
import { NostrEvent } from 'nostr-tools'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useAllDescendantThreads } from './useThread'
|
||||
|
||||
export function useFilteredReplies(stuffKey: string) {
|
||||
const { pubkey } = useNostr()
|
||||
const { minTrustScore, meetsMinTrustScore } = useUserTrust()
|
||||
const { mutePubkeySet } = useMuteList()
|
||||
const { hideContentMentioningMutedUsers } = useContentPolicy()
|
||||
const allThreads = useAllDescendantThreads(stuffKey)
|
||||
const [replies, setReplies] = useState<NostrEvent[]>([])
|
||||
const [hasReplied, setHasReplied] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const filterReplies = async () => {
|
||||
const replyKeySet = new Set<string>()
|
||||
const thread = allThreads.get(stuffKey) || []
|
||||
const filtered: NostrEvent[] = []
|
||||
|
||||
await Promise.all(
|
||||
thread.map(async (evt) => {
|
||||
const key = getEventKey(evt)
|
||||
if (replyKeySet.has(key)) return
|
||||
replyKeySet.add(key)
|
||||
|
||||
if (mutePubkeySet.has(evt.pubkey)) return
|
||||
if (hideContentMentioningMutedUsers && isMentioningMutedUsers(evt, mutePubkeySet)) return
|
||||
|
||||
const meetsTrust = await meetsMinTrustScore(evt.pubkey)
|
||||
if (!meetsTrust) {
|
||||
const replyKey = getEventKey(evt)
|
||||
const repliesForThisReply = allThreads.get(replyKey)
|
||||
// If the reply is not trusted, check if there are any trusted replies for this reply
|
||||
if (repliesForThisReply && repliesForThisReply.length > 0) {
|
||||
let hasTrustedReply = false
|
||||
for (const reply of repliesForThisReply) {
|
||||
if (await meetsMinTrustScore(reply.pubkey)) {
|
||||
hasTrustedReply = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!hasTrustedReply) return
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
filtered.push(evt)
|
||||
})
|
||||
)
|
||||
|
||||
filtered.sort((a, b) => b.created_at - a.created_at)
|
||||
setReplies(filtered)
|
||||
}
|
||||
|
||||
filterReplies()
|
||||
}, [
|
||||
stuffKey,
|
||||
allThreads,
|
||||
mutePubkeySet,
|
||||
hideContentMentioningMutedUsers,
|
||||
minTrustScore,
|
||||
meetsMinTrustScore
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
let replied = false
|
||||
for (const reply of replies) {
|
||||
if (reply.pubkey === pubkey) {
|
||||
replied = true
|
||||
break
|
||||
}
|
||||
}
|
||||
setHasReplied(replied)
|
||||
}, [replies, pubkey])
|
||||
|
||||
return { replies, hasReplied }
|
||||
}
|
||||
|
||||
export function useFilteredAllReplies(stuffKey: string) {
|
||||
const { pubkey } = useNostr()
|
||||
const allThreads = useAllDescendantThreads(stuffKey)
|
||||
const { minTrustScore, meetsMinTrustScore } = useUserTrust()
|
||||
const { mutePubkeySet } = useMuteList()
|
||||
const { hideContentMentioningMutedUsers } = useContentPolicy()
|
||||
const [replies, setReplies] = useState<NostrEvent[]>([])
|
||||
const [hasReplied, setHasReplied] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const filterReplies = async () => {
|
||||
const replyKeySet = new Set<string>()
|
||||
const replyEvents: NostrEvent[] = []
|
||||
|
||||
let parentKeys = [stuffKey]
|
||||
while (parentKeys.length > 0) {
|
||||
const events = parentKeys.flatMap((key) => allThreads.get(key) ?? [])
|
||||
await Promise.all(
|
||||
events.map(async (evt) => {
|
||||
const key = getEventKey(evt)
|
||||
if (replyKeySet.has(key)) return
|
||||
replyKeySet.add(key)
|
||||
|
||||
if (mutePubkeySet.has(evt.pubkey)) return
|
||||
if (hideContentMentioningMutedUsers && isMentioningMutedUsers(evt, mutePubkeySet))
|
||||
return
|
||||
|
||||
const meetsTrust = await meetsMinTrustScore(evt.pubkey)
|
||||
if (!meetsTrust) {
|
||||
const replyKey = getEventKey(evt)
|
||||
const repliesForThisReply = allThreads.get(replyKey)
|
||||
// If the reply is not trusted, check if there are any trusted replies for this reply
|
||||
if (repliesForThisReply && repliesForThisReply.length > 0) {
|
||||
let hasTrustedReply = false
|
||||
for (const reply of repliesForThisReply) {
|
||||
if (await meetsMinTrustScore(reply.pubkey)) {
|
||||
hasTrustedReply = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!hasTrustedReply) return
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
replyEvents.push(evt)
|
||||
})
|
||||
)
|
||||
parentKeys = events.map((evt) => getEventKey(evt))
|
||||
}
|
||||
setReplies(replyEvents.sort((a, b) => a.created_at - b.created_at))
|
||||
}
|
||||
|
||||
filterReplies()
|
||||
}, [
|
||||
stuffKey,
|
||||
allThreads,
|
||||
mutePubkeySet,
|
||||
hideContentMentioningMutedUsers,
|
||||
minTrustScore,
|
||||
meetsMinTrustScore
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
let replied = false
|
||||
for (const reply of replies) {
|
||||
if (reply.pubkey === pubkey) {
|
||||
replied = true
|
||||
break
|
||||
}
|
||||
}
|
||||
setHasReplied(replied)
|
||||
}, [replies, pubkey])
|
||||
|
||||
return { replies, hasReplied }
|
||||
}
|
||||
|
|
@ -638,6 +638,19 @@ export default {
|
|||
'Enter Password': 'أدخل كلمة المرور',
|
||||
Password: 'كلمة المرور',
|
||||
Confirm: 'تأكيد',
|
||||
'trust-filter.title': 'مرشح درجة الثقة',
|
||||
'trust-filter.off': 'إيقاف',
|
||||
'trust-filter.low': 'منخفض',
|
||||
'trust-filter.medium': 'متوسط',
|
||||
'trust-filter.high': 'عالي',
|
||||
'trust-filter.wot': 'WoT',
|
||||
'trust-filter.filter-threshold': 'عتبة التصفية',
|
||||
'trust-filter.quick-presets': 'إعدادات سريعة',
|
||||
'trust-filter.show-all-content': 'إظهار جميع المحتويات',
|
||||
'trust-filter.only-show-wot': 'إظهار شبكة الثقة الخاصة بك فقط (المتابَعون + متابَعوهم)',
|
||||
'trust-filter.hide-bottom-percent':
|
||||
'تصفية أدنى {{score}}٪ من المستخدمين حسب تصنيف الثقة',
|
||||
'trust-filter.trust-score-description': 'تصنف درجة الثقة المستخدمين حسب النسبة المئوية للسمعة',
|
||||
'Auto-load profile pictures': 'تحميل صور الملف الشخصي تلقائيًا'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -659,6 +659,20 @@ export default {
|
|||
'Enter Password': 'Passwort eingeben',
|
||||
Password: 'Passwort',
|
||||
Confirm: 'Bestätigen',
|
||||
'trust-filter.title': 'Vertrauenswert-Filter',
|
||||
'trust-filter.off': 'Aus',
|
||||
'trust-filter.low': 'Niedrig',
|
||||
'trust-filter.medium': 'Mittel',
|
||||
'trust-filter.high': 'Hoch',
|
||||
'trust-filter.wot': 'WoT',
|
||||
'trust-filter.filter-threshold': 'Filterschwelle',
|
||||
'trust-filter.quick-presets': 'Schnellvoreinstellungen',
|
||||
'trust-filter.show-all-content': 'Alle Inhalte anzeigen',
|
||||
'trust-filter.only-show-wot': 'Nur Ihr Vertrauensnetzwerk anzeigen (Folgende + deren Folgende)',
|
||||
'trust-filter.hide-bottom-percent':
|
||||
'Untere {{score}}% der Benutzer nach Vertrauensrang filtern',
|
||||
'trust-filter.trust-score-description':
|
||||
'Der Vertrauenswert ordnet Benutzer nach Reputationsperzentil',
|
||||
'Auto-load profile pictures': 'Profilbilder automatisch laden'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -643,6 +643,19 @@ export default {
|
|||
'Enter Password': 'Enter Password',
|
||||
Password: 'Password',
|
||||
Confirm: 'Confirm',
|
||||
'trust-filter.title': 'Trust Score Filter',
|
||||
'trust-filter.off': 'Off',
|
||||
'trust-filter.low': 'Low',
|
||||
'trust-filter.medium': 'Medium',
|
||||
'trust-filter.high': 'High',
|
||||
'trust-filter.wot': 'WoT',
|
||||
'trust-filter.filter-threshold': 'Filter Threshold',
|
||||
'trust-filter.quick-presets': 'Quick presets',
|
||||
'trust-filter.show-all-content': 'Show all content',
|
||||
'trust-filter.only-show-wot': 'Only show your Web of Trust (follows + their follows)',
|
||||
'trust-filter.hide-bottom-percent':
|
||||
'Filter out bottom {{score}}% of users by trust rank',
|
||||
'trust-filter.trust-score-description': 'Trust score ranks users by reputation percentile',
|
||||
'Auto-load profile pictures': 'Auto-load profile pictures'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -653,6 +653,20 @@ export default {
|
|||
'Enter Password': 'Ingresar contraseña',
|
||||
Password: 'Contraseña',
|
||||
Confirm: 'Confirmar',
|
||||
'trust-filter.title': 'Filtro de puntuación de confianza',
|
||||
'trust-filter.off': 'Desactivado',
|
||||
'trust-filter.low': 'Bajo',
|
||||
'trust-filter.medium': 'Medio',
|
||||
'trust-filter.high': 'Alto',
|
||||
'trust-filter.wot': 'WoT',
|
||||
'trust-filter.filter-threshold': 'Umbral de filtro',
|
||||
'trust-filter.quick-presets': 'Ajustes rápidos',
|
||||
'trust-filter.show-all-content': 'Mostrar todo el contenido',
|
||||
'trust-filter.only-show-wot': 'Mostrar solo tu red de confianza (seguidos + sus seguidos)',
|
||||
'trust-filter.hide-bottom-percent':
|
||||
'Filtrar el {{score}}% inferior de usuarios por clasificación de confianza',
|
||||
'trust-filter.trust-score-description':
|
||||
'La puntuación de confianza clasifica a los usuarios por percentil de reputación',
|
||||
'Auto-load profile pictures': 'Cargar imágenes de perfil automáticamente'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -648,6 +648,21 @@ export default {
|
|||
'Enter Password': 'رمز عبور را وارد کنید',
|
||||
Password: 'رمز عبور',
|
||||
Confirm: 'تأیید',
|
||||
'trust-filter.title': 'فیلتر امتیاز اعتماد',
|
||||
'trust-filter.off': 'خاموش',
|
||||
'trust-filter.low': 'پایین',
|
||||
'trust-filter.medium': 'متوسط',
|
||||
'trust-filter.high': 'بالا',
|
||||
'trust-filter.wot': 'WoT',
|
||||
'trust-filter.filter-threshold': 'آستانه فیلتر',
|
||||
'trust-filter.quick-presets': 'تنظیمات سریع',
|
||||
'trust-filter.show-all-content': 'نمایش همه محتوا',
|
||||
'trust-filter.only-show-wot':
|
||||
'فقط شبکه اعتماد شما را نشان دهید (دنبالشوندگان + دنبالشوندگان آنها)',
|
||||
'trust-filter.hide-bottom-percent':
|
||||
'فیلتر کردن {{score}}٪ پایینترین کاربران بر اساس رتبه اعتماد',
|
||||
'trust-filter.trust-score-description':
|
||||
'امتیاز اعتماد کاربران را بر اساس صدک شهرت رتبهبندی میکند',
|
||||
'Auto-load profile pictures': 'بارگذاری خودکار تصاویر پروفایل'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -656,6 +656,21 @@ export default {
|
|||
'Enter Password': 'Entrer le mot de passe',
|
||||
Password: 'Mot de passe',
|
||||
Confirm: 'Confirmer',
|
||||
'trust-filter.title': 'Filtre de score de confiance',
|
||||
'trust-filter.off': 'Désactivé',
|
||||
'trust-filter.low': 'Faible',
|
||||
'trust-filter.medium': 'Moyen',
|
||||
'trust-filter.high': 'Élevé',
|
||||
'trust-filter.wot': 'WoT',
|
||||
'trust-filter.filter-threshold': 'Seuil de filtrage',
|
||||
'trust-filter.quick-presets': 'Préréglages rapides',
|
||||
'trust-filter.show-all-content': 'Afficher tout le contenu',
|
||||
'trust-filter.only-show-wot':
|
||||
'Afficher uniquement votre réseau de confiance (abonnements + leurs abonnements)',
|
||||
'trust-filter.hide-bottom-percent':
|
||||
'Filtrer les {{score}}% inférieurs des utilisateurs par classement de confiance',
|
||||
'trust-filter.trust-score-description':
|
||||
'Le score de confiance classe les utilisateurs par percentile de réputation',
|
||||
'Auto-load profile pictures': 'Charger les images de profil automatiquement'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -649,6 +649,20 @@ export default {
|
|||
'Enter Password': 'पासवर्ड दर्ज करें',
|
||||
Password: 'पासवर्ड',
|
||||
Confirm: 'पुष्टि करें',
|
||||
'trust-filter.title': 'विश्वास स्कोर फ़िल्टर',
|
||||
'trust-filter.off': 'बंद',
|
||||
'trust-filter.low': 'कम',
|
||||
'trust-filter.medium': 'मध्यम',
|
||||
'trust-filter.high': 'उच्च',
|
||||
'trust-filter.wot': 'WoT',
|
||||
'trust-filter.filter-threshold': 'फ़िल्टर सीमा',
|
||||
'trust-filter.quick-presets': 'त्वरित प्रीसेट',
|
||||
'trust-filter.show-all-content': 'सभी सामग्री दिखाएं',
|
||||
'trust-filter.only-show-wot': 'केवल अपना विश्वास नेटवर्क दिखाएं (फ़ॉलो + उनके फ़ॉलो)',
|
||||
'trust-filter.hide-bottom-percent':
|
||||
'विश्वास रैंक द्वारा निचले {{score}}% उपयोगकर्ताओं को फ़िल्टर करें',
|
||||
'trust-filter.trust-score-description':
|
||||
'विश्वास स्कोर प्रतिष्ठा प्रतिशतक द्वारा उपयोगकर्ताओं को रैंक करता है',
|
||||
'Auto-load profile pictures': 'प्रोफ़ाइल चित्र स्वतः लोड करें'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -641,6 +641,21 @@ export default {
|
|||
'Enter Password': 'Jelszó megadása',
|
||||
Password: 'Jelszó',
|
||||
Confirm: 'Megerősítés',
|
||||
'trust-filter.title': 'Bizalmi pontszám szűrő',
|
||||
'trust-filter.off': 'Ki',
|
||||
'trust-filter.low': 'Alacsony',
|
||||
'trust-filter.medium': 'Közepes',
|
||||
'trust-filter.high': 'Magas',
|
||||
'trust-filter.wot': 'WoT',
|
||||
'trust-filter.filter-threshold': 'Szűrési küszöb',
|
||||
'trust-filter.quick-presets': 'Gyors beállítások',
|
||||
'trust-filter.show-all-content': 'Minden tartalom megjelenítése',
|
||||
'trust-filter.only-show-wot':
|
||||
'Csak a bizalmi hálózatod megjelenítése (követettek + követetteik)',
|
||||
'trust-filter.hide-bottom-percent':
|
||||
'Alsó {{score}}% felhasználók szűrése bizalmi rangsor szerint',
|
||||
'trust-filter.trust-score-description':
|
||||
'A bizalmi pontszám a felhasználókat hírnév percentilis szerint rangsorolja',
|
||||
'Auto-load profile pictures': 'Profilképek automatikus betöltése'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -653,6 +653,20 @@ export default {
|
|||
'Enter Password': 'Inserisci password',
|
||||
Password: 'Password',
|
||||
Confirm: 'Conferma',
|
||||
'trust-filter.title': 'Filtro punteggio di fiducia',
|
||||
'trust-filter.off': 'Disattivato',
|
||||
'trust-filter.low': 'Basso',
|
||||
'trust-filter.medium': 'Medio',
|
||||
'trust-filter.high': 'Alto',
|
||||
'trust-filter.wot': 'WoT',
|
||||
'trust-filter.filter-threshold': 'Soglia di filtro',
|
||||
'trust-filter.quick-presets': 'Preimpostazioni rapide',
|
||||
'trust-filter.show-all-content': 'Mostra tutti i contenuti',
|
||||
'trust-filter.only-show-wot': 'Mostra solo la tua rete di fiducia (seguiti + i loro seguiti)',
|
||||
'trust-filter.hide-bottom-percent':
|
||||
'Filtra il {{score}}% inferiore degli utenti per classifica di fiducia',
|
||||
'trust-filter.trust-score-description':
|
||||
'Il punteggio di fiducia classifica gli utenti per percentile di reputazione',
|
||||
'Auto-load profile pictures': 'Caricamento automatico immagini di profilo'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -647,6 +647,21 @@ export default {
|
|||
'Enter Password': 'パスワードを入力',
|
||||
Password: 'パスワード',
|
||||
Confirm: '確認',
|
||||
'trust-filter.title': '信頼スコアフィルター',
|
||||
'trust-filter.off': 'オフ',
|
||||
'trust-filter.low': '低',
|
||||
'trust-filter.medium': '中',
|
||||
'trust-filter.high': '高',
|
||||
'trust-filter.wot': 'WoT',
|
||||
'trust-filter.filter-threshold': 'フィルター閾値',
|
||||
'trust-filter.quick-presets': 'クイックプリセット',
|
||||
'trust-filter.show-all-content': 'すべてのコンテンツを表示',
|
||||
'trust-filter.only-show-wot':
|
||||
'あなたの信頼ネットワークのみを表示(フォロー + フォローのフォロー)',
|
||||
'trust-filter.hide-bottom-percent':
|
||||
'信頼ランク下位 {{score}}% のユーザーをフィルタリング',
|
||||
'trust-filter.trust-score-description':
|
||||
'信頼スコアは評判パーセンタイルでユーザーをランク付けします',
|
||||
'Auto-load profile pictures': 'プロフィール画像を自動読み込み'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -644,6 +644,18 @@ export default {
|
|||
'Enter Password': '비밀번호 입력',
|
||||
Password: '비밀번호',
|
||||
Confirm: '확인',
|
||||
'trust-filter.title': '신뢰 점수 필터',
|
||||
'trust-filter.off': '끄기',
|
||||
'trust-filter.low': '낮음',
|
||||
'trust-filter.medium': '중간',
|
||||
'trust-filter.high': '높음',
|
||||
'trust-filter.wot': 'WoT',
|
||||
'trust-filter.filter-threshold': '필터 임계값',
|
||||
'trust-filter.quick-presets': '빠른 프리셋',
|
||||
'trust-filter.show-all-content': '모든 콘텐츠 표시',
|
||||
'trust-filter.only-show-wot': '신뢰 네트워크만 표시 (팔로우 + 팔로우의 팔로우)',
|
||||
'trust-filter.hide-bottom-percent': '신뢰도 하위 {{score}}% 사용자 필터링',
|
||||
'trust-filter.trust-score-description': '신뢰 점수는 평판 백분위수로 사용자를 순위 매깁니다',
|
||||
'Auto-load profile pictures': '프로필 사진 자동 로드'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -654,6 +654,20 @@ export default {
|
|||
'Enter Password': 'Wprowadź hasło',
|
||||
Password: 'Hasło',
|
||||
Confirm: 'Potwierdź',
|
||||
'trust-filter.title': 'Filtr wyniku zaufania',
|
||||
'trust-filter.off': 'Wyłączony',
|
||||
'trust-filter.low': 'Niski',
|
||||
'trust-filter.medium': 'Średni',
|
||||
'trust-filter.high': 'Wysoki',
|
||||
'trust-filter.wot': 'WoT',
|
||||
'trust-filter.filter-threshold': 'Próg filtrowania',
|
||||
'trust-filter.quick-presets': 'Szybkie ustawienia',
|
||||
'trust-filter.show-all-content': 'Pokaż całą zawartość',
|
||||
'trust-filter.only-show-wot': 'Pokaż tylko swoją sieć zaufania (obserwowani + ich obserwowani)',
|
||||
'trust-filter.hide-bottom-percent':
|
||||
'Filtruj dolne {{score}}% użytkowników według rankingu zaufania',
|
||||
'trust-filter.trust-score-description':
|
||||
'Wynik zaufania klasyfikuje użytkowników według percentyla reputacji',
|
||||
'Auto-load profile pictures': 'Automatyczne ładowanie zdjęć profilowych'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -649,6 +649,21 @@ export default {
|
|||
'Enter Password': 'Digite a senha',
|
||||
Password: 'Senha',
|
||||
Confirm: 'Confirmar',
|
||||
'trust-filter.title': 'Filtro de pontuação de confiança',
|
||||
'trust-filter.off': 'Desativado',
|
||||
'trust-filter.low': 'Baixo',
|
||||
'trust-filter.medium': 'Médio',
|
||||
'trust-filter.high': 'Alto',
|
||||
'trust-filter.wot': 'WoT',
|
||||
'trust-filter.filter-threshold': 'Limite de filtro',
|
||||
'trust-filter.quick-presets': 'Predefinições rápidas',
|
||||
'trust-filter.show-all-content': 'Mostrar todo o conteúdo',
|
||||
'trust-filter.only-show-wot':
|
||||
'Mostrar apenas sua rede de confiança (seguidos + seguidos deles)',
|
||||
'trust-filter.hide-bottom-percent':
|
||||
'Filtrar os {{score}}% inferiores de usuários por classificação de confiança',
|
||||
'trust-filter.trust-score-description':
|
||||
'A pontuação de confiança classifica os usuários por percentil de reputação',
|
||||
'Auto-load profile pictures': 'Carregar fotos de perfil automaticamente'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -652,6 +652,21 @@ export default {
|
|||
'Enter Password': 'Introduza a palavra-passe',
|
||||
Password: 'Palavra-passe',
|
||||
Confirm: 'Confirmar',
|
||||
'trust-filter.title': 'Filtro de pontuação de confiança',
|
||||
'trust-filter.off': 'Desativado',
|
||||
'trust-filter.low': 'Baixo',
|
||||
'trust-filter.medium': 'Médio',
|
||||
'trust-filter.high': 'Alto',
|
||||
'trust-filter.wot': 'WoT',
|
||||
'trust-filter.filter-threshold': 'Limite de filtro',
|
||||
'trust-filter.quick-presets': 'Predefinições rápidas',
|
||||
'trust-filter.show-all-content': 'Mostrar todo o conteúdo',
|
||||
'trust-filter.only-show-wot':
|
||||
'Mostrar apenas a sua rede de confiança (seguidos + seguidos deles)',
|
||||
'trust-filter.hide-bottom-percent':
|
||||
'Filtrar os {{score}}% inferiores de utilizadores por classificação de confiança',
|
||||
'trust-filter.trust-score-description':
|
||||
'A pontuação de confiança classifica os utilizadores por percentil de reputação',
|
||||
'Auto-load profile pictures': 'Carregar fotos de perfil automaticamente'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -653,6 +653,20 @@ export default {
|
|||
'Enter Password': 'Введите пароль',
|
||||
Password: 'Пароль',
|
||||
Confirm: 'Подтвердить',
|
||||
'trust-filter.title': 'Фильтр доверия',
|
||||
'trust-filter.off': 'Выкл',
|
||||
'trust-filter.low': 'Низкий',
|
||||
'trust-filter.medium': 'Средний',
|
||||
'trust-filter.high': 'Высокий',
|
||||
'trust-filter.wot': 'WoT',
|
||||
'trust-filter.filter-threshold': 'Порог фильтрации',
|
||||
'trust-filter.quick-presets': 'Быстрые предустановки',
|
||||
'trust-filter.show-all-content': 'Показать весь контент',
|
||||
'trust-filter.only-show-wot': 'Показывать только вашу сеть доверия (подписки + их подписки)',
|
||||
'trust-filter.hide-bottom-percent':
|
||||
'Отфильтровать нижние {{score}}% пользователей по рейтингу доверия',
|
||||
'trust-filter.trust-score-description':
|
||||
'Оценка доверия ранжирует пользователей по процентилю репутации',
|
||||
'Auto-load profile pictures': 'Автозагрузка аватаров'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -638,6 +638,21 @@ export default {
|
|||
'Enter Password': 'ป้อนรหัสผ่าน',
|
||||
Password: 'รหัสผ่าน',
|
||||
Confirm: 'ยืนยัน',
|
||||
'trust-filter.title': 'ตัวกรองคะแนนความไว้วางใจ',
|
||||
'trust-filter.off': 'ปิด',
|
||||
'trust-filter.low': 'ต่ำ',
|
||||
'trust-filter.medium': 'ปานกลาง',
|
||||
'trust-filter.high': 'สูง',
|
||||
'trust-filter.wot': 'WoT',
|
||||
'trust-filter.filter-threshold': 'เกณฑ์การกรอง',
|
||||
'trust-filter.quick-presets': 'ค่าที่ตั้งไว้ล่วงหน้าแบบเร็ว',
|
||||
'trust-filter.show-all-content': 'แสดงเนื้อหาทั้งหมด',
|
||||
'trust-filter.only-show-wot':
|
||||
'แสดงเฉพาะเครือข่ายความไว้วางใจของคุณ (ผู้ติดตาม + ผู้ติดตามของพวกเขา)',
|
||||
'trust-filter.hide-bottom-percent':
|
||||
'กรอง {{score}}% ล่างสุดของผู้ใช้ตามอันดับความไว้วางใจ',
|
||||
'trust-filter.trust-score-description':
|
||||
'คะแนนความไว้วางใจจัดอันดับผู้ใช้ตามเปอร์เซ็นไทล์ชื่อเสียง',
|
||||
'Auto-load profile pictures': 'โหลดรูปโปรไฟล์อัตโนมัติ'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -624,6 +624,18 @@ export default {
|
|||
'Enter Password': '輸入密碼',
|
||||
Password: '密碼',
|
||||
Confirm: '確認',
|
||||
'trust-filter.title': '信任分數過濾器',
|
||||
'trust-filter.off': '關閉',
|
||||
'trust-filter.low': '低',
|
||||
'trust-filter.medium': '中',
|
||||
'trust-filter.high': '高',
|
||||
'trust-filter.wot': '信任網路',
|
||||
'trust-filter.filter-threshold': '過濾閾值',
|
||||
'trust-filter.quick-presets': '快速預設',
|
||||
'trust-filter.show-all-content': '顯示所有內容',
|
||||
'trust-filter.only-show-wot': '僅顯示你的信任網路(關注的人 + 他們關注的人)',
|
||||
'trust-filter.hide-bottom-percent': '過濾掉信任度排名後 {{score}}% 的使用者',
|
||||
'trust-filter.trust-score-description': '信任分數按聲譽百分位對使用者進行排名',
|
||||
'Auto-load profile pictures': '自動載入大頭照'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -629,6 +629,18 @@ export default {
|
|||
'Enter Password': '输入密码',
|
||||
Password: '密码',
|
||||
Confirm: '确认',
|
||||
'trust-filter.title': '信任分数过滤器',
|
||||
'trust-filter.off': '关闭',
|
||||
'trust-filter.low': '低',
|
||||
'trust-filter.medium': '中',
|
||||
'trust-filter.high': '高',
|
||||
'trust-filter.wot': '信任网络',
|
||||
'trust-filter.filter-threshold': '过滤阈值',
|
||||
'trust-filter.quick-presets': '快速预设',
|
||||
'trust-filter.show-all-content': '显示所有内容',
|
||||
'trust-filter.only-show-wot': '仅显示你的信任网络(关注的人 + 他们关注的人)',
|
||||
'trust-filter.hide-bottom-percent': '过滤掉信任度排名后 {{score}}% 的用户',
|
||||
'trust-filter.trust-score-description': '信任分数按声誉百分位对用户进行排名',
|
||||
'Auto-load profile pictures': '自动加载头像'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,26 +2,24 @@ import { kinds, NostrEvent } from 'nostr-tools'
|
|||
import { isMentioningMutedUsers } from './event'
|
||||
import { tagNameEquals } from './tag'
|
||||
|
||||
export function notificationFilter(
|
||||
export async function notificationFilter(
|
||||
event: NostrEvent,
|
||||
{
|
||||
pubkey,
|
||||
mutePubkeySet,
|
||||
hideContentMentioningMutedUsers,
|
||||
hideUntrustedNotifications,
|
||||
isUserTrusted
|
||||
meetsMinTrustScore
|
||||
}: {
|
||||
pubkey?: string | null
|
||||
mutePubkeySet: Set<string>
|
||||
hideContentMentioningMutedUsers?: boolean
|
||||
hideUntrustedNotifications?: boolean
|
||||
isUserTrusted: (pubkey: string) => boolean
|
||||
meetsMinTrustScore: (pubkey: string, minScore?: number) => Promise<boolean>
|
||||
}
|
||||
): boolean {
|
||||
): Promise<boolean> {
|
||||
if (
|
||||
mutePubkeySet.has(event.pubkey) ||
|
||||
(hideContentMentioningMutedUsers && isMentioningMutedUsers(event, mutePubkeySet)) ||
|
||||
(hideUntrustedNotifications && !isUserTrusted(event.pubkey))
|
||||
!(await meetsMinTrustScore(event.pubkey))
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { BIG_RELAY_URLS, ExtendedKind } from '@/constants'
|
|||
import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
|
||||
import { getReplaceableEventIdentifier } from '@/lib/event'
|
||||
import { isLocalNetworkUrl, isOnionUrl, isWebsocketUrl } from '@/lib/url'
|
||||
import { useUserTrust } from '@/providers/UserTrustProvider'
|
||||
import storage from '@/services/local-storage.service'
|
||||
import { TPageRef } from '@/types'
|
||||
import { Compass, Plus } from 'lucide-react'
|
||||
|
|
@ -18,7 +17,6 @@ import { useTranslation } from 'react-i18next'
|
|||
type TExploreTabs = 'following' | 'explore' | 'reviews'
|
||||
|
||||
const ExplorePage = forwardRef<TPageRef>((_, ref) => {
|
||||
const { hideUntrustedNotes } = useUserTrust()
|
||||
const [tab, setTab] = useState<TExploreTabs>('explore')
|
||||
const topRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
|
|
@ -52,7 +50,7 @@ const ExplorePage = forwardRef<TPageRef>((_, ref) => {
|
|||
) : (
|
||||
<FollowingFavoriteRelayList />
|
||||
)
|
||||
}, [tab, relayReviewFilterFn, hideUntrustedNotes])
|
||||
}, [tab, relayReviewFilterFn])
|
||||
|
||||
return (
|
||||
<PrimaryPageLayout
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import HideUntrustedContentButton from '@/components/HideUntrustedContentButton'
|
||||
import NotificationList from '@/components/NotificationList'
|
||||
import TrustScoreFilter from '@/components/TrustScoreFilter'
|
||||
import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
|
||||
import { usePrimaryPage } from '@/PageManager'
|
||||
import { TPageRef } from '@/types'
|
||||
|
|
@ -42,7 +42,7 @@ function NotificationListPageTitlebar() {
|
|||
<Bell />
|
||||
<div className="text-lg font-semibold">{t('Notifications')}</div>
|
||||
</div>
|
||||
<HideUntrustedContentButton type="notifications" size="titlebar-icon" />
|
||||
<TrustScoreFilter />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import NoteList from '@/components/NoteList'
|
|||
import Tabs from '@/components/Tabs'
|
||||
import { BIG_RELAY_URLS } from '@/constants'
|
||||
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
|
||||
import { useUserTrust } from '@/providers/UserTrustProvider'
|
||||
import { kinds } from 'nostr-tools'
|
||||
import { forwardRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
|
@ -12,7 +11,6 @@ type TTab = 'my-packs' | 'explore'
|
|||
|
||||
const EmojiPackSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
|
||||
const { t } = useTranslation()
|
||||
const { hideUntrustedNotes } = useUserTrust()
|
||||
const [tab, setTab] = useState<TTab>('my-packs')
|
||||
|
||||
return (
|
||||
|
|
@ -33,7 +31,6 @@ const EmojiPackSettingsPage = forwardRef(({ index }: { index?: number }, ref) =>
|
|||
<NoteList
|
||||
showKinds={[kinds.Emojisets]}
|
||||
subRequests={[{ urls: BIG_RELAY_URLS, filter: {} }]}
|
||||
hideUntrustedNotes={hideUntrustedNotes}
|
||||
/>
|
||||
)}
|
||||
</SecondaryPageLayout>
|
||||
|
|
|
|||
|
|
@ -14,8 +14,7 @@ import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
|
|||
import { cn, isSupportCheckConnectionType } from '@/lib/utils'
|
||||
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
|
||||
import { useUserTrust } from '@/providers/UserTrustProvider'
|
||||
import { TMediaAutoLoadPolicy, TProfilePictureAutoLoadPolicy, TNsfwDisplayPolicy } from '@/types'
|
||||
import { TMediaAutoLoadPolicy, TNsfwDisplayPolicy, TProfilePictureAutoLoadPolicy } from '@/types'
|
||||
import { SelectValue } from '@radix-ui/react-select'
|
||||
import { RotateCcw } from 'lucide-react'
|
||||
import { forwardRef, HTMLProps, useState } from 'react'
|
||||
|
|
@ -36,7 +35,6 @@ const GeneralSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
|
|||
profilePictureAutoLoadPolicy,
|
||||
setProfilePictureAutoLoadPolicy
|
||||
} = useContentPolicy()
|
||||
const { hideUntrustedNotes, updateHideUntrustedNotes } = useUserTrust()
|
||||
const { quickReaction, updateQuickReaction, quickReactionEmoji, updateQuickReactionEmoji } =
|
||||
useUserPreferences()
|
||||
|
||||
|
|
@ -120,16 +118,6 @@ const GeneralSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
|
|||
</Label>
|
||||
<Switch id="autoplay" checked={autoplay} onCheckedChange={setAutoplay} />
|
||||
</SettingItem>
|
||||
<SettingItem>
|
||||
<Label htmlFor="hide-untrusted-notes" className="text-base font-normal">
|
||||
{t('Hide untrusted notes')}
|
||||
</Label>
|
||||
<Switch
|
||||
id="hide-untrusted-notes"
|
||||
checked={hideUntrustedNotes}
|
||||
onCheckedChange={updateHideUntrustedNotes}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem>
|
||||
<Label htmlFor="hide-content-mentioning-muted-users" className="text-base font-normal">
|
||||
{t('Hide content mentioning muted users')}
|
||||
|
|
|
|||
|
|
@ -33,42 +33,47 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
|
|||
const { current } = usePrimaryPage()
|
||||
const active = useMemo(() => current === 'notifications', [current])
|
||||
const { pubkey, notificationsSeenAt, updateNotificationsSeenAt } = useNostr()
|
||||
const { hideUntrustedNotifications, isUserTrusted } = useUserTrust()
|
||||
const { mutePubkeySet } = useMuteList()
|
||||
const { meetsMinTrustScore } = useUserTrust()
|
||||
const { hideContentMentioningMutedUsers } = useContentPolicy()
|
||||
const [newNotifications, setNewNotifications] = useState<NostrEvent[]>([])
|
||||
const [readNotificationIdSet, setReadNotificationIdSet] = useState<Set<string>>(new Set())
|
||||
const filteredNewNotifications = useMemo(() => {
|
||||
const [filteredNewNotifications, setFilteredNewNotifications] = useState<NostrEvent[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (active || notificationsSeenAt < 0) {
|
||||
return []
|
||||
setFilteredNewNotifications([])
|
||||
return
|
||||
}
|
||||
const filtered: NostrEvent[] = []
|
||||
for (const notification of newNotifications) {
|
||||
if (notification.created_at <= notificationsSeenAt || filtered.length >= 10) {
|
||||
break
|
||||
}
|
||||
if (
|
||||
!notificationFilter(notification, {
|
||||
pubkey,
|
||||
mutePubkeySet,
|
||||
hideContentMentioningMutedUsers,
|
||||
hideUntrustedNotifications,
|
||||
isUserTrusted
|
||||
const filterNotifications = async () => {
|
||||
const filtered: NostrEvent[] = []
|
||||
await Promise.allSettled(
|
||||
newNotifications.map(async (notification) => {
|
||||
if (notification.created_at <= notificationsSeenAt || filtered.length >= 10) {
|
||||
return
|
||||
}
|
||||
if (
|
||||
!(await notificationFilter(notification, {
|
||||
pubkey,
|
||||
mutePubkeySet,
|
||||
hideContentMentioningMutedUsers,
|
||||
meetsMinTrustScore
|
||||
}))
|
||||
) {
|
||||
return
|
||||
}
|
||||
filtered.push(notification)
|
||||
})
|
||||
) {
|
||||
continue
|
||||
}
|
||||
filtered.push(notification)
|
||||
)
|
||||
setFilteredNewNotifications(filtered)
|
||||
}
|
||||
return filtered
|
||||
filterNotifications()
|
||||
}, [
|
||||
newNotifications,
|
||||
notificationsSeenAt,
|
||||
mutePubkeySet,
|
||||
hideContentMentioningMutedUsers,
|
||||
hideUntrustedNotifications,
|
||||
isUserTrusted,
|
||||
active
|
||||
meetsMinTrustScore
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -5,14 +5,11 @@ import { createContext, useCallback, useContext, useEffect, useState } from 'rea
|
|||
import { useNostr } from './NostrProvider'
|
||||
|
||||
type TUserTrustContext = {
|
||||
hideUntrustedInteractions: boolean
|
||||
hideUntrustedNotifications: boolean
|
||||
hideUntrustedNotes: boolean
|
||||
updateHideUntrustedInteractions: (hide: boolean) => void
|
||||
updateHideUntrustedNotifications: (hide: boolean) => void
|
||||
updateHideUntrustedNotes: (hide: boolean) => void
|
||||
minTrustScore: number
|
||||
updateMinTrustScore: (score: number) => void
|
||||
isUserTrusted: (pubkey: string) => boolean
|
||||
isSpammer: (pubkey: string) => Promise<boolean>
|
||||
meetsMinTrustScore: (pubkey: string, minScore?: number) => Promise<boolean>
|
||||
}
|
||||
|
||||
const UserTrustContext = createContext<TUserTrustContext | undefined>(undefined)
|
||||
|
|
@ -29,15 +26,7 @@ const wotSet = new Set<string>()
|
|||
|
||||
export function UserTrustProvider({ children }: { children: React.ReactNode }) {
|
||||
const { pubkey: currentPubkey } = useNostr()
|
||||
const [hideUntrustedInteractions, setHideUntrustedInteractions] = useState(() =>
|
||||
storage.getHideUntrustedInteractions()
|
||||
)
|
||||
const [hideUntrustedNotifications, setHideUntrustedNotifications] = useState(() =>
|
||||
storage.getHideUntrustedNotifications()
|
||||
)
|
||||
const [hideUntrustedNotes, setHideUntrustedNotes] = useState(() =>
|
||||
storage.getHideUntrustedNotes()
|
||||
)
|
||||
const [minTrustScore, setMinTrustScore] = useState(() => storage.getMinTrustScore())
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentPubkey) return
|
||||
|
|
@ -81,32 +70,36 @@ export function UserTrustProvider({ children }: { children: React.ReactNode }) {
|
|||
[isUserTrusted]
|
||||
)
|
||||
|
||||
const updateHideUntrustedInteractions = (hide: boolean) => {
|
||||
setHideUntrustedInteractions(hide)
|
||||
storage.setHideUntrustedInteractions(hide)
|
||||
const updateMinTrustScore = (score: number) => {
|
||||
setMinTrustScore(score)
|
||||
storage.setMinTrustScore(score)
|
||||
}
|
||||
|
||||
const updateHideUntrustedNotifications = (hide: boolean) => {
|
||||
setHideUntrustedNotifications(hide)
|
||||
storage.setHideUntrustedNotifications(hide)
|
||||
}
|
||||
const meetsMinTrustScore = useCallback(
|
||||
async (pubkey: string, minScore?: number) => {
|
||||
const threshold = minScore !== undefined ? minScore : minTrustScore
|
||||
if (threshold === 0) return true
|
||||
if (pubkey === currentPubkey) return true
|
||||
|
||||
const updateHideUntrustedNotes = (hide: boolean) => {
|
||||
setHideUntrustedNotes(hide)
|
||||
storage.setHideUntrustedNotes(hide)
|
||||
}
|
||||
// WoT users always have 100% trust score
|
||||
if (wotSet.has(pubkey)) return true
|
||||
|
||||
// Get percentile from reputation system
|
||||
const percentile = await fayan.fetchUserPercentile(pubkey)
|
||||
if (percentile === null) return true // If no data, indicate the trust server is down, so allow the user
|
||||
return percentile >= threshold
|
||||
},
|
||||
[currentPubkey, minTrustScore]
|
||||
)
|
||||
|
||||
return (
|
||||
<UserTrustContext.Provider
|
||||
value={{
|
||||
hideUntrustedInteractions,
|
||||
hideUntrustedNotifications,
|
||||
hideUntrustedNotes,
|
||||
updateHideUntrustedInteractions,
|
||||
updateHideUntrustedNotifications,
|
||||
updateHideUntrustedNotes,
|
||||
minTrustScore,
|
||||
updateMinTrustScore,
|
||||
isUserTrusted,
|
||||
isSpammer
|
||||
isSpammer,
|
||||
meetsMinTrustScore
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { userIdToPubkey } from '@/lib/pubkey'
|
||||
import DataLoader from 'dataloader'
|
||||
|
||||
class FayanService {
|
||||
|
|
@ -22,7 +23,12 @@ class FayanService {
|
|||
return new Array(pubkeys.length).fill(null)
|
||||
}
|
||||
},
|
||||
{ maxBatchSize: 50 }
|
||||
{
|
||||
maxBatchSize: 50,
|
||||
cacheKeyFn: (userId) => {
|
||||
return userIdToPubkey(userId)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
constructor() {
|
||||
|
|
|
|||
|
|
@ -45,9 +45,6 @@ class LocalStorageService {
|
|||
private accountFeedInfoMap: Record<string, TFeedInfo | undefined> = {}
|
||||
private mediaUploadService: string = DEFAULT_NIP_96_SERVICE
|
||||
private autoplay: boolean = true
|
||||
private hideUntrustedInteractions: boolean = false
|
||||
private hideUntrustedNotifications: boolean = false
|
||||
private hideUntrustedNotes: boolean = false
|
||||
private translationServiceConfigMap: Record<string, TTranslationServiceConfig> = {}
|
||||
private mediaUploadServiceConfigMap: Record<string, TMediaUploadServiceConfig> = {}
|
||||
private dismissedTooManyRelaysAlert: boolean = false
|
||||
|
|
@ -66,6 +63,7 @@ class LocalStorageService {
|
|||
private quickReaction: boolean = false
|
||||
private quickReactionEmoji: string | TEmoji = '+'
|
||||
private nsfwDisplayPolicy: TNsfwDisplayPolicy = NSFW_DISPLAY_POLICY.HIDE_CONTENT
|
||||
private minTrustScore: number = 40
|
||||
|
||||
constructor() {
|
||||
if (!LocalStorageService.instance) {
|
||||
|
|
@ -134,25 +132,6 @@ class LocalStorageService {
|
|||
|
||||
this.autoplay = window.localStorage.getItem(StorageKey.AUTOPLAY) !== 'false'
|
||||
|
||||
const hideUntrustedEvents =
|
||||
window.localStorage.getItem(StorageKey.HIDE_UNTRUSTED_EVENTS) === 'true'
|
||||
const storedHideUntrustedInteractions = window.localStorage.getItem(
|
||||
StorageKey.HIDE_UNTRUSTED_INTERACTIONS
|
||||
)
|
||||
const storedHideUntrustedNotifications = window.localStorage.getItem(
|
||||
StorageKey.HIDE_UNTRUSTED_NOTIFICATIONS
|
||||
)
|
||||
const storedHideUntrustedNotes = window.localStorage.getItem(StorageKey.HIDE_UNTRUSTED_NOTES)
|
||||
this.hideUntrustedInteractions = storedHideUntrustedInteractions
|
||||
? storedHideUntrustedInteractions === 'true'
|
||||
: hideUntrustedEvents
|
||||
this.hideUntrustedNotifications = storedHideUntrustedNotifications
|
||||
? storedHideUntrustedNotifications === 'true'
|
||||
: hideUntrustedEvents
|
||||
this.hideUntrustedNotes = storedHideUntrustedNotes
|
||||
? storedHideUntrustedNotes === 'true'
|
||||
: hideUntrustedEvents
|
||||
|
||||
const translationServiceConfigMapStr = window.localStorage.getItem(
|
||||
StorageKey.TRANSLATION_SERVICE_CONFIG_MAP
|
||||
)
|
||||
|
|
@ -274,6 +253,28 @@ class LocalStorageService {
|
|||
this.quickReactionEmoji = quickReactionEmojiStr
|
||||
}
|
||||
|
||||
const minTrustScoreStr = window.localStorage.getItem(StorageKey.MIN_TRUST_SCORE)
|
||||
if (minTrustScoreStr) {
|
||||
const score = parseInt(minTrustScoreStr, 10)
|
||||
if (!isNaN(score) && score >= 0 && score <= 100) {
|
||||
this.minTrustScore = score
|
||||
}
|
||||
} else {
|
||||
const storedHideUntrustedInteractions =
|
||||
window.localStorage.getItem(StorageKey.HIDE_UNTRUSTED_INTERACTIONS) === 'true'
|
||||
const storedHideUntrustedNotifications =
|
||||
window.localStorage.getItem(StorageKey.HIDE_UNTRUSTED_NOTIFICATIONS) === 'true'
|
||||
const storedHideUntrustedNotes =
|
||||
window.localStorage.getItem(StorageKey.HIDE_UNTRUSTED_NOTES) === 'true'
|
||||
if (
|
||||
storedHideUntrustedInteractions ||
|
||||
storedHideUntrustedNotifications ||
|
||||
storedHideUntrustedNotes
|
||||
) {
|
||||
this.minTrustScore = 100 // set to max if any of the old settings were true
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up deprecated data
|
||||
window.localStorage.removeItem(StorageKey.PINNED_PUBKEYS)
|
||||
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)
|
||||
|
|
@ -425,39 +426,6 @@ class LocalStorageService {
|
|||
window.localStorage.setItem(StorageKey.AUTOPLAY, autoplay.toString())
|
||||
}
|
||||
|
||||
getHideUntrustedInteractions() {
|
||||
return this.hideUntrustedInteractions
|
||||
}
|
||||
|
||||
setHideUntrustedInteractions(hideUntrustedInteractions: boolean) {
|
||||
this.hideUntrustedInteractions = hideUntrustedInteractions
|
||||
window.localStorage.setItem(
|
||||
StorageKey.HIDE_UNTRUSTED_INTERACTIONS,
|
||||
hideUntrustedInteractions.toString()
|
||||
)
|
||||
}
|
||||
|
||||
getHideUntrustedNotifications() {
|
||||
return this.hideUntrustedNotifications
|
||||
}
|
||||
|
||||
setHideUntrustedNotifications(hideUntrustedNotifications: boolean) {
|
||||
this.hideUntrustedNotifications = hideUntrustedNotifications
|
||||
window.localStorage.setItem(
|
||||
StorageKey.HIDE_UNTRUSTED_NOTIFICATIONS,
|
||||
hideUntrustedNotifications.toString()
|
||||
)
|
||||
}
|
||||
|
||||
getHideUntrustedNotes() {
|
||||
return this.hideUntrustedNotes
|
||||
}
|
||||
|
||||
setHideUntrustedNotes(hideUntrustedNotes: boolean) {
|
||||
this.hideUntrustedNotes = hideUntrustedNotes
|
||||
window.localStorage.setItem(StorageKey.HIDE_UNTRUSTED_NOTES, hideUntrustedNotes.toString())
|
||||
}
|
||||
|
||||
getTranslationServiceConfig(pubkey?: string | null) {
|
||||
return this.translationServiceConfigMap[pubkey ?? '_'] ?? { service: 'jumble' }
|
||||
}
|
||||
|
|
@ -633,6 +601,17 @@ class LocalStorageService {
|
|||
this.nsfwDisplayPolicy = policy
|
||||
window.localStorage.setItem(StorageKey.NSFW_DISPLAY_POLICY, policy)
|
||||
}
|
||||
|
||||
getMinTrustScore() {
|
||||
return this.minTrustScore
|
||||
}
|
||||
|
||||
setMinTrustScore(score: number) {
|
||||
if (score >= 0 && score <= 100) {
|
||||
this.minTrustScore = score
|
||||
window.localStorage.setItem(StorageKey.MIN_TRUST_SCORE, score.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const instance = new LocalStorageService()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue