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