feat: add badge for suspicious and spam users

This commit is contained in:
codytseng 2025-11-25 23:11:31 +08:00
parent 2b4f673df1
commit c84c479871
23 changed files with 185 additions and 22 deletions

View file

@ -16,6 +16,7 @@ import Nip05 from '../Nip05'
import NoteOptions from '../NoteOptions'
import ParentNotePreview from '../ParentNotePreview'
import TranslateButton from '../TranslateButton'
import TrustScoreBadge from '../TrustScoreBadge'
import UserAvatar from '../UserAvatar'
import Username from '../Username'
import CommunityDefinition from './CommunityDefinition'
@ -125,6 +126,7 @@ export default function Note({
skeletonClassName={size === 'small' ? 'h-3' : 'h-4'}
/>
<FollowingBadge pubkey={event.pubkey} />
<TrustScoreBadge pubkey={event.pubkey} />
<ClientTag event={event} />
</div>
<div className="flex items-center gap-1 text-sm text-muted-foreground">

View file

@ -19,13 +19,14 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import NotFound from '../NotFound'
import SearchInput from '../SearchInput'
import TextWithEmojis from '../TextWithEmojis'
import TrustScoreBadge from '../TrustScoreBadge'
import AvatarWithLightbox from './AvatarWithLightbox'
import BannerWithLightbox from './BannerWithLightbox'
import FollowedBy from './FollowedBy'
import Followings from './Followings'
import ProfileFeed from './ProfileFeed'
import Relays from './Relays'
import TextWithEmojis from '../TextWithEmojis'
import AvatarWithLightbox from './AvatarWithLightbox'
import BannerWithLightbox from './BannerWithLightbox'
export default function Profile({ id }: { id?: string }) {
const { t } = useTranslation()
@ -143,6 +144,7 @@ export default function Profile({ id }: { id?: string }) {
emojis={emojis}
className="text-xl font-semibold truncate select-text"
/>
<TrustScoreBadge pubkey={pubkey} />
{isFollowingYou && (
<div className="text-muted-foreground rounded-full bg-muted text-xs h-fit px-2 shrink-0">
{t('Follows you')}

View file

@ -4,6 +4,8 @@ import { useMemo } from 'react'
import FollowButton from '../FollowButton'
import Nip05 from '../Nip05'
import ProfileAbout from '../ProfileAbout'
import TextWithEmojis from '../TextWithEmojis'
import TrustScoreBadge from '../TrustScoreBadge'
import { SimpleUserAvatar } from '../UserAvatar'
export default function ProfileCard({ userId }: { userId: string }) {
@ -18,7 +20,14 @@ export default function ProfileCard({ userId }: { userId: string }) {
<FollowButton pubkey={pubkey} />
</div>
<div>
<div className="text-lg font-semibold truncate">{username}</div>
<div className="flex gap-2 items-center">
<TextWithEmojis
text={username || ''}
emojis={emojis}
className="text-lg font-semibold truncate"
/>
<TrustScoreBadge pubkey={pubkey} />
</div>
<Nip05 pubkey={pubkey} />
</div>
{about && (

View file

@ -0,0 +1,64 @@
import { cn } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider'
import trustScoreService from '@/services/trust-score.service'
import { AlertTriangle, ShieldAlert } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
export default function TrustScoreBadge({
pubkey,
className
}: {
pubkey: string
className?: string
}) {
const { t } = useTranslation()
const { pubkey: currentPubkey } = useNostr()
const [percentile, setPercentile] = useState<number | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
if (currentPubkey === pubkey) {
setLoading(false)
setPercentile(null)
return
}
const fetchScore = async () => {
try {
const data = await trustScoreService.fetchTrustScore(pubkey)
if (data) {
setPercentile(data.percentile)
}
} catch (error) {
console.error('Failed to fetch trust score:', error)
} finally {
setLoading(false)
}
}
fetchScore()
}, [pubkey, currentPubkey])
if (loading || percentile === null) return null
// percentile < 50: likely spam (red alert)
// percentile < 75: suspicious (yellow warning)
if (percentile < 50) {
return (
<div title={t('Likely spam account (Trust score: {{percentile}}%)', { percentile })}>
<ShieldAlert className={cn('!size-4 text-red-500', className)} />
</div>
)
}
if (percentile < 75) {
return (
<div title={t('Suspicious account (Trust score: {{percentile}}%)', { percentile })}>
<AlertTriangle className={cn('!size-4 text-yellow-600 dark:text-yellow-500', className)} />
</div>
)
}
return null
}

View file

@ -7,6 +7,7 @@ import { userIdToPubkey } from '@/lib/pubkey'
import { cn } from '@/lib/utils'
import { useMemo } from 'react'
import FollowingBadge from '../FollowingBadge'
import TrustScoreBadge from '../TrustScoreBadge'
export default function UserItem({
userId,
@ -32,6 +33,7 @@ export default function UserItem({
skeletonClassName="h-4"
/>
{showFollowingBadge && <FollowingBadge pubkey={pubkey} />}
<TrustScoreBadge pubkey={pubkey} />
</div>
<Nip05 pubkey={userId} />
</div>