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