forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import React from 'react'
2import {type AppBskyActorDefs} from '@atproto/api'
3import {msg, Trans} from '@lingui/macro'
4import {useLingui} from '@lingui/react'
5import {useNavigation} from '@react-navigation/native'
6
7import {logger} from '#/logger'
8import {useProfileShadow} from '#/state/cache/profile-shadow'
9import {
10 useProfileFollowMutationQueue,
11 useProfileQuery,
12} from '#/state/queries/profile'
13import {useRequireAuth} from '#/state/session'
14import * as Toast from '#/view/com/util/Toast'
15import {atoms as a, useBreakpoints} from '#/alf'
16import {Button, ButtonIcon, ButtonText} from '#/components/Button'
17import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
18import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
19import {IS_IOS} from '#/env'
20import {GrowthHack} from './GrowthHack'
21
22export function ThreadItemAnchorFollowButton({
23 did,
24 enabled = true,
25}: {
26 did: string
27 enabled?: boolean
28}) {
29 if (IS_IOS) {
30 return (
31 <GrowthHack>
32 <ThreadItemAnchorFollowButtonInner did={did} enabled={enabled} />
33 </GrowthHack>
34 )
35 }
36
37 return <ThreadItemAnchorFollowButtonInner did={did} enabled={enabled} />
38}
39
40export function ThreadItemAnchorFollowButtonInner({
41 did,
42 enabled = true,
43}: {
44 did: string
45 enabled?: boolean
46}) {
47 const {data: profile, isLoading} = useProfileQuery({did})
48
49 // We will never hit this - the profile will always be cached or loaded above
50 // but it keeps the typechecker happy
51 if (!enabled || isLoading || !profile) return null
52
53 return <PostThreadFollowBtnLoaded profile={profile} />
54}
55
56function PostThreadFollowBtnLoaded({
57 profile: profileUnshadowed,
58}: {
59 profile: AppBskyActorDefs.ProfileViewDetailed
60}) {
61 const navigation = useNavigation()
62 const {_} = useLingui()
63 const {gtMobile} = useBreakpoints()
64 const profile = useProfileShadow(profileUnshadowed)
65 const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
66 profile,
67 'PostThreadItem',
68 )
69 const requireAuth = useRequireAuth()
70
71 const isFollowing = !!profile.viewer?.following
72 const isFollowedBy = !!profile.viewer?.followedBy
73 const [wasFollowing, setWasFollowing] = React.useState<boolean>(isFollowing)
74
75 // This prevents the button from disappearing as soon as we follow.
76 const showFollowBtn = React.useMemo(
77 () => !isFollowing || !wasFollowing,
78 [isFollowing, wasFollowing],
79 )
80
81 /**
82 * We want this button to stay visible even after following, so that the user can unfollow if they want.
83 * However, we need it to disappear after we push to a screen and then come back. We also need it to
84 * show up if we view the post while following, go to the profile and unfollow, then come back to the
85 * post.
86 *
87 * We want to update wasFollowing both on blur and on focus so that we hit all these cases. On native,
88 * we could do this only on focus because the transition animation gives us time to not notice the
89 * sudden rendering of the button. However, on web if we do this, there's an obvious flicker once the
90 * button renders. So, we update the state in both cases.
91 */
92 React.useEffect(() => {
93 const updateWasFollowing = () => {
94 if (wasFollowing !== isFollowing) {
95 setWasFollowing(isFollowing)
96 }
97 }
98
99 const unsubscribeFocus = navigation.addListener('focus', updateWasFollowing)
100 const unsubscribeBlur = navigation.addListener('blur', updateWasFollowing)
101
102 return () => {
103 unsubscribeFocus()
104 unsubscribeBlur()
105 }
106 }, [isFollowing, wasFollowing, navigation])
107
108 const onPress = React.useCallback(() => {
109 if (!isFollowing) {
110 requireAuth(async () => {
111 try {
112 await queueFollow()
113 } catch (e: any) {
114 if (e?.name !== 'AbortError') {
115 logger.error('Failed to follow', {message: String(e)})
116 Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
117 }
118 }
119 })
120 } else {
121 requireAuth(async () => {
122 try {
123 await queueUnfollow()
124 } catch (e: any) {
125 if (e?.name !== 'AbortError') {
126 logger.error('Failed to unfollow', {message: String(e)})
127 Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
128 }
129 }
130 })
131 }
132 }, [isFollowing, requireAuth, queueFollow, _, queueUnfollow])
133
134 if (!showFollowBtn) return null
135
136 return (
137 <Button
138 testID="followBtn"
139 label={_(msg`Follow ${profile.handle}`)}
140 onPress={onPress}
141 size="small"
142 color={isFollowing ? 'secondary' : 'secondary_inverted'}
143 style={[a.rounded_full]}>
144 {gtMobile && (
145 <ButtonIcon icon={isFollowing ? CheckIcon : PlusIcon} size="sm" />
146 )}
147 <ButtonText>
148 {!isFollowing ? (
149 isFollowedBy ? (
150 <Trans>Follow back</Trans>
151 ) : (
152 <Trans>Follow</Trans>
153 )
154 ) : (
155 <Trans>Following</Trans>
156 )}
157 </ButtonText>
158 </Button>
159 )
160}