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

Configure Feed

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

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