Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
120
fork

Configure Feed

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

at a876aae44ea07494ebea9727350aa060b81f317b 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}