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

Configure Feed

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

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