Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

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

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