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