Bluesky app fork with some witchin' additions 馃挮
at readme-update 165 lines 5.2 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({ 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] = React.useState<boolean>(isFollowing) 75 76 const enableSquareButtons = useEnableSquareButtons() 77 78 // This prevents the button from disappearing as soon as we follow. 79 const showFollowBtn = React.useMemo( 80 () => !isFollowing || !wasFollowing, 81 [isFollowing, wasFollowing], 82 ) 83 84 /** 85 * We want this button to stay visible even after following, so that the user can unfollow if they want. 86 * However, we need it to disappear after we push to a screen and then come back. We also need it to 87 * show up if we view the post while following, go to the profile and unfollow, then come back to the 88 * post. 89 * 90 * We want to update wasFollowing both on blur and on focus so that we hit all these cases. On native, 91 * we could do this only on focus because the transition animation gives us time to not notice the 92 * sudden rendering of the button. However, on web if we do this, there's an obvious flicker once the 93 * button renders. So, we update the state in both cases. 94 */ 95 React.useEffect(() => { 96 const updateWasFollowing = () => { 97 if (wasFollowing !== isFollowing) { 98 setWasFollowing(isFollowing) 99 } 100 } 101 102 const unsubscribeFocus = navigation.addListener('focus', updateWasFollowing) 103 const unsubscribeBlur = navigation.addListener('blur', updateWasFollowing) 104 105 return () => { 106 unsubscribeFocus() 107 unsubscribeBlur() 108 } 109 }, [isFollowing, wasFollowing, navigation]) 110 111 const onPress = React.useCallback(() => { 112 if (!isFollowing) { 113 requireAuth(async () => { 114 try { 115 await queueFollow() 116 } catch (e: any) { 117 if (e?.name !== 'AbortError') { 118 logger.error('Failed to follow', {message: String(e)}) 119 Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark') 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()}`), 'xmark') 131 } 132 } 133 }) 134 } 135 }, [isFollowing, requireAuth, queueFollow, _, queueUnfollow]) 136 137 if (!showFollowBtn) return null 138 139 return ( 140 <Button 141 testID="followBtn" 142 label={_(msg`Follow ${profile.handle}`)} 143 onPress={onPress} 144 size="small" 145 color={isFollowing ? 'secondary' : 'secondary_inverted'} 146 style={enableSquareButtons ? [a.rounded_sm] : [a.rounded_full]}> 147 {gtMobile && ( 148 <ButtonIcon icon={isFollowing ? CheckIcon : PlusIcon} size="sm" /> 149 )} 150 <ButtonText> 151 {!isFollowing ? ( 152 isFollowedBy ? ( 153 <Trans>Follow back</Trans> 154 ) : ( 155 <Trans>Follow</Trans> 156 ) 157 ) : isFollowedBy ? ( 158 <Trans>Mutuals</Trans> 159 ) : ( 160 <Trans>Following</Trans> 161 )} 162 </ButtonText> 163 </Button> 164 ) 165}