diff options
Diffstat (limited to 'src/view')
-rw-r--r-- | src/view/com/post-thread/PostThreadItem.tsx | 17 | ||||
-rw-r--r-- | src/view/com/post/Post.tsx | 17 | ||||
-rw-r--r-- | src/view/com/posts/AviFollowButton.tsx | 115 | ||||
-rw-r--r-- | src/view/com/posts/AviFollowButton.web.tsx | 1 | ||||
-rw-r--r-- | src/view/com/posts/FeedItem.tsx | 21 | ||||
-rw-r--r-- | src/view/com/profile/ProfileMenu.tsx | 6 |
6 files changed, 155 insertions, 22 deletions
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 9d2985f15..981b4e72f 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -40,6 +40,7 @@ import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe' import {PostAlerts} from '../../../components/moderation/PostAlerts' import {PostHider} from '../../../components/moderation/PostHider' import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers' +import {AviFollowButton} from '../posts/AviFollowButton' import {WhoCanReply} from '../threadgate/WhoCanReply' import {ErrorMessage} from '../util/error/ErrorMessage' import {Link, TextLink} from '../util/Link' @@ -470,12 +471,16 @@ let PostThreadItemLoaded = ({ {/* If we are in threaded mode, the avatar is rendered in PostMeta */} {!isThreadedChild && ( <View style={styles.layoutAvi}> - <PreviewableUserAvatar - size={38} - profile={post.author} - moderation={moderation.ui('avatar')} - type={post.author.associated?.labeler ? 'labeler' : 'user'} - /> + <AviFollowButton author={post.author} moderation={moderation}> + <PreviewableUserAvatar + size={38} + profile={post.author} + moderation={moderation.ui('avatar')} + type={ + post.author.associated?.labeler ? 'labeler' : 'user' + } + /> + </AviFollowButton> {showChildReplyLine && ( <View diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index 1a7185cd9..f666e2968 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -22,6 +22,7 @@ import {makeProfileLink} from 'lib/routes/links' import {countLines} from 'lib/strings/helpers' import {colors, s} from 'lib/styles' import {precacheProfile} from 'state/queries/profile' +import {AviFollowButton} from '#/view/com/posts/AviFollowButton' import {atoms as a} from '#/alf' import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {RichText} from '#/components/RichText' @@ -146,12 +147,14 @@ function PostInner({ {showReplyLine && <View style={styles.replyLine} />} <View style={styles.layout}> <View style={styles.layoutAvi}> - <PreviewableUserAvatar - size={52} - profile={post.author} - moderation={moderation.ui('avatar')} - type={post.author.associated?.labeler ? 'labeler' : 'user'} - /> + <AviFollowButton author={post.author} moderation={moderation}> + <PreviewableUserAvatar + size={52} + profile={post.author} + moderation={moderation.ui('avatar')} + type={post.author.associated?.labeler ? 'labeler' : 'user'} + /> + </AviFollowButton> </View> <View style={styles.layoutContent}> <PostMeta @@ -245,9 +248,9 @@ const styles = StyleSheet.create({ }, layout: { flexDirection: 'row', + gap: 10, }, layoutAvi: { - width: 70, paddingLeft: 8, }, layoutContent: { diff --git a/src/view/com/posts/AviFollowButton.tsx b/src/view/com/posts/AviFollowButton.tsx new file mode 100644 index 000000000..9358967e1 --- /dev/null +++ b/src/view/com/posts/AviFollowButton.tsx @@ -0,0 +1,115 @@ +import React, {useState} from 'react' +import {View} from 'react-native' +import {AppBskyActorDefs, ModerationDecision} from '@atproto/api' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useNavigation} from '@react-navigation/native' + +import {createHitslop} from '#/lib/constants' +import {NavigationProp} from '#/lib/routes/types' +import {useGate} from '#/lib/statsig/statsig' +import {sanitizeDisplayName} from '#/lib/strings/display-names' +import {useProfileShadow} from '#/state/cache/profile-shadow' +import {useSession} from '#/state/session' +import { + DropdownItem, + NativeDropdown, +} from '#/view/com/util/forms/NativeDropdown' +import * as Toast from '#/view/com/util/Toast' +import {atoms as a, useTheme} from '#/alf' +import {Button} from '#/components/Button' +import {useFollowMethods} from '#/components/hooks/useFollowMethods' +import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' + +export function AviFollowButton({ + author, + moderation, + children, +}: { + author: AppBskyActorDefs.ProfileViewBasic + moderation: ModerationDecision + children: React.ReactNode +}) { + const {_} = useLingui() + const t = useTheme() + const profile = useProfileShadow(author) + const {follow} = useFollowMethods({ + profile: profile, + logContext: 'AvatarButton', + }) + const gate = useGate() + const {currentAccount, hasSession} = useSession() + const [followed, setFollowed] = useState<string | null>(null) + const navigation = useNavigation<NavigationProp>() + + const name = sanitizeDisplayName( + profile.displayName || profile.handle, + moderation.ui('displayName'), + ) + const isFollowing = + profile.viewer?.following || + profile.did === followed || + profile.did === currentAccount?.did + + function onPress() { + follow() + setFollowed(profile.did) + Toast.show(_(msg`Following ${name}`)) + } + + const items: DropdownItem[] = [ + { + label: _(msg`View profile`), + onPress: () => { + navigation.navigate('Profile', {name: profile.did}) + }, + icon: { + ios: { + name: 'arrow.up.right.square', + }, + android: '', + web: ['far', 'arrow-up-right-from-square'], + }, + }, + { + label: _(msg`Follow ${name}`), + onPress: onPress, + icon: { + ios: { + name: 'person.badge.plus', + }, + android: '', + web: ['far', 'user-plus'], + }, + }, + ] + + return hasSession && gate('show_avi_follow_button') ? ( + <View style={a.relative}> + {children} + + {!isFollowing && ( + <Button + label={_(msg`Open ${name} profile shortcut menu`)} + hitSlop={createHitslop(3)} + style={[ + a.rounded_full, + t.atoms.bg_contrast_975, + a.absolute, + { + bottom: -2, + right: -2, + borderWidth: 1, + borderColor: t.atoms.bg.backgroundColor, + }, + ]}> + <NativeDropdown items={items}> + <Plus size="sm" fill={t.atoms.bg.backgroundColor} /> + </NativeDropdown> + </Button> + )} + </View> + ) : ( + children + ) +} diff --git a/src/view/com/posts/AviFollowButton.web.tsx b/src/view/com/posts/AviFollowButton.web.tsx new file mode 100644 index 000000000..6ad3c9f1f --- /dev/null +++ b/src/view/com/posts/AviFollowButton.web.tsx @@ -0,0 +1 @@ +export {Fragment as AviFollowButton} from 'react' diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index 8077c2968..b10ffe19f 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -41,6 +41,7 @@ import {PostEmbeds} from '../util/post-embeds' import {PostMeta} from '../util/PostMeta' import {Text} from '../util/text/Text' import {PreviewableUserAvatar} from '../util/UserAvatar' +import {AviFollowButton} from './AviFollowButton' interface FeedItemProps { record: AppBskyFeedPost.Record @@ -284,13 +285,15 @@ let FeedItemInner = ({ <View style={styles.layout}> <View style={styles.layoutAvi}> - <PreviewableUserAvatar - size={52} - profile={post.author} - moderation={moderation.ui('avatar')} - type={post.author.associated?.labeler ? 'labeler' : 'user'} - onBeforePress={onOpenAuthor} - /> + <AviFollowButton author={post.author} moderation={moderation}> + <PreviewableUserAvatar + size={52} + profile={post.author} + moderation={moderation.ui('avatar')} + type={post.author.associated?.labeler ? 'labeler' : 'user'} + onBeforePress={onOpenAuthor} + /> + </AviFollowButton> {isThreadParent && ( <View style={[ @@ -470,9 +473,13 @@ const styles = StyleSheet.create({ }, layoutAvi: { paddingLeft: 8, + position: 'relative', + zIndex: 999, }, layoutContent: { + position: 'relative', flex: 1, + zIndex: 0, }, alert: { marginTop: 6, diff --git a/src/view/com/profile/ProfileMenu.tsx b/src/view/com/profile/ProfileMenu.tsx index e3357ec1e..5d39e5f0f 100644 --- a/src/view/com/profile/ProfileMenu.tsx +++ b/src/view/com/profile/ProfileMenu.tsx @@ -30,8 +30,10 @@ import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag' import {ListSparkle_Stroke2_Corner0_Rounded as List} from '#/components/icons/ListSparkle' import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute' import {PeopleRemove2_Stroke2_Corner0_Rounded as UserMinus} from '#/components/icons/PeopleRemove2' -import {PersonCheck_Stroke2_Corner0_Rounded as PersonCheck} from '#/components/icons/PersonCheck' -import {PersonX_Stroke2_Corner0_Rounded as PersonX} from '#/components/icons/PersonX' +import { + PersonCheck_Stroke2_Corner0_Rounded as PersonCheck, + PersonX_Stroke2_Corner0_Rounded as PersonX, +} from '#/components/icons/Person' import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker' import * as Menu from '#/components/Menu' |