mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at session/schema 168 lines 5.7 kB view raw
1import React from 'react' 2import {StyleSheet, TouchableOpacity, View} from 'react-native' 3import {AppBskyActorDefs} from '@atproto/api' 4import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5import {msg, Trans} from '@lingui/macro' 6import {useLingui} from '@lingui/react' 7import {useNavigation} from '@react-navigation/native' 8 9import {useGate} from '#/lib/statsig/statsig' 10import {logger} from '#/logger' 11import {track} from 'lib/analytics/analytics' 12import {usePalette} from 'lib/hooks/usePalette' 13import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 14import {s} from 'lib/styles' 15import {Shadow, useProfileShadow} from 'state/cache/profile-shadow' 16import { 17 useProfileFollowMutationQueue, 18 useProfileQuery, 19} from 'state/queries/profile' 20import {useRequireAuth} from 'state/session' 21import {Text} from 'view/com/util/text/Text' 22import * as Toast from 'view/com/util/Toast' 23 24export function PostThreadFollowBtn({did}: {did: string}) { 25 const {data: profile, isLoading} = useProfileQuery({did}) 26 27 // We will never hit this - the profile will always be cached or loaded above 28 // but it keeps the typechecker happy 29 if (isLoading || !profile) return null 30 31 return <PostThreadFollowBtnLoaded profile={profile} /> 32} 33 34function PostThreadFollowBtnLoaded({ 35 profile: profileUnshadowed, 36}: { 37 profile: AppBskyActorDefs.ProfileViewDetailed 38}) { 39 const navigation = useNavigation() 40 const {_} = useLingui() 41 const pal = usePalette('default') 42 const palInverted = usePalette('inverted') 43 const {isTabletOrDesktop} = useWebMediaQueries() 44 const profile: Shadow<AppBskyActorDefs.ProfileViewBasic> = 45 useProfileShadow(profileUnshadowed) 46 const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue( 47 profile, 48 'PostThreadItem', 49 ) 50 const requireAuth = useRequireAuth() 51 const gate = useGate() 52 53 const isFollowing = !!profile.viewer?.following 54 const isFollowedBy = !!profile.viewer?.followedBy 55 const [wasFollowing, setWasFollowing] = React.useState<boolean>(isFollowing) 56 57 // This prevents the button from disappearing as soon as we follow. 58 const showFollowBtn = React.useMemo( 59 () => !isFollowing || !wasFollowing, 60 [isFollowing, wasFollowing], 61 ) 62 63 /** 64 * We want this button to stay visible even after following, so that the user can unfollow if they want. 65 * However, we need it to disappear after we push to a screen and then come back. We also need it to 66 * show up if we view the post while following, go to the profile and unfollow, then come back to the 67 * post. 68 * 69 * We want to update wasFollowing both on blur and on focus so that we hit all these cases. On native, 70 * we could do this only on focus because the transition animation gives us time to not notice the 71 * sudden rendering of the button. However, on web if we do this, there's an obvious flicker once the 72 * button renders. So, we update the state in both cases. 73 */ 74 React.useEffect(() => { 75 const updateWasFollowing = () => { 76 if (wasFollowing !== isFollowing) { 77 setWasFollowing(isFollowing) 78 } 79 } 80 81 const unsubscribeFocus = navigation.addListener('focus', updateWasFollowing) 82 const unsubscribeBlur = navigation.addListener('blur', updateWasFollowing) 83 84 return () => { 85 unsubscribeFocus() 86 unsubscribeBlur() 87 } 88 }, [isFollowing, wasFollowing, navigation]) 89 90 const onPress = React.useCallback(() => { 91 if (!isFollowing) { 92 requireAuth(async () => { 93 try { 94 track('ProfileHeader:FollowButtonClicked') 95 await queueFollow() 96 } catch (e: any) { 97 if (e?.name !== 'AbortError') { 98 logger.error('Failed to follow', {message: String(e)}) 99 Toast.show(_(msg`There was an issue! ${e.toString()}`)) 100 } 101 } 102 }) 103 } else { 104 requireAuth(async () => { 105 try { 106 track('ProfileHeader:UnfollowButtonClicked') 107 await queueUnfollow() 108 } catch (e: any) { 109 if (e?.name !== 'AbortError') { 110 logger.error('Failed to unfollow', {message: String(e)}) 111 Toast.show(_(msg`There was an issue! ${e.toString()}`)) 112 } 113 } 114 }) 115 } 116 }, [isFollowing, requireAuth, queueFollow, _, queueUnfollow]) 117 118 if (!showFollowBtn) return null 119 120 return ( 121 <View style={{width: isTabletOrDesktop ? 130 : 120}}> 122 <View style={styles.btnOuter}> 123 <TouchableOpacity 124 testID="followBtn" 125 onPress={onPress} 126 style={[styles.btn, !isFollowing ? palInverted.view : pal.viewLight]} 127 accessibilityRole="button" 128 accessibilityLabel={_(msg`Follow ${profile.handle}`)} 129 accessibilityHint={_( 130 msg`Shows posts from ${profile.handle} in your feed`, 131 )}> 132 {isTabletOrDesktop && ( 133 <FontAwesomeIcon 134 icon={!isFollowing ? 'plus' : 'check'} 135 style={[!isFollowing ? palInverted.text : pal.text, s.mr5]} 136 /> 137 )} 138 <Text 139 type="button" 140 style={[!isFollowing ? palInverted.text : pal.text, s.bold]} 141 numberOfLines={1}> 142 {!isFollowing ? ( 143 isFollowedBy && gate('show_follow_back_label_v2') ? ( 144 <Trans>Follow Back</Trans> 145 ) : ( 146 <Trans>Follow</Trans> 147 ) 148 ) : ( 149 <Trans>Following</Trans> 150 )} 151 </Text> 152 </TouchableOpacity> 153 </View> 154 </View> 155 ) 156} 157 158const styles = StyleSheet.create({ 159 btnOuter: { 160 marginLeft: 'auto', 161 }, 162 btn: { 163 flexDirection: 'row', 164 borderRadius: 50, 165 paddingVertical: 8, 166 paddingHorizontal: 14, 167 }, 168})