diff options
author | Eric Bailey <git@esb.lol> | 2025-08-26 09:54:19 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-08-26 09:54:19 -0500 |
commit | df20ae237eaf434c6ed0fd032f8328cd9b8c352c (patch) | |
tree | eecd070cf125acc908b1137a569aa369fe5fc436 /src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx | |
parent | e91a6838101c9566ce2dafaa6fe8c77293a5eba6 (diff) | |
download | voidsky-df20ae237eaf434c6ed0fd032f8328cd9b8c352c.tar.zst |
Threads v2 cleanup (#8902)
* Delete root PostThread component * Remove PostThreadItem, migrate DebugMod to use new components * Remove other unused components * Move PostThreadFollowBtn to new home * Move PostThreadComposePrompt to new home * Remove gate * Keep naming in DebugMod * rm v1 prefs --------- Co-authored-by: Samuel Newman <mozzius@protonmail.com>
Diffstat (limited to 'src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx')
-rw-r--r-- | src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx | 139 |
1 files changed, 139 insertions, 0 deletions
diff --git a/src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx b/src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx new file mode 100644 index 000000000..d4cf120cf --- /dev/null +++ b/src/screens/PostThread/components/ThreadItemAnchorFollowButton.tsx @@ -0,0 +1,139 @@ +import React from 'react' +import {type AppBskyActorDefs} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useNavigation} from '@react-navigation/native' + +import {logger} from '#/logger' +import {useProfileShadow} from '#/state/cache/profile-shadow' +import { + useProfileFollowMutationQueue, + useProfileQuery, +} from '#/state/queries/profile' +import {useRequireAuth} from '#/state/session' +import * as Toast from '#/view/com/util/Toast' +import {atoms as a, useBreakpoints} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' +import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' + +export function ThreadItemAnchorFollowButton({did}: {did: string}) { + const {data: profile, isLoading} = useProfileQuery({did}) + + // We will never hit this - the profile will always be cached or loaded above + // but it keeps the typechecker happy + if (isLoading || !profile) return null + + return <PostThreadFollowBtnLoaded profile={profile} /> +} + +function PostThreadFollowBtnLoaded({ + profile: profileUnshadowed, +}: { + profile: AppBskyActorDefs.ProfileViewDetailed +}) { + const navigation = useNavigation() + const {_} = useLingui() + const {gtMobile} = useBreakpoints() + const profile = useProfileShadow(profileUnshadowed) + const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue( + profile, + 'PostThreadItem', + ) + const requireAuth = useRequireAuth() + + const isFollowing = !!profile.viewer?.following + const isFollowedBy = !!profile.viewer?.followedBy + const [wasFollowing, setWasFollowing] = React.useState<boolean>(isFollowing) + + // This prevents the button from disappearing as soon as we follow. + const showFollowBtn = React.useMemo( + () => !isFollowing || !wasFollowing, + [isFollowing, wasFollowing], + ) + + /** + * We want this button to stay visible even after following, so that the user can unfollow if they want. + * However, we need it to disappear after we push to a screen and then come back. We also need it to + * show up if we view the post while following, go to the profile and unfollow, then come back to the + * post. + * + * We want to update wasFollowing both on blur and on focus so that we hit all these cases. On native, + * we could do this only on focus because the transition animation gives us time to not notice the + * sudden rendering of the button. However, on web if we do this, there's an obvious flicker once the + * button renders. So, we update the state in both cases. + */ + React.useEffect(() => { + const updateWasFollowing = () => { + if (wasFollowing !== isFollowing) { + setWasFollowing(isFollowing) + } + } + + const unsubscribeFocus = navigation.addListener('focus', updateWasFollowing) + const unsubscribeBlur = navigation.addListener('blur', updateWasFollowing) + + return () => { + unsubscribeFocus() + unsubscribeBlur() + } + }, [isFollowing, wasFollowing, navigation]) + + const onPress = React.useCallback(() => { + if (!isFollowing) { + requireAuth(async () => { + try { + await queueFollow() + } catch (e: any) { + if (e?.name !== 'AbortError') { + logger.error('Failed to follow', {message: String(e)}) + Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark') + } + } + }) + } else { + requireAuth(async () => { + try { + await queueUnfollow() + } catch (e: any) { + if (e?.name !== 'AbortError') { + logger.error('Failed to unfollow', {message: String(e)}) + Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark') + } + } + }) + } + }, [isFollowing, requireAuth, queueFollow, _, queueUnfollow]) + + if (!showFollowBtn) return null + + return ( + <Button + testID="followBtn" + label={_(msg`Follow ${profile.handle}`)} + onPress={onPress} + size="small" + variant="solid" + color={isFollowing ? 'secondary' : 'secondary_inverted'} + style={[a.rounded_full]}> + {gtMobile && ( + <ButtonIcon + icon={isFollowing ? Check : Plus} + position="left" + size="sm" + /> + )} + <ButtonText> + {!isFollowing ? ( + isFollowedBy ? ( + <Trans>Follow back</Trans> + ) : ( + <Trans>Follow</Trans> + ) + ) : ( + <Trans>Following</Trans> + )} + </ButtonText> + </Button> + ) +} |