diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/components/Button.tsx | 2 | ||||
-rw-r--r-- | src/components/dms/ConvoMenu.tsx | 8 | ||||
-rw-r--r-- | src/components/icons/Person.tsx | 12 | ||||
-rw-r--r-- | src/components/icons/PersonCheck.tsx | 5 | ||||
-rw-r--r-- | src/components/icons/PersonX.tsx | 5 | ||||
-rw-r--r-- | src/lib/statsig/events.ts | 2 | ||||
-rw-r--r-- | src/lib/statsig/gates.ts | 1 | ||||
-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 |
13 files changed, 176 insertions, 36 deletions
diff --git a/src/components/Button.tsx b/src/components/Button.tsx index e22faa060..982f42213 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -64,7 +64,7 @@ type NonTextElements = export type ButtonProps = Pick< PressableProps, - 'disabled' | 'onPress' | 'testID' | 'onLongPress' + 'disabled' | 'onPress' | 'testID' | 'onLongPress' | 'hitSlop' > & AccessibilityProps & VariantProps & { diff --git a/src/components/dms/ConvoMenu.tsx b/src/components/dms/ConvoMenu.tsx index 79ca34f17..3f680120b 100644 --- a/src/components/dms/ConvoMenu.tsx +++ b/src/components/dms/ConvoMenu.tsx @@ -26,9 +26,11 @@ import {ArrowBoxLeft_Stroke2_Corner0_Rounded as ArrowBoxLeft} from '#/components import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid' import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag' import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute' -import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person' -import {PersonCheck_Stroke2_Corner0_Rounded as PersonCheck} from '#/components/icons/PersonCheck' -import {PersonX_Stroke2_Corner0_Rounded as PersonX} from '#/components/icons/PersonX' +import { + Person_Stroke2_Corner0_Rounded as Person, + PersonCheck_Stroke2_Corner0_Rounded as PersonCheck, + PersonX_Stroke2_Corner0_Rounded as PersonX, +} from '#/components/icons/Person' import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker' import * as Menu from '#/components/Menu' import * as Prompt from '#/components/Prompt' diff --git a/src/components/icons/Person.tsx b/src/components/icons/Person.tsx index 6d09148c9..7fceada53 100644 --- a/src/components/icons/Person.tsx +++ b/src/components/icons/Person.tsx @@ -3,3 +3,15 @@ import {createSinglePathSVG} from './TEMPLATE' export const Person_Stroke2_Corner0_Rounded = createSinglePathSVG({ path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.678 19h12.644c-.71-2.909-3.092-5-6.322-5s-5.613 2.091-6.322 5Zm-2.174.906C3.917 15.521 7.242 12 12 12c4.758 0 8.083 3.521 8.496 7.906A1 1 0 0 1 19.5 21h-15a1 1 0 0 1-.996-1.094Z', }) + +export const PersonCheck_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.679 19c.709-2.902 3.079-5 6.321-5a6.69 6.69 0 0 1 2.612.51 1 1 0 0 0 .776-1.844A8.687 8.687 0 0 0 12 12c-4.3 0-7.447 2.884-8.304 6.696-.29 1.29.767 2.304 1.902 2.304H11a1 1 0 1 0 0-2H5.679Zm14.835-4.857a1 1 0 0 1 .344 1.371l-3 5a1 1 0 0 1-1.458.286l-2-1.5a1 1 0 0 1 1.2-1.6l1.113.835 2.43-4.05a1 1 0 0 1 1.372-.342Z', +}) + +export const PersonX_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.679 19c.709-2.902 3.079-5 6.321-5 .302 0 .595.018.878.053a1 1 0 0 0 .243-1.985A9.235 9.235 0 0 0 12 12c-4.3 0-7.447 2.884-8.304 6.696-.29 1.29.767 2.304 1.902 2.304H12a1 1 0 1 0 0-2H5.679Zm9.614-3.707a1 1 0 0 1 1.414 0L18 16.586l1.293-1.293a1 1 0 0 1 1.414 1.414L19.414 18l1.293 1.293a1 1 0 0 1-1.414 1.414L18 19.414l-1.293 1.293a1 1 0 0 1-1.414-1.414L16.586 18l-1.293-1.293a1 1 0 0 1 0-1.414Z', +}) + +export const PersonPlus_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.678 19c.71-2.909 3.092-5 6.322-5 .621 0 1.206.077 1.748.218a1 1 0 1 0 .504-1.936A8.931 8.931 0 0 0 12 12c-4.758 0-8.083 3.521-8.496 7.906A1 1 0 0 0 4.5 21H11a1 1 0 1 0 0-2H5.678ZM18 14a1 1 0 0 1 1 1v2h2a1 1 0 1 1 0 2h-2v2a1 1 0 1 1-2 0v-2h-2a1 1 0 1 1 0-2h2v-2a1 1 0 0 1 1-1Z', +}) diff --git a/src/components/icons/PersonCheck.tsx b/src/components/icons/PersonCheck.tsx deleted file mode 100644 index 097271d89..000000000 --- a/src/components/icons/PersonCheck.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import {createSinglePathSVG} from './TEMPLATE' - -export const PersonCheck_Stroke2_Corner0_Rounded = createSinglePathSVG({ - path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.679 19c.709-2.902 3.079-5 6.321-5a6.69 6.69 0 0 1 2.612.51 1 1 0 0 0 .776-1.844A8.687 8.687 0 0 0 12 12c-4.3 0-7.447 2.884-8.304 6.696-.29 1.29.767 2.304 1.902 2.304H11a1 1 0 1 0 0-2H5.679Zm14.835-4.857a1 1 0 0 1 .344 1.371l-3 5a1 1 0 0 1-1.458.286l-2-1.5a1 1 0 0 1 1.2-1.6l1.113.835 2.43-4.05a1 1 0 0 1 1.372-.342Z', -}) diff --git a/src/components/icons/PersonX.tsx b/src/components/icons/PersonX.tsx deleted file mode 100644 index a015e1376..000000000 --- a/src/components/icons/PersonX.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import {createSinglePathSVG} from './TEMPLATE' - -export const PersonX_Stroke2_Corner0_Rounded = createSinglePathSVG({ - path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.679 19c.709-2.902 3.079-5 6.321-5 .302 0 .595.018.878.053a1 1 0 0 0 .243-1.985A9.235 9.235 0 0 0 12 12c-4.3 0-7.447 2.884-8.304 6.696-.29 1.29.767 2.304 1.902 2.304H12a1 1 0 1 0 0-2H5.679Zm9.614-3.707a1 1 0 0 1 1.414 0L18 16.586l1.293-1.293a1 1 0 0 1 1.414 1.414L19.414 18l1.293 1.293a1 1 0 0 1-1.414 1.414L18 19.414l-1.293 1.293a1 1 0 0 1-1.414-1.414L16.586 18l-1.293-1.293a1 1 0 0 1 0-1.414Z', -}) diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts index 42f4737f8..00444c18c 100644 --- a/src/lib/statsig/events.ts +++ b/src/lib/statsig/events.ts @@ -115,6 +115,7 @@ export type LogEvents = { | 'ProfileHeaderSuggestedFollows' | 'ProfileMenu' | 'ProfileHoverCard' + | 'AvatarButton' } 'profile:unfollow': { logContext: @@ -126,6 +127,7 @@ export type LogEvents = { | 'ProfileMenu' | 'ProfileHoverCard' | 'Chat' + | 'AvatarButton' } 'chat:create': { logContext: 'ProfileHeader' | 'NewChatDialog' diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts index 2721871f3..4481935f7 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -1,4 +1,5 @@ export type Gate = // Keep this alphabetic please. | 'request_notifications_permission_after_onboarding_v2' + | 'show_avi_follow_button' | 'show_follow_back_label_v2' 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' |