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