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