FollowListProvider.tsx raw
1 import {
2 FollowList,
3 fromFollowListToHexSet,
4 Pubkey,
5 CannotFollowSelfError
6 } from '@/domain'
7 import { FollowListRepositoryImpl } from '@/infrastructure/persistence'
8 import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
9 import { useTranslation } from 'react-i18next'
10 import { useNostr } from './NostrProvider'
11
12 type TFollowListContext = {
13 followingSet: Set<string>
14 followList: FollowList | null
15 isLoading: boolean
16 follow: (pubkey: string) => Promise<void>
17 unfollow: (pubkey: string) => Promise<void>
18 }
19
20 const FollowListContext = createContext<TFollowListContext | undefined>(undefined)
21
22 export const useFollowList = () => {
23 const context = useContext(FollowListContext)
24 if (!context) {
25 throw new Error('useFollowList must be used within a FollowListProvider')
26 }
27 return context
28 }
29
30 export function FollowListProvider({ children }: { children: React.ReactNode }) {
31 const { t } = useTranslation()
32 const { pubkey: accountPubkey, publish } = useNostr()
33
34 // State managed by this provider
35 const [followList, setFollowList] = useState<FollowList | null>(null)
36 const [isLoading, setIsLoading] = useState(false)
37
38 // Create repository instance
39 const repository = useMemo(() => {
40 if (!publish) return null
41 return new FollowListRepositoryImpl({ publish })
42 }, [publish])
43
44 // Legacy compatibility: expose as Set<string> for existing consumers
45 const followingSet = useMemo(
46 () => (followList ? fromFollowListToHexSet(followList) : new Set<string>()),
47 [followList]
48 )
49
50 // Load follow list when account changes
51 useEffect(() => {
52 const loadFollowList = async () => {
53 if (!accountPubkey || !repository) {
54 setFollowList(null)
55 return
56 }
57
58 setIsLoading(true)
59 try {
60 const ownerPubkey = Pubkey.tryFromString(accountPubkey)
61 if (!ownerPubkey) {
62 setFollowList(null)
63 return
64 }
65
66 const list = await repository.findByOwner(ownerPubkey)
67 setFollowList(list)
68 } catch (error) {
69 console.error('Failed to load follow list:', error)
70 setFollowList(null)
71 } finally {
72 setIsLoading(false)
73 }
74 }
75
76 loadFollowList()
77 }, [accountPubkey, repository])
78
79 const follow = useCallback(
80 async (pubkey: string) => {
81 if (!accountPubkey || !repository) return
82
83 const ownerPubkey = Pubkey.tryFromString(accountPubkey)
84 const targetPubkey = Pubkey.tryFromString(pubkey)
85 if (!ownerPubkey || !targetPubkey) return
86
87 try {
88 // Fetch latest to avoid conflicts
89 const currentFollowList = await repository.findByOwner(ownerPubkey)
90
91 if (!currentFollowList) {
92 const result = confirm(t('FollowListNotFoundConfirmation'))
93 if (!result) return
94 }
95
96 // Create or update using domain logic
97 const list = currentFollowList ?? FollowList.empty(ownerPubkey)
98
99 const change = list.follow(targetPubkey)
100 if (change.type === 'no_change') return
101
102 // Save via repository (handles publish and caching)
103 await repository.save(list)
104
105 // Update local state
106 setFollowList(list)
107 } catch (error) {
108 if (error instanceof CannotFollowSelfError) {
109 return
110 }
111 console.error('Failed to follow:', error)
112 throw error
113 }
114 },
115 [accountPubkey, repository, t]
116 )
117
118 const unfollow = useCallback(
119 async (pubkey: string) => {
120 if (!accountPubkey || !repository) return
121
122 const ownerPubkey = Pubkey.tryFromString(accountPubkey)
123 const targetPubkey = Pubkey.tryFromString(pubkey)
124 if (!ownerPubkey || !targetPubkey) return
125
126 try {
127 // Fetch latest to avoid conflicts
128 const currentFollowList = await repository.findByOwner(ownerPubkey)
129 if (!currentFollowList) return
130
131 const change = currentFollowList.unfollow(targetPubkey)
132 if (change.type === 'no_change') return
133
134 // Save via repository
135 await repository.save(currentFollowList)
136
137 // Update local state
138 setFollowList(currentFollowList)
139 } catch (error) {
140 console.error('Failed to unfollow:', error)
141 throw error
142 }
143 },
144 [accountPubkey, repository]
145 )
146
147 return (
148 <FollowListContext.Provider
149 value={{
150 followingSet,
151 followList,
152 isLoading,
153 follow,
154 unfollow
155 }}
156 >
157 {children}
158 </FollowListContext.Provider>
159 )
160 }
161