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