diff --git a/src/components/ExternalContentInteractions/index.tsx b/src/components/ExternalContentInteractions/index.tsx index 1214fa1..2b76309 100644 --- a/src/components/ExternalContentInteractions/index.tsx +++ b/src/components/ExternalContentInteractions/index.tsx @@ -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({
- +
diff --git a/src/components/NormalFeed/index.tsx b/src/components/NormalFeed/index.tsx index d0cfa62..97a2dcb 100644 --- a/src/components/NormalFeed/index.tsx +++ b/src/components/NormalFeed/index.tsx @@ -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(() => 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 && } + {trustScoreFilterId && ( + + )} {showKindsFilter && ( ) : ( )} diff --git a/src/components/NoteInteractions/index.tsx b/src/components/NoteInteractions/index.tsx index 769a188..1a02107 100644 --- a/src/components/NoteInteractions/index.tsx +++ b/src/components/NoteInteractions/index.tsx @@ -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 }) { - + {list} diff --git a/src/components/NoteList/index.tsx b/src/components/NoteList/index.tsx index f2337d3..8cb8360 100644 --- a/src/components/NoteList/index.tsx +++ b/src/components/NoteList/index.tsx @@ -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() + 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(() => { diff --git a/src/components/NotificationList/NotificationItem/index.tsx b/src/components/NotificationList/NotificationItem/index.tsx index 9b936aa..58762e1 100644 --- a/src/components/NotificationList/NotificationItem/index.tsx +++ b/src/components/NotificationList/NotificationItem/index.tsx @@ -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 ]) diff --git a/src/components/Profile/ProfileFeed.tsx b/src/components/Profile/ProfileFeed.tsx index 87db565..6800d3b 100644 --- a/src/components/Profile/ProfileFeed.tsx +++ b/src/components/Profile/ProfileFeed.tsx @@ -177,7 +177,6 @@ export default function ProfileFeed({ filterMutedNotes={false} pinnedEventIds={listMode === 'you' || !!search ? [] : pinnedEventIds} showNewNotesDirectly={myPubkey === pubkey} - disableTrustFilter /> ) diff --git a/src/components/ReactionList/index.tsx b/src/components/ReactionList/index.tsx index 31483a9..6bb8d30 100644 --- a/src/components/ReactionList/index.tsx +++ b/src/components/ReactionList/index.tsx @@ -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(null) diff --git a/src/components/Relay/index.tsx b/src/components/Relay/index.tsx index 6e6005e..732c5f9 100644 --- a/src/components/Relay/index.tsx +++ b/src/components/Relay/index.tsx @@ -52,6 +52,7 @@ export default function Relay({ url, className }: { url?: string; className?: st )} 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 (
@@ -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(null) diff --git a/src/components/SearchResult/index.tsx b/src/components/SearchResult/index.tsx index 91671d3..823f8d8 100644 --- a/src/components/SearchResult/index.tsx +++ b/src/components/SearchResult/index.tsx @@ -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 ( @@ -28,13 +29,20 @@ export default function SearchResult({ searchParams }: { searchParams: TSearchPa if (searchParams.type === 'hashtag') { return ( ) } if (searchParams.type === 'nak') { - return + return ( + + ) } return } diff --git a/src/components/StuffStats/LikeButton.tsx b/src/components/StuffStats/LikeButton.tsx index de1729f..9399aaa 100644 --- a/src/components/StuffStats/LikeButton.tsx +++ b/src/components/StuffStats/LikeButton.tsx @@ -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) diff --git a/src/components/StuffStats/RepostButton.tsx b/src/components/StuffStats/RepostButton.tsx index 7983a33..6d51fa1 100644 --- a/src/components/StuffStats/RepostButton.tsx +++ b/src/components/StuffStats/RepostButton.tsx @@ -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 () => { diff --git a/src/components/TrendingNotes/index.tsx b/src/components/TrendingNotes/index.tsx index f1206a8..51c7503 100644 --- a/src/components/TrendingNotes/index.tsx +++ b/src/components/TrendingNotes/index.tsx @@ -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() {
diff --git a/src/components/TrustScoreFilter/index.tsx b/src/components/TrustScoreFilter/index.tsx index 22fd4e2..da160e0 100644 --- a/src/components/TrustScoreFilter/index.tsx +++ b/src/components/TrustScoreFilter/index.tsx @@ -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(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 ? : } - {minTrustScore > 0 && minTrustScore < 100 && ( + {temporaryScore < 100 ? : } + {temporaryScore > 0 && temporaryScore < 100 && (
- {minTrustScore} + {temporaryScore}
)} diff --git a/src/components/UserAggregationList/index.tsx b/src/components/UserAggregationList/index.tsx index 6d6601c..4c684df 100644 --- a/src/components/UserAggregationList/index.tsx +++ b/src/components/UserAggregationList/index.tsx @@ -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 ] ) diff --git a/src/constants.ts b/src/constants.ts index b7f3a86..d05a1b9 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -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' +} diff --git a/src/hooks/useFilteredReplies.tsx b/src/hooks/useFilteredReplies.tsx index 74d637c..dfd35ea 100644 --- a/src/hooks/useFilteredReplies.tsx +++ b/src/hooks/useFilteredReplies.tsx @@ -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([]) @@ -94,6 +96,7 @@ export function useFilteredAllReplies(stuffKey: string) { const filterReplies = async () => { const replyKeySet = new Set() 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 ]) diff --git a/src/i18n/locales/ar.ts b/src/i18n/locales/ar.ts index 5c0f0d8..7ff9688 100644 --- a/src/i18n/locales/ar.ts +++ b/src/i18n/locales/ar.ts @@ -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}}%)' } } diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 9c527a0..d950f26 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -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}}%)' } } diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index c24f506..37e8838 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -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}}%)' } } diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index 06baa5e..79eb862 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -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}}%)' } } diff --git a/src/i18n/locales/fa.ts b/src/i18n/locales/fa.ts index e72717d..642e705 100644 --- a/src/i18n/locales/fa.ts +++ b/src/i18n/locales/fa.ts @@ -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}}%)' } } diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index 3e5d6b7..ab7e34c 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -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}}%)' } } diff --git a/src/i18n/locales/hi.ts b/src/i18n/locales/hi.ts index 24bdfdc..92912cf 100644 --- a/src/i18n/locales/hi.ts +++ b/src/i18n/locales/hi.ts @@ -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}}%)' } } diff --git a/src/i18n/locales/hu.ts b/src/i18n/locales/hu.ts index e8d8f99..900a4c6 100644 --- a/src/i18n/locales/hu.ts +++ b/src/i18n/locales/hu.ts @@ -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}}%)' } } diff --git a/src/i18n/locales/it.ts b/src/i18n/locales/it.ts index 5bb2251..d0fe2e4 100644 --- a/src/i18n/locales/it.ts +++ b/src/i18n/locales/it.ts @@ -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}}%)' } } diff --git a/src/i18n/locales/ja.ts b/src/i18n/locales/ja.ts index dfa477a..3a143a7 100644 --- a/src/i18n/locales/ja.ts +++ b/src/i18n/locales/ja.ts @@ -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}}%)' } } diff --git a/src/i18n/locales/ko.ts b/src/i18n/locales/ko.ts index 599ccd5..6c9f15b 100644 --- a/src/i18n/locales/ko.ts +++ b/src/i18n/locales/ko.ts @@ -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}}%)' } } diff --git a/src/i18n/locales/pl.ts b/src/i18n/locales/pl.ts index eb025f6..03069a9 100644 --- a/src/i18n/locales/pl.ts +++ b/src/i18n/locales/pl.ts @@ -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}}%)' } } diff --git a/src/i18n/locales/pt-BR.ts b/src/i18n/locales/pt-BR.ts index 4e65b52..606ccd3 100644 --- a/src/i18n/locales/pt-BR.ts +++ b/src/i18n/locales/pt-BR.ts @@ -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}}%)' } } diff --git a/src/i18n/locales/pt-PT.ts b/src/i18n/locales/pt-PT.ts index b7ab2eb..2146bba 100644 --- a/src/i18n/locales/pt-PT.ts +++ b/src/i18n/locales/pt-PT.ts @@ -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}}%)' } } diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts index 2a7b309..79ce9b7 100644 --- a/src/i18n/locales/ru.ts +++ b/src/i18n/locales/ru.ts @@ -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}}%)' } } diff --git a/src/i18n/locales/th.ts b/src/i18n/locales/th.ts index fabc437..89146cb 100644 --- a/src/i18n/locales/th.ts +++ b/src/i18n/locales/th.ts @@ -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}}%)' } } diff --git a/src/i18n/locales/zh-TW.ts b/src/i18n/locales/zh-TW.ts index 14a39ab..e61ddfe 100644 --- a/src/i18n/locales/zh-TW.ts +++ b/src/i18n/locales/zh-TW.ts @@ -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}}%)' } } diff --git a/src/i18n/locales/zh.ts b/src/i18n/locales/zh.ts index ce74f16..3f129ae 100644 --- a/src/i18n/locales/zh.ts +++ b/src/i18n/locales/zh.ts @@ -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}}%)' } } diff --git a/src/lib/notification.ts b/src/lib/notification.ts index 5bceced..c34835a 100644 --- a/src/lib/notification.ts +++ b/src/lib/notification.ts @@ -13,7 +13,7 @@ export async function notificationFilter( pubkey?: string | null mutePubkeySet: Set hideContentMentioningMutedUsers?: boolean - meetsMinTrustScore: (pubkey: string, minScore?: number) => Promise + meetsMinTrustScore: (pubkey: string) => Promise } ): Promise { if ( diff --git a/src/pages/primary/NoteListPage/RelaysFeed.tsx b/src/pages/primary/NoteListPage/RelaysFeed.tsx index 05604de..1abb03d 100644 --- a/src/pages/primary/NoteListPage/RelaysFeed.tsx +++ b/src/pages/primary/NoteListPage/RelaysFeed.tsx @@ -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 (
{t('Notifications')}
- + ) } diff --git a/src/pages/secondary/FollowPackPage/index.tsx b/src/pages/secondary/FollowPackPage/index.tsx index ba07c3a..0445ff7 100644 --- a/src/pages/secondary/FollowPackPage/index.tsx +++ b/src/pages/secondary/FollowPackPage/index.tsx @@ -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' && } - {tab === 'feed' && pubkeys.length > 0 && } + {tab === 'feed' && pubkeys.length > 0 && ( + + )} ) @@ -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([]) @@ -112,5 +115,5 @@ function Feed({ pubkeys }: { pubkeys: string[] }) { client.generateSubRequestsForPubkeys(pubkeys, myPubkey).then(setSubRequests) }, [pubkeys, myPubkey]) - return + return } diff --git a/src/pages/secondary/GeneralSettingsPage/DefaultTrustScoreFilter.tsx b/src/pages/secondary/GeneralSettingsPage/DefaultTrustScoreFilter.tsx new file mode 100644 index 0000000..c00abab --- /dev/null +++ b/src/pages/secondary/GeneralSettingsPage/DefaultTrustScoreFilter.tsx @@ -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 ( + + + + updateMinTrustScore(SPECIAL_TRUST_SCORE_FILTER_ID.DEFAULT, value) + } + min={0} + max={100} + step={5} + className="w-full" + /> + + ) +} diff --git a/src/pages/secondary/GeneralSettingsPage/index.tsx b/src/pages/secondary/GeneralSettingsPage/index.tsx index 6e40c26..fc574c8 100644 --- a/src/pages/secondary/GeneralSettingsPage/index.tsx +++ b/src/pages/secondary/GeneralSettingsPage/index.tsx @@ -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) => { +