FollowedBy.tsx raw
1 import UserAvatar from '@/components/UserAvatar'
2 import { useNostr } from '@/providers/NostrProvider'
3 import { useScreenSize } from '@/providers/ScreenSizeProvider'
4 import client from '@/services/client.service'
5 import graphQueryService from '@/services/graph-query.service'
6 import { useEffect, useState } from 'react'
7 import { useTranslation } from 'react-i18next'
8
9 export default function FollowedBy({ pubkey }: { pubkey: string }) {
10 const { t } = useTranslation()
11 const { isSmallScreen } = useScreenSize()
12 const [followedBy, setFollowedBy] = useState<string[]>([])
13 const { pubkey: accountPubkey } = useNostr()
14
15 useEffect(() => {
16 if (!pubkey || !accountPubkey) return
17
18 const init = async () => {
19 const limit = isSmallScreen ? 3 : 5
20
21 // Try graph query first for depth-2 follows
22 const graphResult = await graphQueryService.queryFollowGraph(
23 client.currentRelays,
24 accountPubkey,
25 2
26 )
27
28 if (graphResult?.pubkeys_by_depth && graphResult.pubkeys_by_depth.length >= 2) {
29 // Use graph query results - much more efficient
30 const directFollows = new Set(graphResult.pubkeys_by_depth[0] ?? [])
31
32 // Check which of user's follows also follow the target pubkey
33 const _followedBy: string[] = []
34
35 // We need to check if target pubkey is in each direct follow's follow list
36 // The graph query gives us all follows of follows at depth 2,
37 // but we need to know *which* direct follow has the target in their follows
38 // For now, we'll still need to do individual checks but can optimize with caching
39
40 // Alternative approach: Use followers query on the target
41 const followerResult = await graphQueryService.queryFollowerGraph(
42 client.currentRelays,
43 pubkey,
44 1
45 )
46
47 if (followerResult?.pubkeys_by_depth?.[0]) {
48 // Followers of target pubkey
49 const targetFollowers = new Set(followerResult.pubkeys_by_depth[0])
50
51 // Find which of user's follows are followers of the target
52 for (const following of directFollows) {
53 if (following === pubkey) continue
54 if (targetFollowers.has(following)) {
55 _followedBy.push(following)
56 if (_followedBy.length >= limit) break
57 }
58 }
59 }
60
61 if (_followedBy.length > 0) {
62 setFollowedBy(_followedBy)
63 return
64 }
65 }
66
67 // Fallback to traditional method
68 const followings = (await client.fetchFollowings(accountPubkey)).reverse()
69 const followingsOfFollowings = await Promise.all(
70 followings.map(async (following) => {
71 return client.fetchFollowings(following)
72 })
73 )
74 const _followedBy: string[] = []
75 for (const [index, following] of followings.entries()) {
76 if (following === pubkey) continue
77 if (followingsOfFollowings[index].includes(pubkey)) {
78 _followedBy.push(following)
79 }
80 if (_followedBy.length >= limit) {
81 break
82 }
83 }
84 setFollowedBy(_followedBy)
85 }
86 init()
87 }, [pubkey, accountPubkey, isSmallScreen])
88
89 if (followedBy.length === 0) return null
90
91 return (
92 <div className="flex items-center gap-1">
93 <div className="text-muted-foreground">{t('Followed by')}</div>
94 {followedBy.map((p) => (
95 <UserAvatar userId={p} key={p} size="xSmall" />
96 ))}
97 </div>
98 )
99 }
100