mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
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})