feat: support configurable trust score threshold per context

This commit is contained in:
codytseng 2026-01-14 23:20:28 +08:00
parent 28a1b3096a
commit ca9610b711
46 changed files with 350 additions and 122 deletions

View file

@ -1,5 +1,6 @@
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
import { Separator } from '@/components/ui/separator'
import { SPECIAL_TRUST_SCORE_FILTER_ID } from '@/constants'
import { useState } from 'react'
import QuoteList from '../QuoteList'
import ReactionList from '../ReactionList'
@ -37,7 +38,7 @@ export default function ExternalContentInteractions({
</ScrollArea>
<Separator orientation="vertical" className="h-6" />
<div className="size-10 flex items-center justify-center">
<TrustScoreFilter />
<TrustScoreFilter filterId={SPECIAL_TRUST_SCORE_FILTER_ID.INTERACTIONS} />
</div>
</div>
<Separator />

View file

@ -4,6 +4,7 @@ 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'
@ -11,6 +12,7 @@ import KindFilter from '../KindFilter'
import { RefreshButton } from '../RefreshButton'
export default function NormalFeed({
trustScoreFilterId,
subRequests,
areAlgoRelays = false,
isMainFeed = false,
@ -19,6 +21,7 @@ export default function NormalFeed({
onRefresh,
isPubkeyFeed = false
}: {
trustScoreFilterId?: string
subRequests: TFeedSubRequest[]
areAlgoRelays?: boolean
isMainFeed?: boolean
@ -28,6 +31,7 @@ export default function NormalFeed({
isPubkeyFeed?: boolean
}) {
const { showKinds } = useKindFilter()
const { getMinTrustScore } = useUserTrust()
const [temporaryShowKinds, setTemporaryShowKinds] = useState(showKinds)
const [listMode, setListMode] = useState<TNoteListMode>(() => storage.getNoteListMode())
const supportTouch = useMemo(() => isTouchDevice(), [])
@ -38,6 +42,9 @@ export default function NormalFeed({
return subRequests.every((req) => !req.filter.kinds?.length)
}, [subRequests])
const [trustFilterOpen, setTrustFilterOpen] = useState(false)
const trustScoreThreshold = useMemo(() => {
return trustScoreFilterId ? getMinTrustScore(trustScoreFilterId) : undefined
}, [trustScoreFilterId, getMinTrustScore])
const handleListModeChange = (mode: TNoteListMode) => {
setListMode(mode)
@ -85,7 +92,12 @@ export default function NormalFeed({
}}
/>
)}
{!isPubkeyFeed && <TrustScoreFilter onOpenChange={handleTrustFilterOpenChange} />}
{trustScoreFilterId && (
<TrustScoreFilter
filterId={trustScoreFilterId}
onOpenChange={handleTrustFilterOpenChange}
/>
)}
{showKindsFilter && (
<KindFilter
showKinds={temporaryShowKinds}
@ -105,6 +117,7 @@ export default function NormalFeed({
areAlgoRelays={areAlgoRelays}
showRelayCloseReason={showRelayCloseReason}
isPubkeyFeed={isPubkeyFeed}
trustScoreThreshold={trustScoreThreshold}
/>
) : (
<NoteList
@ -115,6 +128,7 @@ export default function NormalFeed({
areAlgoRelays={areAlgoRelays}
showRelayCloseReason={showRelayCloseReason}
isPubkeyFeed={isPubkeyFeed}
trustScoreThreshold={trustScoreThreshold}
/>
)}
</>

View file

@ -1,5 +1,6 @@
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
import { Separator } from '@/components/ui/separator'
import { SPECIAL_TRUST_SCORE_FILTER_ID } from '@/constants'
import { Event } from 'nostr-tools'
import { useState } from 'react'
import QuoteList from '../QuoteList'
@ -42,7 +43,7 @@ export default function NoteInteractions({ event }: { event: Event }) {
<ScrollBar orientation="horizontal" className="opacity-0 pointer-events-none" />
</ScrollArea>
<Separator orientation="vertical" className="h-6" />
<TrustScoreFilter />
<TrustScoreFilter filterId={SPECIAL_TRUST_SCORE_FILTER_ID.INTERACTIONS} />
</div>
<Separator />
{list}

View file

@ -51,13 +51,13 @@ const NoteList = forwardRef<
filterMutedNotes?: boolean
hideReplies?: boolean
hideSpam?: boolean
trustScoreThreshold?: number
areAlgoRelays?: boolean
showRelayCloseReason?: boolean
pinnedEventIds?: string[]
filterFn?: (event: Event) => boolean
showNewNotesDirectly?: boolean
isPubkeyFeed?: boolean
disableTrustFilter?: boolean
}
>(
(
@ -67,13 +67,13 @@ const NoteList = forwardRef<
filterMutedNotes = true,
hideReplies = false,
hideSpam = false,
trustScoreThreshold,
areAlgoRelays = false,
showRelayCloseReason = false,
pinnedEventIds,
filterFn,
showNewNotesDirectly = false,
isPubkeyFeed = false,
disableTrustFilter = false
isPubkeyFeed = false
},
ref
) => {
@ -106,20 +106,23 @@ const NoteList = forwardRef<
const showNewNotesDirectlyRef = useRef(showNewNotesDirectly)
showNewNotesDirectlyRef.current = showNewNotesDirectly
const pinnedEventHexIdSet = useMemo(() => {
const set = new Set<string>()
pinnedEventIds?.forEach((id) => {
try {
const { type, data } = decode(id)
if (type === 'nevent') {
set.add(data.id)
}
} catch {
// ignore
}
})
return set
}, [pinnedEventIds?.join(',')])
const shouldHideEvent = useCallback(
(evt: Event) => {
const pinnedEventHexIdSet = new Set()
pinnedEventIds?.forEach((id) => {
try {
const { type, data } = decode(id)
if (type === 'nevent') {
pinnedEventHexIdSet.add(data.id)
}
} catch {
// ignore
}
})
if (pinnedEventHexIdSet.has(evt.id)) return true
if (isEventDeleted(evt)) return true
if (filterMutedNotes && mutePubkeySet.has(evt.pubkey)) return true
@ -144,7 +147,7 @@ const NoteList = forwardRef<
return false
},
[mutePubkeySet, JSON.stringify(pinnedEventIds), isEventDeleted, filterFn, mutedWords]
[mutePubkeySet, isEventDeleted, filterFn, mutedWords, pinnedEventHexIdSet]
)
useEffect(() => {
@ -222,7 +225,10 @@ const NoteList = forwardRef<
}
})
if (disableTrustFilter) {
const _trustScoreThreshold = hideSpam
? SPAMMER_PERCENTILE_THRESHOLD
: (trustScoreThreshold ?? 0)
if (!_trustScoreThreshold || _trustScoreThreshold <= 0) {
setFilteredNotes(
filteredEvents.map((evt, i) => {
const key = keys[i]
@ -236,12 +242,7 @@ const NoteList = forwardRef<
await Promise.all(
filteredEvents.map(async (evt, i) => {
// Check trust score filter
if (
!(await meetsMinTrustScore(
evt.pubkey,
hideSpam ? SPAMMER_PERCENTILE_THRESHOLD : undefined
))
) {
if (!(await meetsMinTrustScore(evt.pubkey, _trustScoreThreshold))) {
return null
}
const key = keys[i]
@ -266,7 +267,7 @@ const NoteList = forwardRef<
hideReplies,
hideSpam,
meetsMinTrustScore,
disableTrustFilter
trustScoreThreshold
])
useEffect(() => {
@ -286,6 +287,14 @@ const NoteList = forwardRef<
filteredEvents.push(event)
})
const _trustScoreThreshold = hideSpam
? SPAMMER_PERCENTILE_THRESHOLD
: (trustScoreThreshold ?? 0)
if (!_trustScoreThreshold || _trustScoreThreshold <= 0) {
setFilteredNewEvents(filteredEvents)
return
}
const _filteredNotes = (
await Promise.all(
filteredEvents.map(async (evt) => {
@ -293,7 +302,7 @@ const NoteList = forwardRef<
return null
}
// Check trust score filter
if (!(await meetsMinTrustScore(evt.pubkey))) {
if (!(await meetsMinTrustScore(evt.pubkey, _trustScoreThreshold))) {
return null
}
return evt
@ -303,7 +312,7 @@ const NoteList = forwardRef<
setFilteredNewEvents(_filteredNotes)
}
processNewEvents()
}, [newEvents, shouldHideEvent, isSpammer, hideSpam, meetsMinTrustScore])
}, [newEvents, shouldHideEvent, isSpammer, hideSpam, meetsMinTrustScore, trustScoreThreshold])
const scrollToTop = (behavior: ScrollBehavior = 'instant') => {
setTimeout(() => {

View file

@ -1,4 +1,4 @@
import { ExtendedKind } from '@/constants'
import { ExtendedKind, SPECIAL_TRUST_SCORE_FILTER_ID } from '@/constants'
import { isMentioningMutedUsers } from '@/lib/event'
import { tagNameEquals } from '@/lib/tag'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
@ -24,7 +24,7 @@ export function NotificationItem({
const { pubkey } = useNostr()
const { mutePubkeySet } = useMuteList()
const { hideContentMentioningMutedUsers } = useContentPolicy()
const { minTrustScore, meetsMinTrustScore } = useUserTrust()
const { getMinTrustScore, meetsMinTrustScore } = useUserTrust()
const [canShow, setCanShow] = useState(false)
useEffect(() => {
@ -42,9 +42,12 @@ export function NotificationItem({
}
// Check trust score
if (notification.kind !== kinds.Zap && !(await meetsMinTrustScore(notification.pubkey))) {
setCanShow(false)
return
if (notification.kind !== kinds.Zap) {
const threshold = getMinTrustScore(SPECIAL_TRUST_SCORE_FILTER_ID.NOTIFICATIONS)
if (!(await meetsMinTrustScore(notification.pubkey, threshold))) {
setCanShow(false)
return
}
}
// Check reaction target for kind 7
@ -65,7 +68,7 @@ export function NotificationItem({
pubkey,
mutePubkeySet,
hideContentMentioningMutedUsers,
minTrustScore,
getMinTrustScore,
meetsMinTrustScore
])

View file

@ -177,7 +177,6 @@ export default function ProfileFeed({
filterMutedNotes={false}
pinnedEventIds={listMode === 'you' || !!search ? [] : pinnedEventIds}
showNewNotesDirectly={myPubkey === pubkey}
disableTrustFilter
/>
</>
)

View file

@ -1,4 +1,5 @@
import { useSecondaryPage } from '@/PageManager'
import { SPECIAL_TRUST_SCORE_FILTER_ID } from '@/constants'
import { useStuff } from '@/hooks/useStuff'
import { useStuffStatsById } from '@/hooks/useStuffStatsById'
import { toProfile } from '@/lib/link'
@ -20,7 +21,7 @@ export default function ReactionList({ stuff }: { stuff: Event | string }) {
const { t } = useTranslation()
const { push } = useSecondaryPage()
const { isSmallScreen } = useScreenSize()
const { minTrustScore, meetsMinTrustScore } = useUserTrust()
const { getMinTrustScore, meetsMinTrustScore } = useUserTrust()
const { stuffKey } = useStuff(stuff)
const noteStats = useStuffStatsById(stuffKey)
const [filteredLikes, setFilteredLikes] = useState<
@ -41,18 +42,23 @@ export default function ReactionList({ stuff }: { stuff: Event | string }) {
created_at: number
emoji: string | TEmoji
}[] = []
await Promise.all(
likes.map(async (like) => {
if (await meetsMinTrustScore(like.pubkey)) {
filtered.push(like)
}
})
)
const threshold = getMinTrustScore(SPECIAL_TRUST_SCORE_FILTER_ID.INTERACTIONS)
if (threshold) {
await Promise.all(
likes.map(async (like) => {
if (await meetsMinTrustScore(like.pubkey, threshold)) {
filtered.push(like)
}
})
)
} else {
filtered.push(...likes)
}
filtered.sort((a, b) => b.created_at - a.created_at)
setFilteredLikes(filtered)
}
filterLikes()
}, [noteStats, stuffKey, minTrustScore, meetsMinTrustScore])
}, [noteStats, stuffKey, getMinTrustScore, meetsMinTrustScore])
const [showCount, setShowCount] = useState(SHOW_COUNT)
const bottomRef = useRef<HTMLDivElement | null>(null)

View file

@ -52,6 +52,7 @@ export default function Relay({ url, className }: { url?: string; className?: st
</div>
)}
<NormalFeed
trustScoreFilterId={`relay-${normalizedUrl}`}
subRequests={[
{ urls: [normalizedUrl], filter: debouncedInput ? { search: debouncedInput } : {} }
]}

View file

@ -1,6 +1,7 @@
import { useSecondaryPage } from '@/PageManager'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import { SPECIAL_TRUST_SCORE_FILTER_ID } from '@/constants'
import { useThread } from '@/hooks/useThread'
import { getEventKey, isMentioningMutedUsers } from '@/lib/event'
import { toNote } from '@/lib/link'
@ -42,7 +43,7 @@ export default function ReplyNote({
const { isSmallScreen } = useScreenSize()
const { push } = useSecondaryPage()
const { mutePubkeySet } = useMuteList()
const { minTrustScore, meetsMinTrustScore } = useUserTrust()
const { getMinTrustScore, meetsMinTrustScore } = useUserTrust()
const { hideContentMentioningMutedUsers } = useContentPolicy()
const eventKey = useMemo(() => getEventKey(event), [event])
const replies = useThread(eventKey)
@ -69,6 +70,7 @@ export default function ReplyNote({
return
}
const trustScoreThreshold = getMinTrustScore(SPECIAL_TRUST_SCORE_FILTER_ID.INTERACTIONS)
for (const reply of replies) {
if (mutePubkeySet.has(reply.pubkey)) {
continue
@ -76,7 +78,7 @@ export default function ReplyNote({
if (hideContentMentioningMutedUsers && isMentioningMutedUsers(reply, mutePubkeySet)) {
continue
}
if (!(await meetsMinTrustScore(reply.pubkey))) {
if (trustScoreThreshold && !(await meetsMinTrustScore(reply.pubkey, trustScoreThreshold))) {
continue
}
setHasReplies(true)
@ -86,7 +88,13 @@ export default function ReplyNote({
}
checkHasReplies()
}, [replies, minTrustScore, meetsMinTrustScore, mutePubkeySet, hideContentMentioningMutedUsers])
}, [
replies,
getMinTrustScore,
meetsMinTrustScore,
mutePubkeySet,
hideContentMentioningMutedUsers
])
return (
<div

View file

@ -1,4 +1,5 @@
import { useSecondaryPage } from '@/PageManager'
import { SPECIAL_TRUST_SCORE_FILTER_ID } from '@/constants'
import { useStuffStatsById } from '@/hooks/useStuffStatsById'
import { getEventKey } from '@/lib/event'
import { toProfile } from '@/lib/link'
@ -19,7 +20,7 @@ export default function RepostList({ event }: { event: Event }) {
const { t } = useTranslation()
const { push } = useSecondaryPage()
const { isSmallScreen } = useScreenSize()
const { minTrustScore, meetsMinTrustScore } = useUserTrust()
const { getMinTrustScore, meetsMinTrustScore } = useUserTrust()
const noteStats = useStuffStatsById(getEventKey(event))
const [filteredReposts, setFilteredReposts] = useState<
Array<{ id: string; pubkey: string; created_at: number }>
@ -28,10 +29,15 @@ export default function RepostList({ event }: { event: Event }) {
useEffect(() => {
const filterReposts = async () => {
const reposts = noteStats?.reposts ?? []
const trustScoreThreshold = getMinTrustScore(SPECIAL_TRUST_SCORE_FILTER_ID.INTERACTIONS)
if (!trustScoreThreshold) {
setFilteredReposts([...reposts].sort((a, b) => b.created_at - a.created_at))
return
}
const filtered = (
await Promise.all(
reposts.map(async (repost) => {
if (await meetsMinTrustScore(repost.pubkey)) {
if (await meetsMinTrustScore(repost.pubkey, trustScoreThreshold)) {
return repost
}
})
@ -45,7 +51,7 @@ export default function RepostList({ event }: { event: Event }) {
setFilteredReposts(filtered)
}
filterReposts()
}, [noteStats, event.id, minTrustScore, meetsMinTrustScore])
}, [noteStats, event.id, getMinTrustScore, meetsMinTrustScore])
const [showCount, setShowCount] = useState(SHOW_COUNT)
const bottomRef = useRef<HTMLDivElement | null>(null)

View file

@ -1,4 +1,4 @@
import { SEARCHABLE_RELAY_URLS } from '@/constants'
import { SEARCHABLE_RELAY_URLS, SPECIAL_TRUST_SCORE_FILTER_ID } from '@/constants'
import { getDefaultRelayUrls } from '@/lib/relay'
import { TSearchParams } from '@/types'
import NormalFeed from '../NormalFeed'
@ -20,6 +20,7 @@ export default function SearchResult({ searchParams }: { searchParams: TSearchPa
if (searchParams.type === 'notes') {
return (
<NormalFeed
trustScoreFilterId={SPECIAL_TRUST_SCORE_FILTER_ID.SEARCH}
subRequests={[{ urls: SEARCHABLE_RELAY_URLS, filter: { search: searchParams.search } }]}
showRelayCloseReason
/>
@ -28,13 +29,20 @@ export default function SearchResult({ searchParams }: { searchParams: TSearchPa
if (searchParams.type === 'hashtag') {
return (
<NormalFeed
trustScoreFilterId={SPECIAL_TRUST_SCORE_FILTER_ID.HASHTAG}
subRequests={[{ urls: getDefaultRelayUrls(), filter: { '#t': [searchParams.search] } }]}
showRelayCloseReason
/>
)
}
if (searchParams.type === 'nak') {
return <NormalFeed subRequests={[searchParams.request]} showRelayCloseReason />
return (
<NormalFeed
trustScoreFilterId={SPECIAL_TRUST_SCORE_FILTER_ID.NAK}
subRequests={[searchParams.request]}
showRelayCloseReason
/>
)
}
return <Relay url={searchParams.search} />
}

View file

@ -1,6 +1,6 @@
import { Drawer, DrawerContent, DrawerOverlay } from '@/components/ui/drawer'
import { Popover, PopoverAnchor, PopoverContent } from '@/components/ui/popover'
import { LONG_PRESS_THRESHOLD } from '@/constants'
import { LONG_PRESS_THRESHOLD, SPECIAL_TRUST_SCORE_FILTER_ID } from '@/constants'
import { useStuff } from '@/hooks/useStuff'
import { useStuffStatsById } from '@/hooks/useStuffStatsById'
import {
@ -28,7 +28,7 @@ export default function LikeButton({ stuff }: { stuff: Event | string }) {
const { t } = useTranslation()
const { isSmallScreen } = useScreenSize()
const { pubkey, publish, checkLogin } = useNostr()
const { meetsMinTrustScore } = useUserTrust()
const { getMinTrustScore, meetsMinTrustScore } = useUserTrust()
const { quickReaction, quickReactionEmoji } = useUserPreferences()
const { event, externalContent, stuffKey } = useStuff(stuff)
const [liking, setLiking] = useState(false)
@ -49,9 +49,15 @@ export default function LikeButton({ stuff }: { stuff: Event | string }) {
const stats = noteStats || {}
const likes = stats.likes || []
let count = 0
const trustScoreThreshold = getMinTrustScore(SPECIAL_TRUST_SCORE_FILTER_ID.INTERACTIONS)
if (!trustScoreThreshold) {
setLikeCount(likes.length)
return
}
await Promise.all(
likes.map(async (like) => {
if (await meetsMinTrustScore(like.pubkey)) {
if (await meetsMinTrustScore(like.pubkey, trustScoreThreshold)) {
count++
}
})
@ -59,7 +65,7 @@ export default function LikeButton({ stuff }: { stuff: Event | string }) {
setLikeCount(count)
}
filterLikes()
}, [noteStats, meetsMinTrustScore])
}, [noteStats, meetsMinTrustScore, getMinTrustScore])
useEffect(() => {
setTimeout(() => setIsPickerOpen(false), 100)

View file

@ -21,11 +21,12 @@ import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import PostEditor from '../PostEditor'
import { formatCount } from './utils'
import { SPECIAL_TRUST_SCORE_FILTER_ID } from '@/constants'
export default function RepostButton({ stuff }: { stuff: Event | string }) {
const { t } = useTranslation()
const { isSmallScreen } = useScreenSize()
const { meetsMinTrustScore } = useUserTrust()
const { getMinTrustScore, meetsMinTrustScore } = useUserTrust()
const { publish, checkLogin, pubkey } = useNostr()
const { event, stuffKey } = useStuff(stuff)
const noteStats = useStuffStatsById(stuffKey)
@ -46,9 +47,15 @@ export default function RepostButton({ stuff }: { stuff: Event | string }) {
const reposts = noteStats?.reposts || []
let count = 0
const trustScoreThreshold = getMinTrustScore(SPECIAL_TRUST_SCORE_FILTER_ID.INTERACTIONS)
if (!trustScoreThreshold) {
setRepostCount(reposts.length)
return
}
await Promise.all(
reposts.map(async (repost) => {
if (await meetsMinTrustScore(repost.pubkey)) {
if (await meetsMinTrustScore(repost.pubkey, trustScoreThreshold)) {
count++
}
})
@ -56,7 +63,7 @@ export default function RepostButton({ stuff }: { stuff: Event | string }) {
setRepostCount(count)
}
filterReposts()
}, [noteStats, event, meetsMinTrustScore])
}, [noteStats, event, meetsMinTrustScore, getMinTrustScore])
const canRepost = !hasReposted && !reposting && !!event
const repost = async () => {

View file

@ -1,4 +1,4 @@
import { TRENDING_NOTES_RELAY_URLS } from '@/constants'
import { SPECIAL_TRUST_SCORE_FILTER_ID, TRENDING_NOTES_RELAY_URLS } from '@/constants'
import { simplifyUrl } from '@/lib/url'
import { useTranslation } from 'react-i18next'
import NormalFeed from '../NormalFeed'
@ -19,6 +19,7 @@ export default function TrendingNotes() {
</div>
</div>
<NormalFeed
trustScoreFilterId={SPECIAL_TRUST_SCORE_FILTER_ID.TRENDING}
subRequests={[{ urls: TRENDING_NOTES_RELAY_URLS, filter: {} }]}
showRelayCloseReason
/>

View file

@ -28,20 +28,22 @@ function getDescription(score: number, t: (key: string, options?: any) => string
}
export default function TrustScoreFilter({
filterId,
onOpenChange
}: {
filterId: string
onOpenChange?: (open: boolean) => void
}) {
const { t } = useTranslation()
const { isSmallScreen } = useScreenSize()
const { minTrustScore, updateMinTrustScore } = useUserTrust()
const { getMinTrustScore, updateMinTrustScore } = useUserTrust()
const [open, setOpen] = useState(false)
const [temporaryScore, setTemporaryScore] = useState(minTrustScore)
const [temporaryScore, setTemporaryScore] = useState(0)
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null)
useEffect(() => {
setTemporaryScore(minTrustScore)
}, [minTrustScore])
setTemporaryScore(getMinTrustScore(filterId))
}, [getMinTrustScore, filterId])
// Debounced update function
const handleScoreChange = (newScore: number) => {
@ -54,7 +56,7 @@ export default function TrustScoreFilter({
// Set new timer for debounced update
debounceTimerRef.current = setTimeout(() => {
updateMinTrustScore(newScore)
updateMinTrustScore(filterId, newScore)
}, 300) // 300ms debounce delay
}
@ -81,7 +83,7 @@ export default function TrustScoreFilter({
size="titlebar-icon"
className={cn(
'relative',
minTrustScore === 0
temporaryScore === 0
? 'text-muted-foreground hover:text-foreground'
: 'text-primary hover:text-primary-hover'
)}
@ -89,10 +91,10 @@ export default function TrustScoreFilter({
setOpen(true)
}}
>
{minTrustScore < 100 ? <Shield size={16} /> : <ShieldCheck size={16} />}
{minTrustScore > 0 && minTrustScore < 100 && (
{temporaryScore < 100 ? <Shield size={16} /> : <ShieldCheck size={16} />}
{temporaryScore > 0 && temporaryScore < 100 && (
<div className="absolute inset-0 w-full h-full flex flex-col items-center justify-center text-[0.5rem] font-mono font-bold">
{minTrustScore}
{temporaryScore}
</div>
)}
</Button>

View file

@ -55,6 +55,7 @@ const UserAggregationList = forwardRef<
areAlgoRelays?: boolean
showRelayCloseReason?: boolean
isPubkeyFeed?: boolean
trustScoreThreshold?: number
}
>(
(
@ -64,7 +65,8 @@ const UserAggregationList = forwardRef<
filterMutedNotes = true,
areAlgoRelays = false,
showRelayCloseReason = false,
isPubkeyFeed = false
isPubkeyFeed = false,
trustScoreThreshold
},
ref
) => {
@ -282,7 +284,10 @@ const UserAggregationList = forwardRef<
) {
return null
}
if (!(await meetsMinTrustScore(evt.pubkey))) {
if (
trustScoreThreshold &&
!(await meetsMinTrustScore(evt.pubkey, trustScoreThreshold))
) {
return null
}
@ -300,7 +305,8 @@ const UserAggregationList = forwardRef<
filterMutedNotes,
hideContentMentioningMutedUsers,
isMentioningMutedUsers,
meetsMinTrustScore
meetsMinTrustScore,
trustScoreThreshold
]
)

View file

@ -41,9 +41,10 @@ export const StorageKey = {
QUICK_REACTION: 'quickReaction',
QUICK_REACTION_EMOJI: 'quickReactionEmoji',
NSFW_DISPLAY_POLICY: 'nsfwDisplayPolicy',
MIN_TRUST_SCORE: 'minTrustScore',
DEFAULT_RELAY_URLS: 'defaultRelayUrls',
MUTED_WORDS: 'mutedWords',
MIN_TRUST_SCORE: 'minTrustScore',
MIN_TRUST_SCORE_MAP: 'minTrustScoreMap',
ENABLE_LIVE_FEED: 'enableLiveFeed', // deprecated
HIDE_UNTRUSTED_NOTES: 'hideUntrustedNotes', // deprecated
HIDE_UNTRUSTED_INTERACTIONS: 'hideUntrustedInteractions', // deprecated
@ -472,3 +473,13 @@ export type TPrimaryColor = keyof typeof PRIMARY_COLORS
export const LONG_PRESS_THRESHOLD = 400
export const SPAMMER_PERCENTILE_THRESHOLD = 60
export const SPECIAL_TRUST_SCORE_FILTER_ID = {
DEFAULT: 'default',
INTERACTIONS: 'interactions',
NOTIFICATIONS: 'notifications',
SEARCH: 'search',
HASHTAG: 'hashtag',
NAK: 'nak',
TRENDING: 'trending'
}

View file

@ -6,10 +6,11 @@ import { useUserTrust } from '@/providers/UserTrustProvider'
import { NostrEvent } from 'nostr-tools'
import { useEffect, useState } from 'react'
import { useAllDescendantThreads } from './useThread'
import { SPECIAL_TRUST_SCORE_FILTER_ID } from '@/constants'
export function useFilteredReplies(stuffKey: string) {
const { pubkey } = useNostr()
const { minTrustScore, meetsMinTrustScore } = useUserTrust()
const { getMinTrustScore, meetsMinTrustScore } = useUserTrust()
const { mutePubkeySet } = useMuteList()
const { hideContentMentioningMutedUsers } = useContentPolicy()
const allThreads = useAllDescendantThreads(stuffKey)
@ -22,6 +23,7 @@ export function useFilteredReplies(stuffKey: string) {
const thread = allThreads.get(stuffKey) || []
const filtered: NostrEvent[] = []
const trustScoreThreshold = getMinTrustScore(SPECIAL_TRUST_SCORE_FILTER_ID.INTERACTIONS)
await Promise.all(
thread.map(async (evt) => {
const key = getEventKey(evt)
@ -31,7 +33,7 @@ export function useFilteredReplies(stuffKey: string) {
if (mutePubkeySet.has(evt.pubkey)) return
if (hideContentMentioningMutedUsers && isMentioningMutedUsers(evt, mutePubkeySet)) return
const meetsTrust = await meetsMinTrustScore(evt.pubkey)
const meetsTrust = await meetsMinTrustScore(evt.pubkey, trustScoreThreshold)
if (!meetsTrust) {
const replyKey = getEventKey(evt)
const repliesForThisReply = allThreads.get(replyKey)
@ -39,7 +41,7 @@ export function useFilteredReplies(stuffKey: string) {
if (repliesForThisReply && repliesForThisReply.length > 0) {
let hasTrustedReply = false
for (const reply of repliesForThisReply) {
if (await meetsMinTrustScore(reply.pubkey)) {
if (await meetsMinTrustScore(reply.pubkey, trustScoreThreshold)) {
hasTrustedReply = true
break
}
@ -63,7 +65,7 @@ export function useFilteredReplies(stuffKey: string) {
allThreads,
mutePubkeySet,
hideContentMentioningMutedUsers,
minTrustScore,
getMinTrustScore,
meetsMinTrustScore
])
@ -84,7 +86,7 @@ export function useFilteredReplies(stuffKey: string) {
export function useFilteredAllReplies(stuffKey: string) {
const { pubkey } = useNostr()
const allThreads = useAllDescendantThreads(stuffKey)
const { minTrustScore, meetsMinTrustScore } = useUserTrust()
const { getMinTrustScore, meetsMinTrustScore } = useUserTrust()
const { mutePubkeySet } = useMuteList()
const { hideContentMentioningMutedUsers } = useContentPolicy()
const [replies, setReplies] = useState<NostrEvent[]>([])
@ -94,6 +96,7 @@ export function useFilteredAllReplies(stuffKey: string) {
const filterReplies = async () => {
const replyKeySet = new Set<string>()
const replyEvents: NostrEvent[] = []
const trustScoreThreshold = getMinTrustScore(SPECIAL_TRUST_SCORE_FILTER_ID.INTERACTIONS)
let parentKeys = [stuffKey]
while (parentKeys.length > 0) {
@ -108,7 +111,7 @@ export function useFilteredAllReplies(stuffKey: string) {
if (hideContentMentioningMutedUsers && isMentioningMutedUsers(evt, mutePubkeySet))
return
const meetsTrust = await meetsMinTrustScore(evt.pubkey)
const meetsTrust = await meetsMinTrustScore(evt.pubkey, trustScoreThreshold)
if (!meetsTrust) {
const replyKey = getEventKey(evt)
const repliesForThisReply = allThreads.get(replyKey)
@ -116,7 +119,7 @@ export function useFilteredAllReplies(stuffKey: string) {
if (repliesForThisReply && repliesForThisReply.length > 0) {
let hasTrustedReply = false
for (const reply of repliesForThisReply) {
if (await meetsMinTrustScore(reply.pubkey)) {
if (await meetsMinTrustScore(reply.pubkey, trustScoreThreshold)) {
hasTrustedReply = true
break
}
@ -141,7 +144,7 @@ export function useFilteredAllReplies(stuffKey: string) {
allThreads,
mutePubkeySet,
hideContentMentioningMutedUsers,
minTrustScore,
getMinTrustScore,
meetsMinTrustScore
])

View file

@ -663,6 +663,7 @@ export default {
'Invalid relay URL': 'عنوان URL للمرحل غير صالح',
'Muted words': 'الكلمات المحظورة',
'Add muted word': 'إضافة كلمة محظورة',
'Zap Details': 'تفاصيل Zap'
'Zap Details': 'تفاصيل Zap',
'Default trust score filter threshold ({{n}}%)': 'عتبة مرشح درجة الثقة الافتراضية ({{n}}%)'
}
}

View file

@ -685,6 +685,8 @@ export default {
'Invalid relay URL': 'Ungültige Relay-URL',
'Muted words': 'Stummgeschaltete Wörter',
'Add muted word': 'Stummgeschaltetes Wort hinzufügen',
'Zap Details': 'Zap-Details'
'Zap Details': 'Zap-Details',
'Default trust score filter threshold ({{n}}%)':
'Standard-Vertrauenswert-Filter-Schwelle ({{n}}%)'
}
}

View file

@ -668,6 +668,7 @@ export default {
'Invalid relay URL': 'Invalid relay URL',
'Muted words': 'Muted words',
'Add muted word': 'Add muted word',
'Zap Details': 'Zap Details'
'Zap Details': 'Zap Details',
'Default trust score filter threshold ({{n}}%)': 'Default trust score filter threshold ({{n}}%)'
}
}

View file

@ -679,6 +679,8 @@ export default {
'Invalid relay URL': 'URL de relé no válida',
'Muted words': 'Palabras silenciadas',
'Add muted word': 'Agregar palabra silenciada',
'Zap Details': 'Detalles del Zap'
'Zap Details': 'Detalles del Zap',
'Default trust score filter threshold ({{n}}%)':
'Umbral predeterminado del filtro de puntuación de confianza ({{n}}%)'
}
}

View file

@ -674,6 +674,7 @@ export default {
'Invalid relay URL': 'آدرس URL رله نامعتبر است',
'Muted words': 'کلمات بی‌صدا شده',
'Add muted word': 'افزودن کلمه بی‌صدا',
'Zap Details': 'جزئیات زپ'
'Zap Details': 'جزئیات زپ',
'Default trust score filter threshold ({{n}}%)': 'آستانه فیلتر امتیاز اعتماد پیش‌فرض ({{n}}%)'
}
}

View file

@ -683,6 +683,8 @@ export default {
'Invalid relay URL': 'URL de relais non valide',
'Muted words': 'Mots masqués',
'Add muted word': 'Ajouter un mot masqué',
'Zap Details': 'Détails du Zap'
'Zap Details': 'Détails du Zap',
'Default trust score filter threshold ({{n}}%)':
'Seuil par défaut du filtre de score de confiance ({{n}}%)'
}
}

View file

@ -675,6 +675,7 @@ export default {
'Invalid relay URL': 'अमान्य रिले URL',
'Muted words': 'म्यूट किए गए शब्द',
'Add muted word': 'म्यूट शब्द जोड़ें',
'Zap Details': 'जैप विवरण'
'Zap Details': 'जैप विवरण',
'Default trust score filter threshold ({{n}}%)': 'डिफ़ॉल्ट विश्वास स्कोर फ़िल्टर सीमा ({{n}}%)'
}
}

View file

@ -668,6 +668,8 @@ export default {
'Invalid relay URL': 'Érvénytelen továbbító URL',
'Muted words': 'Némított szavak',
'Add muted word': 'Némított szó hozzáadása',
'Zap Details': 'Zap Részletek'
'Zap Details': 'Zap Részletek',
'Default trust score filter threshold ({{n}}%)':
'Alapértelmezett bizalmi pontszám szűrő küszöbérték ({{n}}%)'
}
}

View file

@ -679,6 +679,8 @@ export default {
'Invalid relay URL': 'URL relay non valido',
'Muted words': 'Parole silenziate',
'Add muted word': 'Aggiungi parola silenziata',
'Zap Details': 'Dettagli Zap'
'Zap Details': 'Dettagli Zap',
'Default trust score filter threshold ({{n}}%)':
'Soglia predefinita del filtro del punteggio di fiducia ({{n}}%)'
}
}

View file

@ -673,6 +673,7 @@ export default {
'Invalid relay URL': '無効なリレーURL',
'Muted words': 'ミュートワード',
'Add muted word': 'ミュートワードを追加',
'Zap Details': 'Zapの詳細'
'Zap Details': 'Zapの詳細',
'Default trust score filter threshold ({{n}}%)': 'デフォルトの信頼スコアフィルター閾値 ({{n}}%)'
}
}

View file

@ -669,6 +669,7 @@ export default {
'Invalid relay URL': '유효하지 않은 릴레이 URL',
'Muted words': '차단 단어',
'Add muted word': '차단 단어 추가',
'Zap Details': '잽 세부 정보'
'Zap Details': '잽 세부 정보',
'Default trust score filter threshold ({{n}}%)': '기본 신뢰 점수 필터 임계값 ({{n}}%)'
}
}

View file

@ -680,6 +680,7 @@ export default {
'Invalid relay URL': 'Nieprawidłowy adres URL przekaźnika',
'Muted words': 'Wyciszone słowa',
'Add muted word': 'Dodaj wyciszone słowo',
'Zap Details': 'Szczegóły zapu'
'Zap Details': 'Szczegóły zapu',
'Default trust score filter threshold ({{n}}%)': 'Domyślny próg filtra wyniku zaufania ({{n}}%)'
}
}

View file

@ -676,6 +676,8 @@ export default {
'Invalid relay URL': 'URL de relé inválida',
'Muted words': 'Palavras silenciadas',
'Add muted word': 'Adicionar palavra silenciada',
'Zap Details': 'Detalhes do Zap'
'Zap Details': 'Detalhes do Zap',
'Default trust score filter threshold ({{n}}%)':
'Limite padrão do filtro de pontuação de confiança ({{n}}%)'
}
}

View file

@ -679,6 +679,8 @@ export default {
'Invalid relay URL': 'URL de relay inválido',
'Muted words': 'Palavras silenciadas',
'Add muted word': 'Adicionar palavra silenciada',
'Zap Details': 'Detalhes do Zap'
'Zap Details': 'Detalhes do Zap',
'Default trust score filter threshold ({{n}}%)':
'Limite predefinido do filtro de pontuação de confiança ({{n}}%)'
}
}

View file

@ -679,6 +679,8 @@ export default {
'Invalid relay URL': 'Неверный URL реле',
'Muted words': 'Заблокированные слова',
'Add muted word': 'Добавить заблокированное слово',
'Zap Details': 'Детали запа'
'Zap Details': 'Детали запа',
'Default trust score filter threshold ({{n}}%)':
'Порог фильтра рейтинга доверия по умолчанию ({{n}}%)'
}
}

View file

@ -664,6 +664,8 @@ export default {
'Invalid relay URL': 'URL รีเลย์ไม่ถูกต้อง',
'Muted words': 'คำที่ถูกปิดเสียง',
'Add muted word': 'เพิ่มคำที่ถูกปิดเสียง',
'Zap Details': 'รายละเอียดซาตส์'
'Zap Details': 'รายละเอียดซาตส์',
'Default trust score filter threshold ({{n}}%)':
'เกณฑ์ตัวกรองคะแนนความไว้วางใจเริ่มต้น ({{n}}%)'
}
}

View file

@ -647,6 +647,7 @@ export default {
'Invalid relay URL': '無效的中繼地址',
'Muted words': '屏蔽詞',
'Add muted word': '添加屏蔽詞',
'Zap Details': '打閃詳情'
'Zap Details': '打閃詳情',
'Default trust score filter threshold ({{n}}%)': '預設信任分數過濾閾值 ({{n}}%)'
}
}

View file

@ -652,6 +652,7 @@ export default {
'Invalid relay URL': '无效的中继地址',
'Muted words': '屏蔽词',
'Add muted word': '添加屏蔽词',
'Zap Details': '打闪详情'
'Zap Details': '打闪详情',
'Default trust score filter threshold ({{n}}%)': '默认信任分数过滤阈值 ({{n}}%)'
}
}

View file

@ -13,7 +13,7 @@ export async function notificationFilter(
pubkey?: string | null
mutePubkeySet: Set<string>
hideContentMentioningMutedUsers?: boolean
meetsMinTrustScore: (pubkey: string, minScore?: number) => Promise<boolean>
meetsMinTrustScore: (pubkey: string) => Promise<boolean>
}
): Promise<boolean> {
if (

View file

@ -2,12 +2,20 @@ import NormalFeed from '@/components/NormalFeed'
import { checkAlgoRelay } from '@/lib/relay'
import { useFeed } from '@/providers/FeedProvider'
import relayInfoService from '@/services/relay-info.service'
import { useEffect, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
export default function RelaysFeed() {
const { relayUrls } = useFeed()
const { relayUrls, feedInfo } = useFeed()
const [isReady, setIsReady] = useState(false)
const [areAlgoRelays, setAreAlgoRelays] = useState(false)
const trustScoreFilterId = useMemo(() => {
if (feedInfo?.feedType === 'relay' && feedInfo.id) {
return `relay-${feedInfo.id}`
} else if (feedInfo?.feedType === 'relays' && feedInfo.id) {
return `relays-${feedInfo.id}`
}
return 'relays-default'
}, [feedInfo])
useEffect(() => {
const init = async () => {
@ -24,6 +32,7 @@ export default function RelaysFeed() {
return (
<NormalFeed
trustScoreFilterId={trustScoreFilterId}
subRequests={[{ urls: relayUrls, filter: {} }]}
areAlgoRelays={areAlgoRelays}
isMainFeed

View file

@ -1,5 +1,6 @@
import NotificationList from '@/components/NotificationList'
import TrustScoreFilter from '@/components/TrustScoreFilter'
import { SPECIAL_TRUST_SCORE_FILTER_ID } from '@/constants'
import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
import { usePrimaryPage } from '@/PageManager'
import { TPageRef } from '@/types'
@ -42,7 +43,7 @@ function NotificationListPageTitlebar() {
<Bell />
<div className="text-lg font-semibold">{t('Notifications')}</div>
</div>
<TrustScoreFilter />
<TrustScoreFilter filterId={SPECIAL_TRUST_SCORE_FILTER_ID.NOTIFICATIONS} />
</div>
)
}

View file

@ -4,6 +4,7 @@ import ProfileList from '@/components/ProfileList'
import { Skeleton } from '@/components/ui/skeleton'
import { useFetchEvent } from '@/hooks/useFetchEvent'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { getEventKey } from '@/lib/event'
import { getFollowPackInfoFromEvent } from '@/lib/event-metadata'
import { cn } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider'
@ -96,7 +97,9 @@ const FollowPackPage = forwardRef(({ id, index }: { id?: string; index?: number
{/* Content */}
{tab === 'users' && <ProfileList pubkeys={pubkeys} />}
{tab === 'feed' && pubkeys.length > 0 && <Feed pubkeys={pubkeys} />}
{tab === 'feed' && pubkeys.length > 0 && (
<Feed trustScoreFilterId={`follow-pack-${getEventKey(event)}`} pubkeys={pubkeys} />
)}
</div>
</SecondaryPageLayout>
)
@ -104,7 +107,7 @@ const FollowPackPage = forwardRef(({ id, index }: { id?: string; index?: number
FollowPackPage.displayName = 'FollowPackPage'
export default FollowPackPage
function Feed({ pubkeys }: { pubkeys: string[] }) {
function Feed({ trustScoreFilterId, pubkeys }: { trustScoreFilterId: string; pubkeys: string[] }) {
const { pubkey: myPubkey } = useNostr()
const [subRequests, setSubRequests] = useState<TFeedSubRequest[]>([])
@ -112,5 +115,5 @@ function Feed({ pubkeys }: { pubkeys: string[] }) {
client.generateSubRequestsForPubkeys(pubkeys, myPubkey).then(setSubRequests)
}, [pubkeys, myPubkey])
return <NormalFeed subRequests={subRequests} />
return <NormalFeed trustScoreFilterId={trustScoreFilterId} subRequests={subRequests} />
}

View file

@ -0,0 +1,29 @@
import { Label } from '@/components/ui/label'
import { Slider } from '@/components/ui/slider'
import { SPECIAL_TRUST_SCORE_FILTER_ID } from '@/constants'
import { useUserTrust } from '@/providers/UserTrustProvider'
import { useTranslation } from 'react-i18next'
import SettingItem from './SettingItem'
export default function DefaultTrustScoreFilter() {
const { t } = useTranslation()
const { minTrustScore, updateMinTrustScore } = useUserTrust()
return (
<SettingItem className="flex-col items-start gap-2">
<Label className="text-base font-normal">
{t('Default trust score filter threshold ({{n}}%)', { n: minTrustScore })}
</Label>
<Slider
value={[minTrustScore]}
onValueChange={([value]) =>
updateMinTrustScore(SPECIAL_TRUST_SCORE_FILTER_ID.DEFAULT, value)
}
min={0}
max={100}
step={5}
className="w-full"
/>
</SettingItem>
)
}

View file

@ -19,6 +19,7 @@ import { SelectValue } from '@radix-ui/react-select'
import { RotateCcw } from 'lucide-react'
import { forwardRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import DefaultTrustScoreFilter from './DefaultTrustScoreFilter'
import MutedWords from './MutedWords'
import SettingItem from './SettingItem'
@ -150,6 +151,7 @@ const GeneralSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
</SelectContent>
</Select>
</SettingItem>
<DefaultTrustScoreFilter />
<SettingItem>
<Label htmlFor="quick-reaction" className="text-base font-normal">
<div>{t('Quick reaction')}</div>

View file

@ -108,7 +108,22 @@ const NoteListPage = forwardRef(({ index }: { index?: number }, ref) => {
</div>
)
} else if (data) {
content = <NormalFeed subRequests={subRequests} disable24hMode={data.type !== 'domain'} />
let trustScoreFilterId: string
if (data.type === 'hashtag') {
trustScoreFilterId = 'hashtag'
} else if (data.type === 'domain') {
trustScoreFilterId = `domain-${data.domain}`
} else {
trustScoreFilterId = 'search'
}
content = (
<NormalFeed
trustScoreFilterId={trustScoreFilterId}
subRequests={subRequests}
disable24hMode={data.type !== 'domain'}
/>
)
}
return (

View file

@ -1,4 +1,4 @@
import { ExtendedKind } from '@/constants'
import { ExtendedKind, SPECIAL_TRUST_SCORE_FILTER_ID } from '@/constants'
import { compareEvents } from '@/lib/event'
import { notificationFilter } from '@/lib/notification'
import { getDefaultRelayUrls } from '@/lib/relay'
@ -34,7 +34,7 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
const active = useMemo(() => current === 'notifications', [current])
const { pubkey, notificationsSeenAt, updateNotificationsSeenAt } = useNostr()
const { mutePubkeySet } = useMuteList()
const { meetsMinTrustScore } = useUserTrust()
const { getMinTrustScore, meetsMinTrustScore } = useUserTrust()
const { hideContentMentioningMutedUsers } = useContentPolicy()
const [newNotifications, setNewNotifications] = useState<NostrEvent[]>([])
const [readNotificationIdSet, setReadNotificationIdSet] = useState<Set<string>>(new Set())
@ -47,6 +47,7 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
}
const filterNotifications = async () => {
const filtered: NostrEvent[] = []
const trustScoreThreshold = getMinTrustScore(SPECIAL_TRUST_SCORE_FILTER_ID.NOTIFICATIONS)
await Promise.allSettled(
newNotifications.map(async (notification) => {
if (notification.created_at <= notificationsSeenAt || filtered.length >= 10) {
@ -57,7 +58,10 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
pubkey,
mutePubkeySet,
hideContentMentioningMutedUsers,
meetsMinTrustScore
meetsMinTrustScore: async (pubkey: string) => {
if (trustScoreThreshold === 0) return true
return meetsMinTrustScore(pubkey, trustScoreThreshold)
}
}))
) {
return

View file

@ -1,3 +1,4 @@
import { SPECIAL_TRUST_SCORE_FILTER_ID } from '@/constants'
import client from '@/services/client.service'
import fayan from '@/services/fayan.service'
import storage from '@/services/local-storage.service'
@ -6,10 +7,12 @@ import { useNostr } from './NostrProvider'
type TUserTrustContext = {
minTrustScore: number
updateMinTrustScore: (score: number) => void
minTrustScoreMap: Record<string, number>
getMinTrustScore: (id: string) => number
updateMinTrustScore: (id: string, score: number) => void
isUserTrusted: (pubkey: string) => boolean
isSpammer: (pubkey: string) => Promise<boolean>
meetsMinTrustScore: (pubkey: string, minScore?: number) => Promise<boolean>
meetsMinTrustScore: (pubkey: string, minScore: number) => Promise<boolean>
}
const UserTrustContext = createContext<TUserTrustContext | undefined>(undefined)
@ -27,6 +30,9 @@ const wotSet = new Set<string>()
export function UserTrustProvider({ children }: { children: React.ReactNode }) {
const { pubkey: currentPubkey } = useNostr()
const [minTrustScore, setMinTrustScore] = useState(() => storage.getMinTrustScore())
const [minTrustScoreMap, setMinTrustScoreMap] = useState<Record<string, number>>(() =>
storage.getMinTrustScoreMap()
)
useEffect(() => {
if (!currentPubkey) return
@ -70,15 +76,31 @@ export function UserTrustProvider({ children }: { children: React.ReactNode }) {
[isUserTrusted]
)
const updateMinTrustScore = (score: number) => {
setMinTrustScore(score)
storage.setMinTrustScore(score)
const getMinTrustScore = useCallback(
(id: string) => {
return id === SPECIAL_TRUST_SCORE_FILTER_ID.DEFAULT
? minTrustScore
: (minTrustScoreMap[id] ?? minTrustScore)
},
[minTrustScore, minTrustScoreMap]
)
const updateMinTrustScore = (id: string, score: number) => {
if (score < 0 || score > 100) return
if (id === SPECIAL_TRUST_SCORE_FILTER_ID.DEFAULT) {
setMinTrustScore(score)
storage.setMinTrustScore(score)
} else {
const newMap = { ...minTrustScoreMap, [id]: score }
setMinTrustScoreMap(newMap)
storage.setMinTrustScoreMap(newMap)
}
}
const meetsMinTrustScore = useCallback(
async (pubkey: string, minScore?: number) => {
const threshold = minScore !== undefined ? minScore : minTrustScore
if (threshold === 0) return true
async (pubkey: string, minScore: number) => {
if (minScore === 0) return true
if (pubkey === currentPubkey) return true
// WoT users always have 100% trust score
@ -87,15 +109,17 @@ export function UserTrustProvider({ children }: { children: React.ReactNode }) {
// 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
return percentile >= minScore
},
[currentPubkey, minTrustScore]
[currentPubkey]
)
return (
<UserTrustContext.Provider
value={{
minTrustScore,
minTrustScoreMap,
getMinTrustScore,
updateMinTrustScore,
isUserTrusted,
isSpammer,

View file

@ -64,9 +64,10 @@ class LocalStorageService {
private quickReaction: boolean = false
private quickReactionEmoji: string | TEmoji = '+'
private nsfwDisplayPolicy: TNsfwDisplayPolicy = NSFW_DISPLAY_POLICY.HIDE_CONTENT
private minTrustScore: number = 40
private defaultRelayUrls: string[] = BIG_RELAY_URLS
private mutedWords: string[] = []
private minTrustScore: number = 0
private minTrustScoreMap: Record<string, number> = {}
constructor() {
if (!LocalStorageService.instance) {
@ -278,6 +279,18 @@ class LocalStorageService {
}
}
const minTrustScoreMapStr = window.localStorage.getItem(StorageKey.MIN_TRUST_SCORE_MAP)
if (minTrustScoreMapStr) {
try {
const map = JSON.parse(minTrustScoreMapStr)
if (typeof map === 'object' && map !== null) {
this.minTrustScoreMap = map
}
} catch {
// Invalid JSON, use default
}
}
const defaultRelayUrlsStr = window.localStorage.getItem(StorageKey.DEFAULT_RELAY_URLS)
if (defaultRelayUrlsStr) {
try {
@ -645,6 +658,15 @@ class LocalStorageService {
}
}
getMinTrustScoreMap() {
return this.minTrustScoreMap
}
setMinTrustScoreMap(map: Record<string, number>) {
this.minTrustScoreMap = map
window.localStorage.setItem(StorageKey.MIN_TRUST_SCORE_MAP, JSON.stringify(map))
}
getDefaultRelayUrls() {
return this.defaultRelayUrls
}