this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at e28f6d2f370b4e882ed6f23d08ca0f8d94dbac5f 165 lines 5.2 kB view raw
1import {useCallback, useEffect, useMemo, useState} from 'react' 2import {type AppBskyActorDefs} from '@atproto/api' 3import {msg} from '@lingui/core/macro' 4import {useLingui} from '@lingui/react' 5import {Trans} from '@lingui/react/macro' 6import {useNavigation} from '@react-navigation/native' 7 8import {logger} from '#/logger' 9import {useProfileShadow} from '#/state/cache/profile-shadow' 10import { 11 useProfileFollowMutationQueue, 12 useProfileQuery, 13} from '#/state/queries/profile' 14import {useRequireAuth} from '#/state/session' 15import {atoms as a, useBreakpoints} from '#/alf' 16import {Button, ButtonIcon, ButtonText} from '#/components/Button' 17import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check' 18import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus' 19import * as Toast from '#/components/Toast' 20import {IS_IOS} from '#/env' 21import {GrowthHack} from './GrowthHack' 22 23export function ThreadItemAnchorFollowButton({ 24 did, 25 enabled = true, 26}: { 27 did: string 28 enabled?: boolean 29}) { 30 if (IS_IOS) { 31 return ( 32 <GrowthHack> 33 <ThreadItemAnchorFollowButtonInner did={did} enabled={enabled} /> 34 </GrowthHack> 35 ) 36 } 37 38 return <ThreadItemAnchorFollowButtonInner did={did} enabled={enabled} /> 39} 40 41export function ThreadItemAnchorFollowButtonInner({ 42 did, 43 enabled = true, 44}: { 45 did: string 46 enabled?: boolean 47}) { 48 const {data: profile, isLoading} = useProfileQuery({did}) 49 50 // We will never hit this - the profile will always be cached or loaded above 51 // but it keeps the typechecker happy 52 if (!enabled || isLoading || !profile) return null 53 54 return <PostThreadFollowBtnLoaded profile={profile} /> 55} 56 57function PostThreadFollowBtnLoaded({ 58 profile: profileUnshadowed, 59}: { 60 profile: AppBskyActorDefs.ProfileViewDetailed 61}) { 62 const navigation = useNavigation() 63 const {_} = useLingui() 64 const {gtMobile} = useBreakpoints() 65 const profile = useProfileShadow(profileUnshadowed) 66 const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue( 67 profile, 68 'PostThreadItem', 69 ) 70 const requireAuth = useRequireAuth() 71 72 const isFollowing = !!profile.viewer?.following 73 const isFollowedBy = !!profile.viewer?.followedBy 74 const [wasFollowing, setWasFollowing] = useState<boolean>(isFollowing) 75 76 // This prevents the button from disappearing as soon as we follow. 77 const showFollowBtn = useMemo( 78 () => !isFollowing || !wasFollowing, 79 [isFollowing, wasFollowing], 80 ) 81 82 /** 83 * We want this button to stay visible even after following, so that the user can unfollow if they want. 84 * However, we need it to disappear after we push to a screen and then come back. We also need it to 85 * show up if we view the post while following, go to the profile and unfollow, then come back to the 86 * post. 87 * 88 * We want to update wasFollowing both on blur and on focus so that we hit all these cases. On native, 89 * we could do this only on focus because the transition animation gives us time to not notice the 90 * sudden rendering of the button. However, on web if we do this, there's an obvious flicker once the 91 * button renders. So, we update the state in both cases. 92 */ 93 useEffect(() => { 94 const updateWasFollowing = () => { 95 if (wasFollowing !== isFollowing) { 96 setWasFollowing(isFollowing) 97 } 98 } 99 100 const unsubscribeFocus = navigation.addListener('focus', updateWasFollowing) 101 const unsubscribeBlur = navigation.addListener('blur', updateWasFollowing) 102 103 return () => { 104 unsubscribeFocus() 105 unsubscribeBlur() 106 } 107 }, [isFollowing, wasFollowing, navigation]) 108 109 const onPress = useCallback(() => { 110 if (!isFollowing) { 111 requireAuth(async () => { 112 try { 113 await queueFollow() 114 } catch (e: any) { 115 if (e?.name !== 'AbortError') { 116 logger.error('Failed to follow', {message: String(e)}) 117 Toast.show(_(msg`There was an issue! ${e.toString()}`), { 118 type: 'error', 119 }) 120 } 121 } 122 }) 123 } else { 124 requireAuth(async () => { 125 try { 126 await queueUnfollow() 127 } catch (e: any) { 128 if (e?.name !== 'AbortError') { 129 logger.error('Failed to unfollow', {message: String(e)}) 130 Toast.show(_(msg`There was an issue! ${e.toString()}`), { 131 type: 'error', 132 }) 133 } 134 } 135 }) 136 } 137 }, [isFollowing, requireAuth, queueFollow, _, queueUnfollow]) 138 139 if (!showFollowBtn) return null 140 141 return ( 142 <Button 143 testID="followBtn" 144 label={_(msg`Follow ${profile.handle}`)} 145 onPress={onPress} 146 size="small" 147 color={isFollowing ? 'secondary' : 'secondary_inverted'} 148 style={[a.rounded_full]}> 149 {gtMobile && ( 150 <ButtonIcon icon={isFollowing ? CheckIcon : PlusIcon} size="sm" /> 151 )} 152 <ButtonText maxFontSizeMultiplier={2}> 153 {!isFollowing ? ( 154 isFollowedBy ? ( 155 <Trans>Follow back</Trans> 156 ) : ( 157 <Trans>Follow</Trans> 158 ) 159 ) : ( 160 <Trans>Following</Trans> 161 )} 162 </ButtonText> 163 </Button> 164 ) 165}