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