feat: support configurable trust score threshold per context
This commit is contained in:
parent
28a1b3096a
commit
ca9610b711
46 changed files with 350 additions and 122 deletions
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
])
|
||||
|
||||
|
|
|
|||
|
|
@ -177,7 +177,6 @@ export default function ProfileFeed({
|
|||
filterMutedNotes={false}
|
||||
pinnedEventIds={listMode === 'you' || !!search ? [] : pinnedEventIds}
|
||||
showNewNotesDirectly={myPubkey === pubkey}
|
||||
disableTrustFilter
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 } : {} }
|
||||
]}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
]
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
])
|
||||
|
||||
|
|
|
|||
|
|
@ -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}}%)'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}}%)'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}}%)'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}}%)'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}}%)'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}}%)'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}}%)'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}}%)'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}}%)'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}}%)'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}}%)'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}}%)'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}}%)'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}}%)'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}}%)'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}}%)'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}}%)'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}}%)'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue