feat: hide relay reviews from spammer
This commit is contained in:
@@ -6,7 +6,6 @@ import { useKindFilter } from '@/providers/KindFilterProvider'
|
|||||||
import { useUserTrust } from '@/providers/UserTrustProvider'
|
import { useUserTrust } from '@/providers/UserTrustProvider'
|
||||||
import storage from '@/services/local-storage.service'
|
import storage from '@/services/local-storage.service'
|
||||||
import { TFeedSubRequest, TNoteListMode } from '@/types'
|
import { TFeedSubRequest, TNoteListMode } from '@/types'
|
||||||
import { Event } from 'nostr-tools'
|
|
||||||
import { useMemo, useRef, useState } from 'react'
|
import { useMemo, useRef, useState } from 'react'
|
||||||
import KindFilter from '../KindFilter'
|
import KindFilter from '../KindFilter'
|
||||||
import { RefreshButton } from '../RefreshButton'
|
import { RefreshButton } from '../RefreshButton'
|
||||||
@@ -16,14 +15,12 @@ export default function NormalFeed({
|
|||||||
areAlgoRelays = false,
|
areAlgoRelays = false,
|
||||||
isMainFeed = false,
|
isMainFeed = false,
|
||||||
showRelayCloseReason = false,
|
showRelayCloseReason = false,
|
||||||
filterFn,
|
|
||||||
disable24hMode = false
|
disable24hMode = false
|
||||||
}: {
|
}: {
|
||||||
subRequests: TFeedSubRequest[]
|
subRequests: TFeedSubRequest[]
|
||||||
areAlgoRelays?: boolean
|
areAlgoRelays?: boolean
|
||||||
isMainFeed?: boolean
|
isMainFeed?: boolean
|
||||||
showRelayCloseReason?: boolean
|
showRelayCloseReason?: boolean
|
||||||
filterFn?: (event: Event) => boolean
|
|
||||||
disable24hMode?: boolean
|
disable24hMode?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { hideUntrustedNotes } = useUserTrust()
|
const { hideUntrustedNotes } = useUserTrust()
|
||||||
@@ -91,7 +88,6 @@ export default function NormalFeed({
|
|||||||
ref={userAggregationListRef}
|
ref={userAggregationListRef}
|
||||||
showKinds={temporaryShowKinds}
|
showKinds={temporaryShowKinds}
|
||||||
subRequests={subRequests}
|
subRequests={subRequests}
|
||||||
filterFn={filterFn}
|
|
||||||
areAlgoRelays={areAlgoRelays}
|
areAlgoRelays={areAlgoRelays}
|
||||||
showRelayCloseReason={showRelayCloseReason}
|
showRelayCloseReason={showRelayCloseReason}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ const NoteList = forwardRef<
|
|||||||
filterMutedNotes?: boolean
|
filterMutedNotes?: boolean
|
||||||
hideReplies?: boolean
|
hideReplies?: boolean
|
||||||
hideUntrustedNotes?: boolean
|
hideUntrustedNotes?: boolean
|
||||||
|
hideSpam?: boolean
|
||||||
areAlgoRelays?: boolean
|
areAlgoRelays?: boolean
|
||||||
showRelayCloseReason?: boolean
|
showRelayCloseReason?: boolean
|
||||||
pinnedEventIds?: string[]
|
pinnedEventIds?: string[]
|
||||||
@@ -60,6 +61,7 @@ const NoteList = forwardRef<
|
|||||||
filterMutedNotes = true,
|
filterMutedNotes = true,
|
||||||
hideReplies = false,
|
hideReplies = false,
|
||||||
hideUntrustedNotes = false,
|
hideUntrustedNotes = false,
|
||||||
|
hideSpam = false,
|
||||||
areAlgoRelays = false,
|
areAlgoRelays = false,
|
||||||
showRelayCloseReason = false,
|
showRelayCloseReason = false,
|
||||||
pinnedEventIds,
|
pinnedEventIds,
|
||||||
@@ -70,7 +72,7 @@ const NoteList = forwardRef<
|
|||||||
) => {
|
) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { startLogin } = useNostr()
|
const { startLogin } = useNostr()
|
||||||
const { isUserTrusted } = useUserTrust()
|
const { isUserTrusted, isSpammer } = useUserTrust()
|
||||||
const { mutePubkeySet } = useMuteList()
|
const { mutePubkeySet } = useMuteList()
|
||||||
const { hideContentMentioningMutedUsers } = useContentPolicy()
|
const { hideContentMentioningMutedUsers } = useContentPolicy()
|
||||||
const { isEventDeleted } = useDeletedEvent()
|
const { isEventDeleted } = useDeletedEvent()
|
||||||
@@ -79,7 +81,12 @@ const NoteList = forwardRef<
|
|||||||
const [newEvents, setNewEvents] = useState<Event[]>([])
|
const [newEvents, setNewEvents] = useState<Event[]>([])
|
||||||
const [hasMore, setHasMore] = useState<boolean>(true)
|
const [hasMore, setHasMore] = useState<boolean>(true)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [filtering, setFiltering] = useState(false)
|
||||||
const [timelineKey, setTimelineKey] = useState<string | undefined>(undefined)
|
const [timelineKey, setTimelineKey] = useState<string | undefined>(undefined)
|
||||||
|
const [filteredNotes, setFilteredNotes] = useState<
|
||||||
|
{ key: string; event: Event; reposters: string[] }[]
|
||||||
|
>([])
|
||||||
|
const [filteredNewEvents, setFilteredNewEvents] = useState<Event[]>([])
|
||||||
const [refreshCount, setRefreshCount] = useState(0)
|
const [refreshCount, setRefreshCount] = useState(0)
|
||||||
const [showCount, setShowCount] = useState(SHOW_COUNT)
|
const [showCount, setShowCount] = useState(SHOW_COUNT)
|
||||||
const supportTouch = useMemo(() => isTouchDevice(), [])
|
const supportTouch = useMemo(() => isTouchDevice(), [])
|
||||||
@@ -120,7 +127,8 @@ const NoteList = forwardRef<
|
|||||||
[hideUntrustedNotes, mutePubkeySet, JSON.stringify(pinnedEventIds), isEventDeleted, filterFn]
|
[hideUntrustedNotes, mutePubkeySet, JSON.stringify(pinnedEventIds), isEventDeleted, filterFn]
|
||||||
)
|
)
|
||||||
|
|
||||||
const filteredNotes = useMemo(() => {
|
useEffect(() => {
|
||||||
|
const processEvents = async () => {
|
||||||
// Store processed event keys to avoid duplicates
|
// Store processed event keys to avoid duplicates
|
||||||
const keySet = new Set<string>()
|
const keySet = new Set<string>()
|
||||||
// Map to track reposters for each event key
|
// Map to track reposters for each event key
|
||||||
@@ -187,31 +195,64 @@ const NoteList = forwardRef<
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return filteredEvents.map((evt, i) => {
|
const _filteredNotes = (
|
||||||
|
await Promise.all(
|
||||||
|
filteredEvents.map(async (evt, i) => {
|
||||||
|
if (hideSpam && (await isSpammer(evt.pubkey))) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
const key = keys[i]
|
const key = keys[i]
|
||||||
return { key, event: evt, reposters: Array.from(repostersMap.get(key) ?? []) }
|
return { key, event: evt, reposters: Array.from(repostersMap.get(key) ?? []) }
|
||||||
})
|
})
|
||||||
}, [events, shouldHideEvent, hideReplies])
|
)
|
||||||
|
).filter(Boolean) as {
|
||||||
|
key: string
|
||||||
|
event: Event
|
||||||
|
reposters: string[]
|
||||||
|
}[]
|
||||||
|
|
||||||
|
setFilteredNotes(_filteredNotes)
|
||||||
|
}
|
||||||
|
|
||||||
|
setFiltering(true)
|
||||||
|
processEvents().finally(() => setFiltering(false))
|
||||||
|
}, [events, shouldHideEvent, hideReplies, isSpammer, hideSpam])
|
||||||
|
|
||||||
const slicedNotes = useMemo(() => {
|
const slicedNotes = useMemo(() => {
|
||||||
return filteredNotes.slice(0, showCount)
|
return filteredNotes.slice(0, showCount)
|
||||||
}, [filteredNotes, showCount])
|
}, [filteredNotes, showCount])
|
||||||
|
|
||||||
const filteredNewEvents = useMemo(() => {
|
useEffect(() => {
|
||||||
|
const processNewEvents = async () => {
|
||||||
const keySet = new Set<string>()
|
const keySet = new Set<string>()
|
||||||
|
const filteredEvents: Event[] = []
|
||||||
|
|
||||||
return newEvents.filter((event: Event) => {
|
newEvents.forEach((event) => {
|
||||||
if (shouldHideEvent(event)) return false
|
if (shouldHideEvent(event)) return
|
||||||
if (hideReplies && isReplyNoteEvent(event)) return false
|
if (hideReplies && isReplyNoteEvent(event)) return
|
||||||
|
|
||||||
const key = getEventKey(event)
|
const key = getEventKey(event)
|
||||||
if (keySet.has(key)) {
|
if (keySet.has(key)) {
|
||||||
return false
|
return
|
||||||
}
|
}
|
||||||
keySet.add(key)
|
keySet.add(key)
|
||||||
return true
|
filteredEvents.push(event)
|
||||||
})
|
})
|
||||||
}, [newEvents, shouldHideEvent])
|
|
||||||
|
const _filteredNotes = (
|
||||||
|
await Promise.all(
|
||||||
|
filteredEvents.map(async (evt) => {
|
||||||
|
if (hideSpam && (await isSpammer(evt.pubkey))) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return evt
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).filter(Boolean) as Event[]
|
||||||
|
setFilteredNewEvents(_filteredNotes)
|
||||||
|
}
|
||||||
|
processNewEvents()
|
||||||
|
}, [newEvents, shouldHideEvent, isSpammer, hideSpam])
|
||||||
|
|
||||||
const scrollToTop = (behavior: ScrollBehavior = 'instant') => {
|
const scrollToTop = (behavior: ScrollBehavior = 'instant') => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -381,7 +422,7 @@ const NoteList = forwardRef<
|
|||||||
reposters={reposters}
|
reposters={reposters}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{hasMore || loading ? (
|
{hasMore || loading || filtering ? (
|
||||||
<div ref={bottomRef}>
|
<div ref={bottomRef}>
|
||||||
<NoteCardLoadingSkeleton />
|
<NoteCardLoadingSkeleton />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export default function RelayReviewsPreview({ relayUrl }: { relayUrl: string })
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { push } = useSecondaryPage()
|
const { push } = useSecondaryPage()
|
||||||
const { pubkey, checkLogin } = useNostr()
|
const { pubkey, checkLogin } = useNostr()
|
||||||
const { hideUntrustedNotes, isUserTrusted } = useUserTrust()
|
const { hideUntrustedNotes, isUserTrusted, isSpammer } = useUserTrust()
|
||||||
const { mutePubkeySet } = useMuteList()
|
const { mutePubkeySet } = useMuteList()
|
||||||
const [showEditor, setShowEditor] = useState(false)
|
const [showEditor, setShowEditor] = useState(false)
|
||||||
const [myReview, setMyReview] = useState<NostrEvent | null>(null)
|
const [myReview, setMyReview] = useState<NostrEvent | null>(null)
|
||||||
@@ -69,12 +69,9 @@ export default function RelayReviewsPreview({ relayUrl }: { relayUrl: string })
|
|||||||
let myReview: NostrEvent | null = null
|
let myReview: NostrEvent | null = null
|
||||||
|
|
||||||
events.sort((a, b) => compareEvents(b, a))
|
events.sort((a, b) => compareEvents(b, a))
|
||||||
|
|
||||||
for (const evt of events) {
|
for (const evt of events) {
|
||||||
if (
|
if (mutePubkeySet.has(evt.pubkey) || pubkeySet.has(evt.pubkey)) {
|
||||||
mutePubkeySet.has(evt.pubkey) ||
|
|
||||||
pubkeySet.has(evt.pubkey) ||
|
|
||||||
(hideUntrustedNotes && !isUserTrusted(evt.pubkey))
|
|
||||||
) {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const stars = getStarsFromRelayReviewEvent(evt)
|
const stars = getStarsFromRelayReviewEvent(evt)
|
||||||
@@ -90,8 +87,19 @@ export default function RelayReviewsPreview({ relayUrl }: { relayUrl: string })
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filteredReviews = (
|
||||||
|
await Promise.all(
|
||||||
|
reviews.map(async (evt) => {
|
||||||
|
if (await isSpammer(evt.pubkey)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return evt
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).filter(Boolean) as NostrEvent[]
|
||||||
|
|
||||||
setMyReview(myReview)
|
setMyReview(myReview)
|
||||||
setReviews(reviews)
|
setReviews(filteredReviews)
|
||||||
setInitialized(true)
|
setInitialized(true)
|
||||||
}
|
}
|
||||||
init()
|
init()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useNostr } from '@/providers/NostrProvider'
|
import { useNostr } from '@/providers/NostrProvider'
|
||||||
import { useUserTrust } from '@/providers/UserTrustProvider'
|
import { useUserTrust } from '@/providers/UserTrustProvider'
|
||||||
import trustScoreService from '@/services/trust-score.service'
|
import fayan from '@/services/fayan.service'
|
||||||
import { AlertTriangle, ShieldAlert } from 'lucide-react'
|
import { AlertTriangle, ShieldAlert } from 'lucide-react'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@@ -34,9 +34,9 @@ export default function TrustScoreBadge({
|
|||||||
|
|
||||||
const fetchScore = async () => {
|
const fetchScore = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await trustScoreService.fetchTrustScore(pubkey)
|
const percentile = await fayan.fetchUserPercentile(pubkey)
|
||||||
if (data) {
|
if (percentile !== null) {
|
||||||
setPercentile(data.percentile)
|
setPercentile(percentile)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch trust score:', error)
|
console.error('Failed to fetch trust score:', error)
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ const UserAggregationList = forwardRef<
|
|||||||
{
|
{
|
||||||
subRequests: TFeedSubRequest[]
|
subRequests: TFeedSubRequest[]
|
||||||
showKinds?: number[]
|
showKinds?: number[]
|
||||||
filterFn?: (event: Event) => boolean
|
|
||||||
filterMutedNotes?: boolean
|
filterMutedNotes?: boolean
|
||||||
areAlgoRelays?: boolean
|
areAlgoRelays?: boolean
|
||||||
showRelayCloseReason?: boolean
|
showRelayCloseReason?: boolean
|
||||||
@@ -58,7 +57,6 @@ const UserAggregationList = forwardRef<
|
|||||||
{
|
{
|
||||||
subRequests,
|
subRequests,
|
||||||
showKinds,
|
showKinds,
|
||||||
filterFn,
|
|
||||||
filterMutedNotes = true,
|
filterMutedNotes = true,
|
||||||
areAlgoRelays = false,
|
areAlgoRelays = false,
|
||||||
showRelayCloseReason = false
|
showRelayCloseReason = false
|
||||||
@@ -242,9 +240,6 @@ const UserAggregationList = forwardRef<
|
|||||||
) {
|
) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if (filterFn && !filterFn(evt)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
@@ -252,7 +247,6 @@ const UserAggregationList = forwardRef<
|
|||||||
hideUntrustedNotes,
|
hideUntrustedNotes,
|
||||||
mutePubkeySet,
|
mutePubkeySet,
|
||||||
isEventDeleted,
|
isEventDeleted,
|
||||||
filterFn,
|
|
||||||
currentPubkey,
|
currentPubkey,
|
||||||
filterMutedNotes,
|
filterMutedNotes,
|
||||||
isUserTrusted,
|
isUserTrusted,
|
||||||
|
|||||||
@@ -45,9 +45,9 @@ const ExplorePage = forwardRef<TPageRef>((_, ref) => {
|
|||||||
<NoteList
|
<NoteList
|
||||||
showKinds={[ExtendedKind.RELAY_REVIEW]}
|
showKinds={[ExtendedKind.RELAY_REVIEW]}
|
||||||
subRequests={[{ urls: BIG_RELAY_URLS, filter: {} }]}
|
subRequests={[{ urls: BIG_RELAY_URLS, filter: {} }]}
|
||||||
filterMutedNotes
|
|
||||||
hideUntrustedNotes={hideUntrustedNotes}
|
|
||||||
filterFn={relayReviewFilterFn}
|
filterFn={relayReviewFilterFn}
|
||||||
|
filterMutedNotes
|
||||||
|
hideSpam
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<FollowingFavoriteRelayList />
|
<FollowingFavoriteRelayList />
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const RelayReviewsPage = forwardRef(({ url, index }: { url?: string; index?: num
|
|||||||
filter: { '#d': [normalizedUrl] }
|
filter: { '#d': [normalizedUrl] }
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
|
hideSpam
|
||||||
/>
|
/>
|
||||||
</SecondaryPageLayout>
|
</SecondaryPageLayout>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import client from '@/services/client.service'
|
import client from '@/services/client.service'
|
||||||
|
import fayan from '@/services/fayan.service'
|
||||||
import storage from '@/services/local-storage.service'
|
import storage from '@/services/local-storage.service'
|
||||||
import { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
import { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
||||||
import { useNostr } from './NostrProvider'
|
import { useNostr } from './NostrProvider'
|
||||||
@@ -11,6 +12,7 @@ type TUserTrustContext = {
|
|||||||
updateHideUntrustedNotifications: (hide: boolean) => void
|
updateHideUntrustedNotifications: (hide: boolean) => void
|
||||||
updateHideUntrustedNotes: (hide: boolean) => void
|
updateHideUntrustedNotes: (hide: boolean) => void
|
||||||
isUserTrusted: (pubkey: string) => boolean
|
isUserTrusted: (pubkey: string) => boolean
|
||||||
|
isSpammer: (pubkey: string) => Promise<boolean>
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserTrustContext = createContext<TUserTrustContext | undefined>(undefined)
|
const UserTrustContext = createContext<TUserTrustContext | undefined>(undefined)
|
||||||
@@ -69,6 +71,16 @@ export function UserTrustProvider({ children }: { children: React.ReactNode }) {
|
|||||||
[currentPubkey]
|
[currentPubkey]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const isSpammer = useCallback(
|
||||||
|
async (pubkey: string) => {
|
||||||
|
if (isUserTrusted(pubkey)) return false
|
||||||
|
const percentile = await fayan.fetchUserPercentile(pubkey)
|
||||||
|
if (percentile === null) return false
|
||||||
|
return percentile < 60
|
||||||
|
},
|
||||||
|
[isUserTrusted]
|
||||||
|
)
|
||||||
|
|
||||||
const updateHideUntrustedInteractions = (hide: boolean) => {
|
const updateHideUntrustedInteractions = (hide: boolean) => {
|
||||||
setHideUntrustedInteractions(hide)
|
setHideUntrustedInteractions(hide)
|
||||||
storage.setHideUntrustedInteractions(hide)
|
storage.setHideUntrustedInteractions(hide)
|
||||||
@@ -93,7 +105,8 @@ export function UserTrustProvider({ children }: { children: React.ReactNode }) {
|
|||||||
updateHideUntrustedInteractions,
|
updateHideUntrustedInteractions,
|
||||||
updateHideUntrustedNotifications,
|
updateHideUntrustedNotifications,
|
||||||
updateHideUntrustedNotes,
|
updateHideUntrustedNotes,
|
||||||
isUserTrusted
|
isUserTrusted,
|
||||||
|
isSpammer
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
43
src/services/fayan.service.ts
Normal file
43
src/services/fayan.service.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import DataLoader from 'dataloader'
|
||||||
|
|
||||||
|
class FayanService {
|
||||||
|
static instance: FayanService
|
||||||
|
|
||||||
|
private userPercentileDataLoader = new DataLoader<string, number | null>(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 0
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const data = await res.json()
|
||||||
|
if (typeof data.percentile === 'number') {
|
||||||
|
return data.percentile
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
if (!FayanService.instance) {
|
||||||
|
FayanService.instance = this
|
||||||
|
}
|
||||||
|
return FayanService.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
// null means server error
|
||||||
|
async fetchUserPercentile(userId: string): Promise<number | null> {
|
||||||
|
return await this.userPercentileDataLoader.load(userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = new FayanService()
|
||||||
|
export default instance
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import DataLoader from 'dataloader'
|
|
||||||
|
|
||||||
export interface TrustScoreData {
|
|
||||||
percentile: number
|
|
||||||
}
|
|
||||||
|
|
||||||
class TrustScoreService {
|
|
||||||
static instance: TrustScoreService
|
|
||||||
|
|
||||||
private trustScoreDataLoader = new DataLoader<string, TrustScoreData | null>(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<TrustScoreData | null> {
|
|
||||||
return await this.trustScoreDataLoader.load(userId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const instance = new TrustScoreService()
|
|
||||||
|
|
||||||
export default instance
|
|
||||||
Reference in New Issue
Block a user