Add follow button to feed item avatar (#3560)
* add follow button to feed item avatar * remove confirmation * add confirmation (just system alert) * Shrink the avi follow indicator a smidge * gate the follow button * remove from your own posts * add to post thread item * hide the follow button locally to component * Use native dropdown * Add follow btn to notifications and search * UI tweaks * Hide on PWI * Add toast for confirmation * Check gate last * compiler * Rm unused * Use names --------- Co-authored-by: Paul Frazee <pfrazee@gmail.com> Co-authored-by: Eric Bailey <git@esb.lol> Co-authored-by: Dan Abramov <dan.abramov@gmail.com>zio/stable
parent
9879159438
commit
8569e2e389
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="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" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 508 B |
|
@ -64,7 +64,7 @@ type NonTextElements =
|
|||
|
||||
export type ButtonProps = Pick<
|
||||
PressableProps,
|
||||
'disabled' | 'onPress' | 'testID' | 'onLongPress'
|
||||
'disabled' | 'onPress' | 'testID' | 'onLongPress' | 'hitSlop'
|
||||
> &
|
||||
AccessibilityProps &
|
||||
VariantProps & {
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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',
|
||||
})
|
||||
|
|
|
@ -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',
|
||||
})
|
|
@ -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',
|
||||
})
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export {Fragment as AviFollowButton} from 'react'
|
|
@ -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,
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Reference in New Issue