diff --git a/src/components/Note/index.tsx b/src/components/Note/index.tsx index 15fbb5b..1098bfa 100644 --- a/src/components/Note/index.tsx +++ b/src/components/Note/index.tsx @@ -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'} /> +
diff --git a/src/components/Profile/index.tsx b/src/components/Profile/index.tsx index 6d98f7d..aea4e98 100644 --- a/src/components/Profile/index.tsx +++ b/src/components/Profile/index.tsx @@ -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" /> + {isFollowingYou && (
{t('Follows you')} diff --git a/src/components/ProfileCard/index.tsx b/src/components/ProfileCard/index.tsx index 96e1f16..b2a4c3b 100644 --- a/src/components/ProfileCard/index.tsx +++ b/src/components/ProfileCard/index.tsx @@ -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 }) {
-
{username}
+
+ + +
{about && ( diff --git a/src/components/TrustScoreBadge/index.tsx b/src/components/TrustScoreBadge/index.tsx new file mode 100644 index 0000000..86210c8 --- /dev/null +++ b/src/components/TrustScoreBadge/index.tsx @@ -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(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 ( +
+ +
+ ) + } + + if (percentile < 75) { + return ( +
+ +
+ ) + } + + return null +} diff --git a/src/components/UserItem/index.tsx b/src/components/UserItem/index.tsx index 7dfc628..dc2eef6 100644 --- a/src/components/UserItem/index.tsx +++ b/src/components/UserItem/index.tsx @@ -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 && } +
diff --git a/src/i18n/locales/ar.ts b/src/i18n/locales/ar.ts index 2c2f2a0..ff07d9a 100644 --- a/src/i18n/locales/ar.ts +++ b/src/i18n/locales/ar.ts @@ -550,6 +550,8 @@ export default { 'Failed to republish to optimal relays: {{error}}': 'فشل إعادة النشر إلى المرحلات المثلى: {{error}}', 'External Content': 'محتوى خارجي', Highlight: 'تسليط الضوء', - 'Optimal relays and {{count}} other relays': 'المرحلات المثلى و {{count}} مرحلات أخرى' + 'Optimal relays and {{count}} other relays': 'المرحلات المثلى و {{count}} مرحلات أخرى', + 'Likely spam account (Trust score: {{percentile}}%)': 'حساب مشبوه للغاية (درجة الثقة: {{percentile}}%)', + 'Suspicious account (Trust score: {{percentile}}%)': 'حساب مشبوه (درجة الثقة: {{percentile}}%)' } } diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 0eeeb22..3e6bd39 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -566,6 +566,8 @@ export default { 'Fehler beim Neuveröffentlichen auf optimale Relays: {{error}}', 'External Content': 'Externer Inhalt', Highlight: 'Hervorheben', - 'Optimal relays and {{count}} other relays': 'Optimale Relays und {{count}} andere Relays' + 'Optimal relays and {{count}} other relays': 'Optimale Relays und {{count}} andere Relays', + 'Likely spam account (Trust score: {{percentile}}%)': 'Wahrscheinlich Spam-Konto (Vertrauenswert: {{percentile}}%)', + 'Suspicious account (Trust score: {{percentile}}%)': 'Verdächtiges Konto (Vertrauenswert: {{percentile}}%)' } } diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 748a94b..d1f9688 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -547,9 +547,14 @@ export default { 'Optimal relays': 'Optimal relays', "Successfully republish to optimal relays (your write relays and mentioned users' read relays)": "Successfully republish to optimal relays (your write relays and mentioned users' read relays)", - 'Failed to republish to optimal relays: {{error}}': 'Failed to republish to optimal relays: {{error}}', + 'Failed to republish to optimal relays: {{error}}': + 'Failed to republish to optimal relays: {{error}}', 'External Content': 'External Content', Highlight: 'Highlight', - 'Optimal relays and {{count}} other relays': 'Optimal relays and {{count}} other relays' + 'Optimal relays and {{count}} other relays': 'Optimal relays and {{count}} other relays', + 'Likely spam account (Trust score: {{percentile}}%)': + 'Likely spam account (Trust score: {{percentile}}%)', + 'Suspicious account (Trust score: {{percentile}}%)': + 'Suspicious account (Trust score: {{percentile}}%)' } } diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index 6a15bae..4764480 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -561,6 +561,8 @@ export default { 'Failed to republish to optimal relays: {{error}}': 'Error al republicar en relays óptimos: {{error}}', 'External Content': 'Contenido externo', Highlight: 'Destacado', - 'Optimal relays and {{count}} other relays': 'Relays óptimos y {{count}} otros relays' + 'Optimal relays and {{count}} other relays': 'Relays óptimos y {{count}} otros relays', + 'Likely spam account (Trust score: {{percentile}}%)': 'Probablemente cuenta spam (Puntuación de confianza: {{percentile}}%)', + 'Suspicious account (Trust score: {{percentile}}%)': 'Cuenta sospechosa (Puntuación de confianza: {{percentile}}%)' } } diff --git a/src/i18n/locales/fa.ts b/src/i18n/locales/fa.ts index 98d5a59..ae0bf45 100644 --- a/src/i18n/locales/fa.ts +++ b/src/i18n/locales/fa.ts @@ -555,6 +555,8 @@ export default { 'Failed to republish to optimal relays: {{error}}': 'خطا در انتشار مجدد در رله‌های بهینه: {{error}}', 'External Content': 'محتوای خارجی', Highlight: 'برجسته‌سازی', - 'Optimal relays and {{count}} other relays': 'رله‌های بهینه و {{count}} رله دیگر' + 'Optimal relays and {{count}} other relays': 'رله‌های بهینه و {{count}} رله دیگر', + 'Likely spam account (Trust score: {{percentile}}%)': 'احتمالاً حساب هرزنامه (امتیاز اعتماد: {{percentile}}%)', + 'Suspicious account (Trust score: {{percentile}}%)': 'حساب مشکوک (امتیاز اعتماد: {{percentile}}%)' } } diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index 0a957bd..65a273c 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -564,6 +564,8 @@ export default { 'Failed to republish to optimal relays: {{error}}': 'Échec de la republication sur les relais optimaux : {{error}}', 'External Content': 'Contenu externe', Highlight: 'Surligner', - 'Optimal relays and {{count}} other relays': 'Relais optimaux et {{count}} autres relais' + 'Optimal relays and {{count}} other relays': 'Relais optimaux et {{count}} autres relais', + 'Likely spam account (Trust score: {{percentile}}%)': 'Compte probablement spam (Score de confiance: {{percentile}}%)', + 'Suspicious account (Trust score: {{percentile}}%)': 'Compte suspect (Score de confiance: {{percentile}}%)' } } diff --git a/src/i18n/locales/hi.ts b/src/i18n/locales/hi.ts index f767251..2419984 100644 --- a/src/i18n/locales/hi.ts +++ b/src/i18n/locales/hi.ts @@ -556,6 +556,8 @@ export default { 'Failed to republish to optimal relays: {{error}}': 'इष्टतम रिले पर पुनः प्रकाशित करने में विफल: {{error}}', 'External Content': 'बाहरी सामग्री', Highlight: 'हाइलाइट', - 'Optimal relays and {{count}} other relays': 'इष्टतम रिले और {{count}} अन्य रिले' + 'Optimal relays and {{count}} other relays': 'इष्टतम रिले और {{count}} अन्य रिले', + 'Likely spam account (Trust score: {{percentile}}%)': 'संभावित स्पैम खाता (विश्वास स्कोर: {{percentile}}%)', + 'Suspicious account (Trust score: {{percentile}}%)': 'संदिग्ध खाता (विश्वास स्कोर: {{percentile}}%)' } } diff --git a/src/i18n/locales/hu.ts b/src/i18n/locales/hu.ts index ba8d1c8..d587da9 100644 --- a/src/i18n/locales/hu.ts +++ b/src/i18n/locales/hu.ts @@ -551,6 +551,8 @@ export default { 'Nem sikerült újra közzétenni az optimális relay-eken: {{error}}', 'External Content': 'Külső tartalom', Highlight: 'Kiemelés', - 'Optimal relays and {{count}} other relays': 'Optimális relay-ek és {{count}} másik relay' + 'Optimal relays and {{count}} other relays': 'Optimális relay-ek és {{count}} másik relay', + 'Likely spam account (Trust score: {{percentile}}%)': 'Valószínűleg spam fiók (Megbízhatósági pontszám: {{percentile}}%)', + 'Suspicious account (Trust score: {{percentile}}%)': 'Gyanús fiók (Megbízhatósági pontszám: {{percentile}}%)' } } diff --git a/src/i18n/locales/it.ts b/src/i18n/locales/it.ts index 3e8ef2a..4a0d571 100644 --- a/src/i18n/locales/it.ts +++ b/src/i18n/locales/it.ts @@ -560,6 +560,8 @@ export default { 'Failed to republish to optimal relays: {{error}}': 'Errore nella ripubblicazione sui relay ottimali: {{error}}', 'External Content': 'Contenuto esterno', Highlight: 'Evidenzia', - 'Optimal relays and {{count}} other relays': 'Relay ottimali e {{count}} altri relay' + 'Optimal relays and {{count}} other relays': 'Relay ottimali e {{count}} altri relay', + 'Likely spam account (Trust score: {{percentile}}%)': 'Probabile account spam (Punteggio di fiducia: {{percentile}}%)', + 'Suspicious account (Trust score: {{percentile}}%)': 'Account sospetto (Punteggio di fiducia: {{percentile}}%)' } } diff --git a/src/i18n/locales/ja.ts b/src/i18n/locales/ja.ts index 4825b8c..9abff23 100644 --- a/src/i18n/locales/ja.ts +++ b/src/i18n/locales/ja.ts @@ -555,6 +555,8 @@ export default { 'Failed to republish to optimal relays: {{error}}': '最適なリレーへの再公開に失敗しました:{{error}}', 'External Content': '外部コンテンツ', Highlight: 'ハイライト', - 'Optimal relays and {{count}} other relays': '最適なリレーと他の{{count}}個のリレー' + 'Optimal relays and {{count}} other relays': '最適なリレーと他の{{count}}個のリレー', + 'Likely spam account (Trust score: {{percentile}}%)': 'スパムの可能性が高いアカウント(信頼スコア:{{percentile}}%)', + 'Suspicious account (Trust score: {{percentile}}%)': '疑わしいアカウント(信頼スコア:{{percentile}}%)' } } diff --git a/src/i18n/locales/ko.ts b/src/i18n/locales/ko.ts index 79ed373..721054f 100644 --- a/src/i18n/locales/ko.ts +++ b/src/i18n/locales/ko.ts @@ -555,6 +555,8 @@ export default { 'Failed to republish to optimal relays: {{error}}': '최적 릴레이에 재게시 실패: {{error}}', 'External Content': '외부 콘텐츠', Highlight: '하이라이트', - 'Optimal relays and {{count}} other relays': '최적 릴레이 및 기타 {{count}}개 릴레이' + 'Optimal relays and {{count}} other relays': '최적 릴레이 및 기타 {{count}}개 릴레이', + 'Likely spam account (Trust score: {{percentile}}%)': '스팸 계정 가능성 높음 (신뢰 점수: {{percentile}}%)', + 'Suspicious account (Trust score: {{percentile}}%)': '의심스러운 계정 (신뢰 점수: {{percentile}}%)' } } diff --git a/src/i18n/locales/pl.ts b/src/i18n/locales/pl.ts index 58ec5d5..a55cfb7 100644 --- a/src/i18n/locales/pl.ts +++ b/src/i18n/locales/pl.ts @@ -561,6 +561,8 @@ export default { 'Nie udało się opublikować ponownie na optymalnych przekaźnikach: {{error}}', 'External Content': 'Treść zewnętrzna', Highlight: 'Podświetl', - 'Optimal relays and {{count}} other relays': 'Optymalne przekaźniki i {{count}} innych przekaźników' + 'Optimal relays and {{count}} other relays': 'Optymalne przekaźniki i {{count}} innych przekaźników', + 'Likely spam account (Trust score: {{percentile}}%)': 'Prawdopodobnie konto spamowe (Wynik zaufania: {{percentile}}%)', + 'Suspicious account (Trust score: {{percentile}}%)': 'Podejrzane konto (Wynik zaufania: {{percentile}}%)' } } diff --git a/src/i18n/locales/pt-BR.ts b/src/i18n/locales/pt-BR.ts index 33ab972..57a0912 100644 --- a/src/i18n/locales/pt-BR.ts +++ b/src/i18n/locales/pt-BR.ts @@ -556,6 +556,8 @@ export default { 'Failed to republish to optimal relays: {{error}}': 'Falha ao republicar nos relays ideais: {{error}}', 'External Content': 'Conteúdo externo', Highlight: 'Marcação', - 'Optimal relays and {{count}} other relays': 'Relays ideais e {{count}} outros relays' + 'Optimal relays and {{count}} other relays': 'Relays ideais e {{count}} outros relays', + 'Likely spam account (Trust score: {{percentile}}%)': 'Provável conta de spam (Pontuação de confiança: {{percentile}}%)', + 'Suspicious account (Trust score: {{percentile}}%)': 'Conta suspeita (Pontuação de confiança: {{percentile}}%)' } } diff --git a/src/i18n/locales/pt-PT.ts b/src/i18n/locales/pt-PT.ts index 65f5c9c..b9b4b6a 100644 --- a/src/i18n/locales/pt-PT.ts +++ b/src/i18n/locales/pt-PT.ts @@ -559,6 +559,8 @@ export default { 'Failed to republish to optimal relays: {{error}}': 'Falha ao republicar nos relays ideais: {{error}}', 'External Content': 'Conteúdo externo', Highlight: 'Destacar', - 'Optimal relays and {{count}} other relays': 'Relays ideais e {{count}} outros relays' + 'Optimal relays and {{count}} other relays': 'Relays ideais e {{count}} outros relays', + 'Likely spam account (Trust score: {{percentile}}%)': 'Provável conta de spam (Pontuação de confiança: {{percentile}}%)', + 'Suspicious account (Trust score: {{percentile}}%)': 'Conta suspeita (Pontuação de confiança: {{percentile}}%)' } } diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts index 2ab9c06..c1a3a5b 100644 --- a/src/i18n/locales/ru.ts +++ b/src/i18n/locales/ru.ts @@ -561,6 +561,8 @@ export default { 'Failed to republish to optimal relays: {{error}}': 'Не удалось опубликовать в оптимальные релеи: {{error}}', 'External Content': 'Внешний контент', Highlight: 'Выделить', - 'Optimal relays and {{count}} other relays': 'Оптимальные релеи и {{count}} других релеев' + 'Optimal relays and {{count}} other relays': 'Оптимальные релеи и {{count}} других релеев', + 'Likely spam account (Trust score: {{percentile}}%)': 'Вероятно спам-аккаунт (Оценка доверия: {{percentile}}%)', + 'Suspicious account (Trust score: {{percentile}}%)': 'Подозрительный аккаунт (Оценка доверия: {{percentile}}%)' } } diff --git a/src/i18n/locales/th.ts b/src/i18n/locales/th.ts index 56af3e5..8f5ffab 100644 --- a/src/i18n/locales/th.ts +++ b/src/i18n/locales/th.ts @@ -548,6 +548,8 @@ export default { 'Failed to republish to optimal relays: {{error}}': 'เผยแพร่ซ้ำไปยังรีเลย์ที่เหมาะสมล้มเหลว: {{error}}', 'External Content': 'เนื้อหาภายนอก', Highlight: 'ไฮไลต์', - 'Optimal relays and {{count}} other relays': 'รีเลย์ที่เหมาะสมและรีเลย์อื่น {{count}} รายการ' + 'Optimal relays and {{count}} other relays': 'รีเลย์ที่เหมาะสมและรีเลย์อื่น {{count}} รายการ', + 'Likely spam account (Trust score: {{percentile}}%)': 'น่าจะเป็นบัญชีสแปม (คะแนนความน่าเชื่อถือ: {{percentile}}%)', + 'Suspicious account (Trust score: {{percentile}}%)': 'บัญชีที่น่าสงสัย (คะแนนความน่าเชื่อถือ: {{percentile}}%)' } } diff --git a/src/i18n/locales/zh.ts b/src/i18n/locales/zh.ts index 0f2d69e..83decff 100644 --- a/src/i18n/locales/zh.ts +++ b/src/i18n/locales/zh.ts @@ -543,6 +543,8 @@ export default { 'Failed to republish to optimal relays: {{error}}': '重新发布到最佳中继器失败:{{error}}', 'External Content': '外部内容', Highlight: '高亮', - 'Optimal relays and {{count}} other relays': '最佳中继器和其他 {{count}} 个中继器' + 'Optimal relays and {{count}} other relays': '最佳中继器和其他 {{count}} 个中继器', + 'Likely spam account (Trust score: {{percentile}}%)': '疑似垃圾账号(信任分数:{{percentile}}%)', + 'Suspicious account (Trust score: {{percentile}}%)': '可疑账号(信任分数:{{percentile}}%)' } } diff --git a/src/services/trust-score.service.ts b/src/services/trust-score.service.ts new file mode 100644 index 0000000..5abd761 --- /dev/null +++ b/src/services/trust-score.service.ts @@ -0,0 +1,47 @@ +import DataLoader from 'dataloader' + +export interface TrustScoreData { + percentile: number +} + +class TrustScoreService { + static instance: TrustScoreService + + private trustScoreDataLoader = new DataLoader(async (userIds) => { + return await Promise.all( + userIds.map(async (userId) => { + try { + const res = await fetch(`https://fayan.jumble.social/${userId}`) + if (!res.ok) { + if (res.status === 404) { + return { percentile: 0 } + } + return null + } + const data = await res.json() + if (typeof data.percentile === 'number') { + return { percentile: data.percentile } + } + return null + } catch { + return null + } + }) + ) + }) + + constructor() { + if (!TrustScoreService.instance) { + TrustScoreService.instance = this + } + return TrustScoreService.instance + } + + async fetchTrustScore(userId: string): Promise { + return await this.trustScoreDataLoader.load(userId) + } +} + +const instance = new TrustScoreService() + +export default instance