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