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 {
AppBskyActorDefs,
@@ -27,12 +27,15 @@ import {TimeElapsed} from '#/view/com/util/TimeElapsed'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
import * as tokens from '#/alf/tokens'
import {useDialogControl} from '#/components/Dialog'
import {ConvoMenu} from '#/components/dms/ConvoMenu'
import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt'
import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/Bell2'
import {Link} from '#/components/Link'
import {useMenuControl} from '#/components/Menu'
import {PostAlerts} from '#/components/moderation/PostAlerts'
import {Text} from '#/components/Typography'
import {SwipeableRow} from './SwipeableRow'
export let ChatListItem = ({
convo,
@@ -74,6 +77,7 @@ function ChatListItemReady({
const {_} = useLingui()
const {currentAccount} = useSession()
const menuControl = useMenuControl()
const leaveConvoControl = useDialogControl()
const {gtMobile} = useBreakpoints()
const profile = useProfileShadow(profileUnshadowed)
const moderation = React.useMemo(
@@ -82,7 +86,7 @@ function ChatListItemReady({
)
const playHaptic = useHaptics()
const blockInfo = React.useMemo(() => {
const blockInfo = useMemo(() => {
const modui = moderation.ui('profileView')
const blocks = modui.alerts.filter(alert => alert.type === 'blocking')
const listBlocks = blocks.filter(alert => alert.source.type === 'list')
@@ -103,7 +107,7 @@ function ChatListItemReady({
const isDimStyle = convo.muted || moderation.blocked || isDeletedAccount
const {lastMessage, lastMessageSentAt} = React.useMemo(() => {
const {lastMessage, lastMessageSentAt} = useMemo(() => {
let lastMessage = _(msg`No messages yet`)
let lastMessageSentAt: string | null = null
@@ -197,182 +201,190 @@ function ChatListItemReady({
}, [playHaptic, menuControl])
return (
<View
// @ts-expect-error web only
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onFocus={onFocus}
onBlur={onMouseLeave}
style={[a.relative]}>
<SwipeableRow onDelete={leaveConvoControl.open}>
<View
style={[
a.z_10,
a.absolute,
{top: tokens.space.md, left: tokens.space.lg},
]}>
<PreviewableUserAvatar
profile={profile}
size={52}
moderation={moderation.ui('avatar')}
/>
</View>
// @ts-expect-error web only
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onFocus={onFocus}
onBlur={onMouseLeave}
style={[a.relative, t.atoms.bg]}>
<View
style={[
a.z_10,
a.absolute,
{top: tokens.space.md, left: tokens.space.lg},
]}>
<PreviewableUserAvatar
profile={profile}
size={52}
moderation={moderation.ui('avatar')}
/>
</View>
<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
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,
t.atoms.border_contrast_low,
]}>
{/* 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
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,
t.atoms.border_contrast_low,
]}>
{/* 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]}>
<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
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>
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>
{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>
)}
<PostAlerts
modui={moderation.ui('contentList')}
size="lg"
style={[a.pt_xs]}
/>
</View>
{!isDeletedAccount && (
<Text
numberOfLines={1}
style={[a.text_sm, t.atoms.text_contrast_medium, a.pb_xs]}>
@{profile.handle}
</Text>
{convo.unreadCount > 0 && (
<View
style={[
a.absolute,
a.rounded_full,
{
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>
)}
</Link>
{convo.unreadCount > 0 && (
<View
style={[
a.absolute,
a.rounded_full,
{
backgroundColor: isDimStyle
? t.palette.contrast_200
: t.palette.primary_500,
height: 7,
width: 7,
top: 15,
right: 12,
},
]}
/>
)}
</View>
)}
</Link>
<ConvoMenu
convo={convo}
profile={profile}
control={menuControl}
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,
},
]}
/>
</View>
<ConvoMenu
convo={convo}
profile={profile}
control={menuControl}
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,
},
]}
/>
<LeaveConvoPrompt
control={leaveConvoControl}
convoId={convo.id}
currentScreen="list"
/>
</View>
</SwipeableRow>
)
}
@@ -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'