SocialGraphFilterProvider.tsx raw
1 import { useFetchFollowGraph } from '@/hooks/useFetchFollowGraph'
2 import storage, { dispatchSettingsChanged } from '@/services/local-storage.service'
3 import { createContext, useCallback, useContext, useMemo, useState } from 'react'
4 import { useNostr } from './NostrProvider'
5
6 type TSocialGraphFilterContext = {
7 // Settings
8 proximityLevel: number | null // null = disabled, 1 = direct follows, 2 = follows of follows
9 includeMode: boolean // true = include only graph members, false = exclude graph members
10 updateProximityLevel: (level: number | null) => void
11 updateIncludeMode: (include: boolean) => void
12
13 // Cached data
14 graphPubkeys: Set<string> // Pre-computed Set for O(1) lookup
15 graphPubkeyCount: number
16 isLoading: boolean
17
18 // Filter function for use in feeds
19 isPubkeyAllowed: (pubkey: string) => boolean
20 }
21
22 const SocialGraphFilterContext = createContext<TSocialGraphFilterContext | undefined>(undefined)
23
24 export const useSocialGraphFilter = () => {
25 const context = useContext(SocialGraphFilterContext)
26 if (!context) {
27 throw new Error('useSocialGraphFilter must be used within a SocialGraphFilterProvider')
28 }
29 return context
30 }
31
32 export function SocialGraphFilterProvider({ children }: { children: React.ReactNode }) {
33 const { pubkey } = useNostr()
34 const [proximityLevel, setProximityLevel] = useState<number | null>(
35 storage.getSocialGraphProximity()
36 )
37 const [includeMode, setIncludeMode] = useState<boolean>(storage.getSocialGraphIncludeMode())
38
39 // Fetch the follow graph when proximity is enabled
40 const { pubkeysByDepth, isLoading } = useFetchFollowGraph(
41 proximityLevel !== null ? pubkey : null,
42 proximityLevel ?? 1
43 )
44
45 // Build the Set of graph pubkeys (always includes self)
46 const graphPubkeys = useMemo(() => {
47 const set = new Set<string>()
48
49 // Always include self in the graph
50 if (pubkey) {
51 set.add(pubkey)
52 }
53
54 // Add pubkeys up to selected depth
55 if (proximityLevel && pubkeysByDepth.length) {
56 for (let depth = 0; depth < proximityLevel && depth < pubkeysByDepth.length; depth++) {
57 pubkeysByDepth[depth].forEach((pk) => set.add(pk))
58 }
59 }
60
61 return set
62 }, [pubkey, proximityLevel, pubkeysByDepth])
63
64 const graphPubkeyCount = graphPubkeys.size
65
66 const updateProximityLevel = useCallback((level: number | null) => {
67 storage.setSocialGraphProximity(level)
68 setProximityLevel(level)
69 dispatchSettingsChanged()
70 }, [])
71
72 const updateIncludeMode = useCallback((include: boolean) => {
73 storage.setSocialGraphIncludeMode(include)
74 setIncludeMode(include)
75 dispatchSettingsChanged()
76 }, [])
77
78 const isPubkeyAllowed = useCallback(
79 (targetPubkey: string): boolean => {
80 // If filter disabled, allow all
81 if (proximityLevel === null) return true
82
83 // If loading, allow all (graceful degradation)
84 if (isLoading) return true
85
86 // Always allow self
87 if (targetPubkey === pubkey) return true
88
89 const isInGraph = graphPubkeys.has(targetPubkey)
90
91 // Include mode: only allow if in graph
92 // Exclude mode: only allow if NOT in graph
93 return includeMode ? isInGraph : !isInGraph
94 },
95 [proximityLevel, isLoading, graphPubkeys, includeMode, pubkey]
96 )
97
98 return (
99 <SocialGraphFilterContext.Provider
100 value={{
101 proximityLevel,
102 includeMode,
103 updateProximityLevel,
104 updateIncludeMode,
105 graphPubkeys,
106 graphPubkeyCount,
107 isLoading,
108 isPubkeyAllowed
109 }}
110 >
111 {children}
112 </SocialGraphFilterContext.Provider>
113 )
114 }
115