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