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