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

Configure Feed

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

at main 170 lines 5.4 kB view raw
1import {useCallback, useEffect, useMemo, useState} 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 {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 * as Toast from '#/components/Toast' 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] = useState<boolean>(isFollowing) 76 77 const enableSquareButtons = useEnableSquareButtons() 78 79 // This prevents the button from disappearing as soon as we follow. 80 const showFollowBtn = 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 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 = 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()}`), { 121 type: 'error', 122 }) 123 } 124 } 125 }) 126 } else { 127 requireAuth(async () => { 128 try { 129 await queueUnfollow() 130 } catch (e: any) { 131 if (e?.name !== 'AbortError') { 132 logger.error('Failed to unfollow', {message: String(e)}) 133 Toast.show(_(msg`There was an issue! ${e.toString()}`), { 134 type: 'error', 135 }) 136 } 137 } 138 }) 139 } 140 }, [isFollowing, requireAuth, queueFollow, _, queueUnfollow]) 141 142 if (!showFollowBtn) return null 143 144 return ( 145 <Button 146 testID="followBtn" 147 label={_(msg`Follow ${profile.handle}`)} 148 onPress={onPress} 149 size="small" 150 color={isFollowing ? 'secondary' : 'secondary_inverted'} 151 style={enableSquareButtons ? [a.rounded_sm] : [a.rounded_full]}> 152 {gtMobile && ( 153 <ButtonIcon icon={isFollowing ? CheckIcon : PlusIcon} size="sm" /> 154 )} 155 <ButtonText maxFontSizeMultiplier={2}> 156 {!isFollowing ? ( 157 isFollowedBy ? ( 158 <Trans>Follow back</Trans> 159 ) : ( 160 <Trans>Follow</Trans> 161 ) 162 ) : isFollowedBy ? ( 163 <Trans>Mutuals</Trans> 164 ) : ( 165 <Trans>Following</Trans> 166 )} 167 </ButtonText> 168 </Button> 169 ) 170}