Bluesky app fork with some witchin' additions 馃挮
at linkat-integration 230 lines 6.5 kB view raw
1import React from 'react' 2import {type AppBskyActorDefs} from '@atproto/api' 3 4import {AccordionAnimation} from '#/lib/custom-animations/AccordionAnimation' 5import {useModerationOpts} from '#/state/preferences/moderation-opts' 6import { 7 useSuggestedFollowsByActorQuery, 8 useSuggestedFollowsQuery, 9} from '#/state/queries/suggested-follows' 10import {useBreakpoints} from '#/alf' 11import {ProfileGrid} from '#/components/FeedInterstitials' 12import {IS_ANDROID} from '#/env' 13 14const DISMISS_ANIMATION_DURATION = 200 15 16export function ProfileHeaderSuggestedFollows({actorDid}: {actorDid: string}) { 17 const {gtMobile} = useBreakpoints() 18 const moderationOpts = useModerationOpts() 19 const maxLength = gtMobile ? 4 : 12 20 const {isLoading, data, error} = useSuggestedFollowsByActorQuery({ 21 did: actorDid, 22 }) 23 const { 24 data: moreSuggestions, 25 fetchNextPage, 26 hasNextPage, 27 isFetchingNextPage, 28 } = useSuggestedFollowsQuery({limit: 25}) 29 30 const [dismissedDids, setDismissedDids] = React.useState<Set<string>>( 31 new Set(), 32 ) 33 const [dismissingDids, setDismissingDids] = React.useState<Set<string>>( 34 new Set(), 35 ) 36 37 const onDismiss = React.useCallback((did: string) => { 38 // Start the fade animation 39 setDismissingDids(prev => new Set(prev).add(did)) 40 // After animation completes, actually remove from list 41 setTimeout(() => { 42 setDismissedDids(prev => new Set(prev).add(did)) 43 setDismissingDids(prev => { 44 const next = new Set(prev) 45 next.delete(did) 46 return next 47 }) 48 }, DISMISS_ANIMATION_DURATION) 49 }, []) 50 51 // Combine profiles from the actor-specific query with fallback suggestions 52 const allProfiles = React.useMemo(() => { 53 const actorProfiles = data?.suggestions ?? [] 54 const fallbackProfiles = 55 moreSuggestions?.pages.flatMap(page => page.actors) ?? [] 56 57 // Dedupe by did, preferring actor-specific profiles 58 const seen = new Set<string>() 59 const combined: AppBskyActorDefs.ProfileView[] = [] 60 61 for (const profile of actorProfiles) { 62 if (!seen.has(profile.did)) { 63 seen.add(profile.did) 64 combined.push(profile) 65 } 66 } 67 68 for (const profile of fallbackProfiles) { 69 if (!seen.has(profile.did) && profile.did !== actorDid) { 70 seen.add(profile.did) 71 combined.push(profile) 72 } 73 } 74 75 return combined 76 }, [data?.suggestions, moreSuggestions?.pages, actorDid]) 77 78 const filteredProfiles = React.useMemo(() => { 79 return allProfiles.filter(p => !dismissedDids.has(p.did)) 80 }, [allProfiles, dismissedDids]) 81 82 // Fetch more when running low 83 React.useEffect(() => { 84 if ( 85 moderationOpts && 86 filteredProfiles.length < maxLength && 87 hasNextPage && 88 !isFetchingNextPage 89 ) { 90 fetchNextPage() 91 } 92 }, [ 93 filteredProfiles.length, 94 maxLength, 95 hasNextPage, 96 isFetchingNextPage, 97 fetchNextPage, 98 moderationOpts, 99 ]) 100 101 return ( 102 <ProfileGrid 103 isSuggestionsLoading={isLoading} 104 profiles={filteredProfiles} 105 totalProfileCount={allProfiles.length} 106 recId={data?.recId} 107 error={error} 108 viewContext="profileHeader" 109 onDismiss={onDismiss} 110 dismissingDids={dismissingDids} 111 /> 112 ) 113} 114 115export function AnimatedProfileHeaderSuggestedFollows({ 116 isExpanded, 117 actorDid, 118}: { 119 isExpanded: boolean 120 actorDid: string 121}) { 122 const {gtMobile} = useBreakpoints() 123 const moderationOpts = useModerationOpts() 124 const maxLength = gtMobile ? 4 : 12 125 const {isLoading, data, error} = useSuggestedFollowsByActorQuery({ 126 did: actorDid, 127 }) 128 const { 129 data: moreSuggestions, 130 fetchNextPage, 131 hasNextPage, 132 isFetchingNextPage, 133 } = useSuggestedFollowsQuery({limit: 25}) 134 135 const [dismissedDids, setDismissedDids] = React.useState<Set<string>>( 136 new Set(), 137 ) 138 const [dismissingDids, setDismissingDids] = React.useState<Set<string>>( 139 new Set(), 140 ) 141 142 const onDismiss = React.useCallback((did: string) => { 143 // Start the fade animation 144 setDismissingDids(prev => new Set(prev).add(did)) 145 // After animation completes, actually remove from list 146 setTimeout(() => { 147 setDismissedDids(prev => new Set(prev).add(did)) 148 setDismissingDids(prev => { 149 const next = new Set(prev) 150 next.delete(did) 151 return next 152 }) 153 }, DISMISS_ANIMATION_DURATION) 154 }, []) 155 156 // Combine profiles from the actor-specific query with fallback suggestions 157 const allProfiles = React.useMemo(() => { 158 const actorProfiles = data?.suggestions ?? [] 159 const fallbackProfiles = 160 moreSuggestions?.pages.flatMap(page => page.actors) ?? [] 161 162 // Dedupe by did, preferring actor-specific profiles 163 const seen = new Set<string>() 164 const combined: AppBskyActorDefs.ProfileView[] = [] 165 166 for (const profile of actorProfiles) { 167 if (!seen.has(profile.did)) { 168 seen.add(profile.did) 169 combined.push(profile) 170 } 171 } 172 173 for (const profile of fallbackProfiles) { 174 if (!seen.has(profile.did) && profile.did !== actorDid) { 175 seen.add(profile.did) 176 combined.push(profile) 177 } 178 } 179 180 return combined 181 }, [data?.suggestions, moreSuggestions?.pages, actorDid]) 182 183 const filteredProfiles = React.useMemo(() => { 184 return allProfiles.filter(p => !dismissedDids.has(p.did)) 185 }, [allProfiles, dismissedDids]) 186 187 // Fetch more when running low 188 React.useEffect(() => { 189 if ( 190 moderationOpts && 191 filteredProfiles.length < maxLength && 192 hasNextPage && 193 !isFetchingNextPage 194 ) { 195 fetchNextPage() 196 } 197 }, [ 198 filteredProfiles.length, 199 maxLength, 200 hasNextPage, 201 isFetchingNextPage, 202 fetchNextPage, 203 moderationOpts, 204 ]) 205 206 if (!allProfiles.length && !isLoading) return null 207 208 /* NOTE (caidanw): 209 * Android does not work well with this feature yet. 210 * This issue stems from Android not allowing dragging on clickable elements in the profile header. 211 * Blocking the ability to scroll on Android is too much of a trade-off for now. 212 **/ 213 if (IS_ANDROID) return null 214 215 return ( 216 <AccordionAnimation isExpanded={isExpanded}> 217 <ProfileGrid 218 isSuggestionsLoading={isLoading} 219 profiles={filteredProfiles} 220 totalProfileCount={allProfiles.length} 221 recId={data?.recId} 222 error={error} 223 viewContext="profileHeader" 224 onDismiss={onDismiss} 225 dismissingDids={dismissingDids} 226 isVisible={isExpanded} 227 /> 228 </AccordionAnimation> 229 ) 230}