Fix chat request buttons not moving with swipe gesture (#9155)

* portal in buttons so they move with swipe

* remove outline style buttons
This commit is contained in:
Samuel Newman
2025-10-10 19:30:29 +03:00
committed by GitHub
parent b7b47d30bf
commit 54b8eacba1
4 changed files with 231 additions and 217 deletions
+194 -176
View File
@@ -42,11 +42,14 @@ import {Trash_Stroke2_Corner0_Rounded} from '#/components/icons/Trash'
import {Link} from '#/components/Link' import {Link} from '#/components/Link'
import {useMenuControl} from '#/components/Menu' import {useMenuControl} from '#/components/Menu'
import {PostAlerts} from '#/components/moderation/PostAlerts' import {PostAlerts} from '#/components/moderation/PostAlerts'
import {createPortalGroup} from '#/components/Portal'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification' import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck' import {VerificationCheck} from '#/components/verification/VerificationCheck'
import type * as bsky from '#/types/bsky' import type * as bsky from '#/types/bsky'
export const ChatListItemPortal = createPortalGroup()
export let ChatListItem = ({ export let ChatListItem = ({
convo, convo,
showMenu = true, showMenu = true,
@@ -331,200 +334,215 @@ function ChatListItemReady({
const hasUnread = convo.unreadCount > 0 && !isDeletedAccount const hasUnread = convo.unreadCount > 0 && !isDeletedAccount
return ( return (
<GestureActionView actions={actions}> <ChatListItemPortal.Provider>
<View <GestureActionView actions={actions}>
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
// @ts-expect-error web only
onFocus={onFocus}
onBlur={onMouseLeave}
style={[a.relative, t.atoms.bg]}>
<View <View
style={[ onMouseEnter={onMouseEnter}
a.z_10, onMouseLeave={onMouseLeave}
a.absolute, // @ts-expect-error web only
{top: tokens.space.md, left: tokens.space.lg}, onFocus={onFocus}
]}> onBlur={onMouseLeave}
<PreviewableUserAvatar style={[a.relative, t.atoms.bg]}>
profile={profile} <View
size={52} style={[
moderation={moderation.ui('avatar')} a.z_10,
/> a.absolute,
</View> {top: tokens.space.md, left: tokens.space.lg},
]}>
<Link <PreviewableUserAvatar
to={`/messages/${convo.id}`} profile={profile}
label={displayName} size={52}
accessibilityHint={ moderation={moderation.ui('avatar')}
!isDeletedAccount />
? _(msg`Go to conversation with ${profile.handle}`) </View>
: _(
msg`This conversation is with a deleted or a deactivated account. Press for options`,
)
}
accessibilityActions={
isNative
? [
{name: 'magicTap', label: _(msg`Open conversation options`)},
{name: 'longpress', label: _(msg`Open conversation options`)},
]
: undefined
}
onPress={onPress}
onLongPress={isNative ? onLongPress : undefined}
onAccessibilityAction={onLongPress}>
{({hovered, pressed, focused}) => (
<View
style={[
a.flex_row,
isDeletedAccount ? a.align_center : a.align_start,
a.flex_1,
a.px_lg,
a.py_md,
a.gap_md,
(hovered || pressed || focused) && t.atoms.bg_contrast_25,
]}>
{/* Avatar goes here */}
<View style={{width: 52, height: 52}} />
<Link
to={`/messages/${convo.id}`}
label={displayName}
accessibilityHint={
!isDeletedAccount
? _(msg`Go to conversation with ${profile.handle}`)
: _(
msg`This conversation is with a deleted or a deactivated account. Press for options`,
)
}
accessibilityActions={
isNative
? [
{
name: 'magicTap',
label: _(msg`Open conversation options`),
},
{
name: 'longpress',
label: _(msg`Open conversation options`),
},
]
: undefined
}
onPress={onPress}
onLongPress={isNative ? onLongPress : undefined}
onAccessibilityAction={onLongPress}>
{({hovered, pressed, focused}) => (
<View <View
style={[a.flex_1, a.justify_center, web({paddingRight: 45})]}> style={[
<View style={[a.w_full, a.flex_row, a.align_end, a.pb_2xs]}> a.flex_row,
<View style={[a.flex_shrink]}> isDeletedAccount ? a.align_center : a.align_start,
a.flex_1,
a.px_lg,
a.py_md,
a.gap_md,
(hovered || pressed || focused) && t.atoms.bg_contrast_25,
]}>
{/* Avatar goes here */}
<View style={{width: 52, height: 52}} />
<View
style={[a.flex_1, a.justify_center, web({paddingRight: 45})]}>
<View style={[a.w_full, a.flex_row, a.align_end, a.pb_2xs]}>
<View style={[a.flex_shrink]}>
<Text
emoji
numberOfLines={1}
style={[
a.text_md,
t.atoms.text,
a.font_semi_bold,
{lineHeight: 21},
isDimStyle && t.atoms.text_contrast_medium,
]}>
{displayName}
</Text>
</View>
{verification.showBadge && (
<View style={[a.pl_xs, a.self_center]}>
<VerificationCheck
width={14}
verifier={verification.role === 'verifier'}
/>
</View>
)}
{lastMessageSentAt && (
<View style={[a.pl_xs]}>
<TimeElapsed timestamp={lastMessageSentAt}>
{({timeElapsed}) => (
<Text
style={[
a.text_sm,
{lineHeight: 21},
t.atoms.text_contrast_medium,
web({whiteSpace: 'preserve nowrap'}),
]}>
&middot; {timeElapsed}
</Text>
)}
</TimeElapsed>
</View>
)}
{(convo.muted || moderation.blocked) && (
<Text
style={[
a.text_sm,
{lineHeight: 21},
t.atoms.text_contrast_medium,
web({whiteSpace: 'preserve nowrap'}),
]}>
{' '}
&middot;{' '}
<BellStroke
size="xs"
style={[t.atoms.text_contrast_medium]}
/>
</Text>
)}
</View>
{!isDeletedAccount && (
<Text <Text
emoji
numberOfLines={1} numberOfLines={1}
style={[
a.text_md,
t.atoms.text,
a.font_semi_bold,
{lineHeight: 21},
isDimStyle && t.atoms.text_contrast_medium,
]}>
{displayName}
</Text>
</View>
{verification.showBadge && (
<View style={[a.pl_xs, a.self_center]}>
<VerificationCheck
width={14}
verifier={verification.role === 'verifier'}
/>
</View>
)}
{lastMessageSentAt && (
<View style={[a.pl_xs]}>
<TimeElapsed timestamp={lastMessageSentAt}>
{({timeElapsed}) => (
<Text
style={[
a.text_sm,
{lineHeight: 21},
t.atoms.text_contrast_medium,
web({whiteSpace: 'preserve nowrap'}),
]}>
&middot; {timeElapsed}
</Text>
)}
</TimeElapsed>
</View>
)}
{(convo.muted || moderation.blocked) && (
<Text
style={[ style={[
a.text_sm, a.text_sm,
{lineHeight: 21},
t.atoms.text_contrast_medium, t.atoms.text_contrast_medium,
web({whiteSpace: 'preserve nowrap'}), a.pb_xs,
]}> ]}>
{' '} @{profile.handle}
&middot;{' '}
<BellStroke
size="xs"
style={[t.atoms.text_contrast_medium]}
/>
</Text> </Text>
)} )}
<Text
emoji
numberOfLines={2}
style={[
a.text_sm,
a.leading_snug,
hasUnread ? a.font_semi_bold : t.atoms.text_contrast_high,
isDimStyle && t.atoms.text_contrast_medium,
]}>
{lastMessage}
</Text>
<PostAlerts
modui={moderation.ui('contentList')}
size="lg"
style={[a.pt_xs]}
/>
{children}
</View> </View>
{!isDeletedAccount && ( {hasUnread && (
<Text <View
numberOfLines={1} style={[
style={[a.text_sm, t.atoms.text_contrast_medium, a.pb_xs]}> a.absolute,
@{profile.handle} a.rounded_full,
</Text> {
backgroundColor: isDimStyle
? t.palette.contrast_200
: t.palette.primary_500,
height: 7,
width: 7,
top: 15,
right: 12,
},
]}
/>
)} )}
<Text
emoji
numberOfLines={2}
style={[
a.text_sm,
a.leading_snug,
hasUnread ? a.font_semi_bold : t.atoms.text_contrast_high,
isDimStyle && t.atoms.text_contrast_medium,
]}>
{lastMessage}
</Text>
<PostAlerts
modui={moderation.ui('contentList')}
size="lg"
style={[a.pt_xs]}
/>
{children}
</View> </View>
)}
</Link>
{hasUnread && ( <ChatListItemPortal.Outlet />
<View
style={[ {showMenu && (
a.absolute, <ConvoMenu
a.rounded_full, convo={convo}
{ profile={profile}
backgroundColor: isDimStyle control={menuControl}
? t.palette.contrast_200 currentScreen="list"
: t.palette.primary_500, showMarkAsRead={convo.unreadCount > 0}
height: 7, hideTrigger={isNative}
width: 7, blockInfo={blockInfo}
top: 15, style={[
right: 12, a.absolute,
}, a.h_full,
]} a.self_end,
/> a.justify_center,
)} {
</View> right: tokens.space.lg,
opacity:
!gtMobile || showActions || menuControl.isOpen ? 1 : 0,
},
]}
latestReportableMessage={latestReportableMessage}
/>
)} )}
</Link> <LeaveConvoPrompt
control={leaveConvoControl}
{showMenu && ( convoId={convo.id}
<ConvoMenu
convo={convo}
profile={profile}
control={menuControl}
currentScreen="list" currentScreen="list"
showMarkAsRead={convo.unreadCount > 0}
hideTrigger={isNative}
blockInfo={blockInfo}
style={[
a.absolute,
a.h_full,
a.self_end,
a.justify_center,
{
right: tokens.space.lg,
opacity: !gtMobile || showActions || menuControl.isOpen ? 1 : 0,
},
]}
latestReportableMessage={latestReportableMessage}
/> />
)} </View>
<LeaveConvoPrompt </GestureActionView>
control={leaveConvoControl} </ChatListItemPortal.Provider>
convoId={convo.id}
currentScreen="list"
/>
</View>
</GestureActionView>
) )
} }
@@ -46,7 +46,7 @@ export function ChatStatusInfo({convoState}: {convoState: ActiveConvoStates}) {
label={_(msg`Block or report`)} label={_(msg`Block or report`)}
convo={convoState.convo} convo={convoState.convo}
profile={otherUser} profile={otherUser}
color="negative" color="negative_subtle"
size="small" size="small"
currentScreen="conversation" currentScreen="conversation"
/> />
@@ -70,8 +70,7 @@ export function ChatStatusInfo({convoState}: {convoState: ActiveConvoStates}) {
<AcceptChatButton <AcceptChatButton
onAcceptConvo={onAcceptChat} onAcceptConvo={onAcceptChat}
convo={convoState.convo} convo={convoState.convo}
color="primary" color="primary_subtle"
variant="outline"
size="small" size="small"
currentScreen="conversation" currentScreen="conversation"
/> />
@@ -36,7 +36,6 @@ export function RejectMenu({
convo, convo,
profile, profile,
size = 'tiny', size = 'tiny',
variant = 'outline',
color = 'secondary', color = 'secondary',
label, label,
showDeleteConvo, showDeleteConvo,
@@ -117,7 +116,6 @@ export function RejectMenu({
label={triggerProps.accessibilityLabel} label={triggerProps.accessibilityLabel}
style={[a.flex_1]} style={[a.flex_1]}
color={color} color={color}
variant={variant}
size={size}> size={size}>
<ButtonText> <ButtonText>
{label || ( {label || (
@@ -129,7 +127,7 @@ export function RejectMenu({
</Button> </Button>
)} )}
</Menu.Trigger> </Menu.Trigger>
<Menu.Outer> <Menu.Outer showCancel>
<Menu.Group> <Menu.Group>
{showDeleteConvo && ( {showDeleteConvo && (
<Menu.Item <Menu.Item
@@ -181,7 +179,6 @@ export function RejectMenu({
export function AcceptChatButton({ export function AcceptChatButton({
convo, convo,
size = 'tiny', size = 'tiny',
variant = 'solid',
color = 'secondary_inverted', color = 'secondary_inverted',
label, label,
currentScreen, currentScreen,
@@ -248,7 +245,6 @@ export function AcceptChatButton({
{...props} {...props}
label={label || _(msg`Accept chat request`)} label={label || _(msg`Accept chat request`)}
size={size} size={size}
variant={variant}
color={color} color={color}
style={a.flex_1} style={a.flex_1}
onPress={onPressAccept}> onPress={onPressAccept}>
@@ -266,7 +262,6 @@ export function AcceptChatButton({
export function DeleteChatButton({ export function DeleteChatButton({
convo, convo,
size = 'tiny', size = 'tiny',
variant = 'outline',
color = 'secondary', color = 'secondary',
label, label,
currentScreen, currentScreen,
@@ -315,7 +310,6 @@ export function DeleteChatButton({
<Button <Button
label={label || _(msg`Delete chat`)} label={label || _(msg`Delete chat`)}
size={size} size={size}
variant={variant}
color={color} color={color}
style={a.flex_1} style={a.flex_1}
onPress={onPressDelete} onPress={onPressDelete}
@@ -7,7 +7,7 @@ import {useSession} from '#/state/session'
import {atoms as a, tokens} from '#/alf' import {atoms as a, tokens} from '#/alf'
import {KnownFollowers} from '#/components/KnownFollowers' import {KnownFollowers} from '#/components/KnownFollowers'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {ChatListItem} from './ChatListItem' import {ChatListItem, ChatListItemPortal} from './ChatListItem'
import {AcceptChatButton, DeleteChatButton, RejectMenu} from './RequestButtons' import {AcceptChatButton, DeleteChatButton, RejectMenu} from './RequestButtons'
export function RequestListItem({convo}: {convo: ChatBskyConvoDefs.ConvoView}) { export function RequestListItem({convo}: {convo: ChatBskyConvoDefs.ConvoView}) {
@@ -42,37 +42,40 @@ export function RequestListItem({convo}: {convo: ChatBskyConvoDefs.ConvoView}) {
<Trans comment="Accept a chat request">Accept Request</Trans> <Trans comment="Accept a chat request">Accept Request</Trans>
</Text> </Text>
</View> </View>
{/* then, this gets absolutely positioned on top of the spacer */}
<ChatListItemPortal.Portal>
<View
style={[
a.absolute,
a.pr_md,
a.w_full,
a.flex_row,
a.align_center,
a.gap_sm,
{
bottom: tokens.space.md,
paddingLeft: tokens.space.lg + 52 + tokens.space.md,
},
]}>
{!isDeletedAccount ? (
<>
<AcceptChatButton convo={convo} currentScreen="list" />
<RejectMenu
convo={convo}
profile={otherUser}
showDeleteConvo
currentScreen="list"
/>
</>
) : (
<>
<DeleteChatButton convo={convo} currentScreen="list" />
<View style={a.flex_1} />
</>
)}
</View>
</ChatListItemPortal.Portal>
</ChatListItem> </ChatListItem>
<View
style={[
a.absolute,
a.pr_md,
a.w_full,
a.flex_row,
a.align_center,
a.gap_sm,
{
bottom: tokens.space.md,
paddingLeft: tokens.space.lg + 52 + tokens.space.md,
},
]}>
{!isDeletedAccount ? (
<>
<AcceptChatButton convo={convo} currentScreen="list" />
<RejectMenu
convo={convo}
profile={otherUser}
showDeleteConvo
currentScreen="list"
/>
</>
) : (
<>
<DeleteChatButton convo={convo} currentScreen="list" />
<View style={a.flex_1} />
</>
)}
</View>
</View> </View>
) )
} }