swipe action to leave chat

This commit is contained in:
Samuel Newman
2024-10-02 23:37:36 +03:00
parent 00486e9499
commit cef17f1a0a
3 changed files with 228 additions and 170 deletions
+182 -170
View File
@@ -1,4 +1,4 @@
import React, {useCallback, useState} from 'react' import React, {useCallback, useMemo, useState} from 'react'
import {GestureResponderEvent, View} from 'react-native' import {GestureResponderEvent, View} from 'react-native'
import { import {
AppBskyActorDefs, AppBskyActorDefs,
@@ -27,12 +27,15 @@ import {TimeElapsed} from '#/view/com/util/TimeElapsed'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
import * as tokens from '#/alf/tokens' import * as tokens from '#/alf/tokens'
import {useDialogControl} from '#/components/Dialog'
import {ConvoMenu} from '#/components/dms/ConvoMenu' import {ConvoMenu} from '#/components/dms/ConvoMenu'
import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt'
import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2' import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2'
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 {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {SwipeableRow} from './SwipeableRow'
export let ChatListItem = ({ export let ChatListItem = ({
convo, convo,
@@ -74,6 +77,7 @@ function ChatListItemReady({
const {_} = useLingui() const {_} = useLingui()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const menuControl = useMenuControl() const menuControl = useMenuControl()
const leaveConvoControl = useDialogControl()
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const profile = useProfileShadow(profileUnshadowed) const profile = useProfileShadow(profileUnshadowed)
const moderation = React.useMemo( const moderation = React.useMemo(
@@ -82,7 +86,7 @@ function ChatListItemReady({
) )
const playHaptic = useHaptics() const playHaptic = useHaptics()
const blockInfo = React.useMemo(() => { const blockInfo = useMemo(() => {
const modui = moderation.ui('profileView') const modui = moderation.ui('profileView')
const blocks = modui.alerts.filter(alert => alert.type === 'blocking') const blocks = modui.alerts.filter(alert => alert.type === 'blocking')
const listBlocks = blocks.filter(alert => alert.source.type === 'list') const listBlocks = blocks.filter(alert => alert.source.type === 'list')
@@ -103,7 +107,7 @@ function ChatListItemReady({
const isDimStyle = convo.muted || moderation.blocked || isDeletedAccount const isDimStyle = convo.muted || moderation.blocked || isDeletedAccount
const {lastMessage, lastMessageSentAt} = React.useMemo(() => { const {lastMessage, lastMessageSentAt} = useMemo(() => {
let lastMessage = _(msg`No messages yet`) let lastMessage = _(msg`No messages yet`)
let lastMessageSentAt: string | null = null let lastMessageSentAt: string | null = null
@@ -197,182 +201,190 @@ function ChatListItemReady({
}, [playHaptic, menuControl]) }, [playHaptic, menuControl])
return ( return (
<View <SwipeableRow onDelete={leaveConvoControl.open}>
// @ts-expect-error web only
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onFocus={onFocus}
onBlur={onMouseLeave}
style={[a.relative]}>
<View <View
style={[ // @ts-expect-error web only
a.z_10, onMouseEnter={onMouseEnter}
a.absolute, onMouseLeave={onMouseLeave}
{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},
]}>
<PreviewableUserAvatar
profile={profile}
size={52}
moderation={moderation.ui('avatar')}
/>
</View>
<Link <Link
to={`/messages/${convo.id}`} to={`/messages/${convo.id}`}
label={displayName} label={displayName}
accessibilityHint={ accessibilityHint={
!isDeletedAccount !isDeletedAccount
? _(msg`Go to conversation with ${profile.handle}`) ? _(msg`Go to conversation with ${profile.handle}`)
: _( : _(
msg`This conversation is with a deleted or a deactivated account. Press for options.`, msg`This conversation is with a deleted or a deactivated account. Press for options.`,
) )
} }
accessibilityActions={ accessibilityActions={
isNative isNative
? [ ? [
{name: 'magicTap', label: _(msg`Open conversation options`)}, {name: 'magicTap', label: _(msg`Open conversation options`)},
{name: 'longpress', label: _(msg`Open conversation options`)}, {name: 'longpress', label: _(msg`Open conversation options`)},
] ]
: undefined : undefined
} }
onPress={onPress} onPress={onPress}
onLongPress={isNative ? onLongPress : undefined} onLongPress={isNative ? onLongPress : undefined}
onAccessibilityAction={onLongPress}> onAccessibilityAction={onLongPress}>
{({hovered, pressed, focused}) => ( {({hovered, pressed, focused}) => (
<View <View
style={[ style={[
a.flex_row, a.flex_row,
isDeletedAccount ? a.align_center : a.align_start, isDeletedAccount ? a.align_center : a.align_start,
a.flex_1, a.flex_1,
a.px_lg, a.px_lg,
a.py_md, a.py_md,
a.gap_md, a.gap_md,
(hovered || pressed || focused) && t.atoms.bg_contrast_25, (hovered || pressed || focused) && t.atoms.bg_contrast_25,
t.atoms.border_contrast_low, t.atoms.border_contrast_low,
]}> ]}>
{/* Avatar goes here */} {/* Avatar goes here */}
<View style={{width: 52, height: 52}} /> <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]}>
<Text
numberOfLines={1}
style={[{maxWidth: '85%'}, web([a.leading_normal])]}>
<Text
emoji
style={[
a.text_md,
t.atoms.text,
a.font_bold,
{lineHeight: 21},
isDimStyle && t.atoms.text_contrast_medium,
]}>
{displayName}
</Text>
</Text>
{lastMessageSentAt && (
<TimeElapsed timestamp={lastMessageSentAt}>
{({timeElapsed}) => (
<Text
style={[
a.text_sm,
{lineHeight: 21},
t.atoms.text_contrast_medium,
web({whiteSpace: 'preserve nowrap'}),
]}>
{' '}
&middot; {timeElapsed}
</Text>
)}
</TimeElapsed>
)}
{(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
numberOfLines={1}
style={[a.text_sm, t.atoms.text_contrast_medium, a.pb_xs]}>
@{profile.handle}
</Text>
)}
<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]}>
<Text <Text
numberOfLines={1} emoji
style={[{maxWidth: '85%'}, web([a.leading_normal])]}> numberOfLines={2}
<Text style={[
emoji a.text_sm,
style={[ a.leading_snug,
a.text_md, convo.unreadCount > 0
t.atoms.text, ? a.font_bold
a.font_bold, : t.atoms.text_contrast_high,
{lineHeight: 21}, isDimStyle && t.atoms.text_contrast_medium,
isDimStyle && t.atoms.text_contrast_medium, ]}>
]}> {lastMessage}
{displayName}
</Text>
</Text> </Text>
{lastMessageSentAt && (
<TimeElapsed timestamp={lastMessageSentAt}> <PostAlerts
{({timeElapsed}) => ( modui={moderation.ui('contentList')}
<Text size="lg"
style={[ style={[a.pt_xs]}
a.text_sm, />
{lineHeight: 21},
t.atoms.text_contrast_medium,
web({whiteSpace: 'preserve nowrap'}),
]}>
{' '}
&middot; {timeElapsed}
</Text>
)}
</TimeElapsed>
)}
{(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> </View>
{!isDeletedAccount && ( {convo.unreadCount > 0 && (
<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,
convo.unreadCount > 0
? a.font_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]}
/>
</View> </View>
)}
</Link>
{convo.unreadCount > 0 && ( <ConvoMenu
<View convo={convo}
style={[ profile={profile}
a.absolute, control={menuControl}
a.rounded_full, currentScreen="list"
{ showMarkAsRead={convo.unreadCount > 0}
backgroundColor: isDimStyle hideTrigger={isNative}
? t.palette.contrast_200 blockInfo={blockInfo}
: t.palette.primary_500, style={[
height: 7, a.absolute,
width: 7, a.h_full,
top: 15, a.self_end,
right: 12, a.justify_center,
}, {
]} right: tokens.space.lg,
/> opacity: !gtMobile || showActions || menuControl.isOpen ? 1 : 0,
)} },
</View> ]}
)} />
</Link> <LeaveConvoPrompt
control={leaveConvoControl}
<ConvoMenu convoId={convo.id}
convo={convo} currentScreen="list"
profile={profile} />
control={menuControl} </View>
currentScreen="list" </SwipeableRow>
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,
},
]}
/>
</View>
) )
} }
@@ -0,0 +1,45 @@
import React, {useCallback} from 'react'
import {View} from 'react-native'
// TODO: replace with ReanimatedSwipeable when we next upgrade RNGH -sfn
import Swipeable from 'react-native-gesture-handler/Swipeable'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {Trash_Stroke2_Corner2_Rounded as TrashIcon} from '#/components/icons/Trash'
export function SwipeableRow({
children,
onDelete,
}: {
children: React.ReactNode
onDelete: () => void
}) {
const {_} = useLingui()
const t = useTheme()
const renderActions = useCallback(() => {
return (
<Button label={_(msg`Delete chat`)} onPress={onDelete} style={[a.px_lg]}>
<View
style={[
{height: 50, width: 50, backgroundColor: t.palette.negative_400},
a.rounded_full,
a.align_center,
a.justify_center,
]}>
<TrashIcon fill={t.palette.white} size="xl" />
</View>
</Button>
)
}, [_, t, onDelete])
return (
<Swipeable
renderRightActions={renderActions}
containerStyle={[{backgroundColor: t.palette.negative_25}]}>
{children}
</Swipeable>
)
}
@@ -0,0 +1 @@
export {View as SwipeableRow} from 'react-native'