Swipeable to delete chat, custom swipeable (#5614)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#080B12" fill-rule="evenodd" d="M4 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v6.386c1.064-.002 2 .86 2 2.001V19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-6.613c0-1.142.936-2.003 2-2.001V4Zm2 6.946 6 2 6-2V4H6v6.946ZM9 8a1 1 0 0 1 1-1h4a1 1 0 1 1 0 2h-4a1 1 0 0 1-1-1Zm2.367 6.843L4 12.387V19h16v-6.613l-7.367 2.456a2 2 0 0 1-1.265 0Z" clip-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 426 B |
@@ -0,0 +1,5 @@
|
|||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const Envelope_Open_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M4 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v6.386c1.064-.002 2 .86 2 2.001V19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-6.613c0-1.142.936-2.003 2-2.001V4Zm2 6.946 6 2 6-2V4H6v6.946ZM9 8a1 1 0 0 1 1-1h4a1 1 0 1 1 0 2h-4a1 1 0 0 1-1-1Zm2.367 6.843L4 12.387V19h16v-6.613l-7.367 2.456a2 2 0 0 1-1.265 0Z',
|
||||||
|
})
|
||||||
@@ -0,0 +1,410 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {ColorValue, Dimensions, StyleSheet, View} from 'react-native'
|
||||||
|
import {Gesture, GestureDetector} from 'react-native-gesture-handler'
|
||||||
|
import Animated, {
|
||||||
|
clamp,
|
||||||
|
interpolate,
|
||||||
|
interpolateColor,
|
||||||
|
runOnJS,
|
||||||
|
useAnimatedReaction,
|
||||||
|
useAnimatedStyle,
|
||||||
|
useDerivedValue,
|
||||||
|
useReducedMotion,
|
||||||
|
useSharedValue,
|
||||||
|
withSequence,
|
||||||
|
withTiming,
|
||||||
|
} from 'react-native-reanimated'
|
||||||
|
|
||||||
|
import {useHaptics} from '#/lib/haptics'
|
||||||
|
|
||||||
|
interface GestureAction {
|
||||||
|
color: ColorValue
|
||||||
|
action: () => void
|
||||||
|
threshold: number
|
||||||
|
icon: React.ElementType
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GestureActions {
|
||||||
|
leftFirst?: GestureAction
|
||||||
|
leftSecond?: GestureAction
|
||||||
|
rightFirst?: GestureAction
|
||||||
|
rightSecond?: GestureAction
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_WIDTH = Dimensions.get('screen').width
|
||||||
|
const ICON_SIZE = 32
|
||||||
|
|
||||||
|
export function GestureActionView({
|
||||||
|
children,
|
||||||
|
actions,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
actions: GestureActions
|
||||||
|
}) {
|
||||||
|
if (
|
||||||
|
(actions.leftSecond && !actions.leftFirst) ||
|
||||||
|
(actions.rightSecond && !actions.rightFirst)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'You must provide the first action before the second action',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const [activeAction, setActiveAction] = React.useState<
|
||||||
|
'leftFirst' | 'leftSecond' | 'rightFirst' | 'rightSecond' | null
|
||||||
|
>(null)
|
||||||
|
|
||||||
|
const haptic = useHaptics()
|
||||||
|
const isReducedMotion = useReducedMotion()
|
||||||
|
|
||||||
|
const transX = useSharedValue(0)
|
||||||
|
const clampedTransX = useDerivedValue(() => {
|
||||||
|
const min = actions.leftFirst ? -MAX_WIDTH : 0
|
||||||
|
const max = actions.rightFirst ? MAX_WIDTH : 0
|
||||||
|
return clamp(transX.value, min, max)
|
||||||
|
})
|
||||||
|
|
||||||
|
const iconScale = useSharedValue(1)
|
||||||
|
const isActive = useSharedValue(false)
|
||||||
|
const hitFirst = useSharedValue(false)
|
||||||
|
const hitSecond = useSharedValue(false)
|
||||||
|
|
||||||
|
const runPopAnimation = () => {
|
||||||
|
'worklet'
|
||||||
|
if (isReducedMotion) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
iconScale.value = withSequence(
|
||||||
|
withTiming(1.2, {duration: 175}),
|
||||||
|
withTiming(1, {duration: 100}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
useAnimatedReaction(
|
||||||
|
() => transX,
|
||||||
|
() => {
|
||||||
|
if (transX.value === 0) {
|
||||||
|
runOnJS(setActiveAction)(null)
|
||||||
|
} else if (transX.value < 0) {
|
||||||
|
if (
|
||||||
|
actions.leftSecond &&
|
||||||
|
transX.value <= -actions.leftSecond.threshold
|
||||||
|
) {
|
||||||
|
if (activeAction !== 'leftSecond') {
|
||||||
|
runOnJS(setActiveAction)('leftSecond')
|
||||||
|
}
|
||||||
|
} else if (activeAction !== 'leftFirst') {
|
||||||
|
runOnJS(setActiveAction)('leftFirst')
|
||||||
|
}
|
||||||
|
} else if (transX.value > 0) {
|
||||||
|
if (
|
||||||
|
actions.rightSecond &&
|
||||||
|
transX.value > actions.rightSecond.threshold
|
||||||
|
) {
|
||||||
|
if (activeAction !== 'rightSecond') {
|
||||||
|
runOnJS(setActiveAction)('rightSecond')
|
||||||
|
}
|
||||||
|
} else if (activeAction !== 'rightFirst') {
|
||||||
|
runOnJS(setActiveAction)('rightFirst')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const panGesture = Gesture.Pan()
|
||||||
|
.activeOffsetX([-10, 10])
|
||||||
|
// Absurdly high value so it doesn't interfere with the pan gestures above (i.e., scroll)
|
||||||
|
// reanimated doesn't offer great support for disabling y/x axes :/
|
||||||
|
.activeOffsetY([-200, 200])
|
||||||
|
.onStart(() => {
|
||||||
|
'worklet'
|
||||||
|
isActive.value = true
|
||||||
|
})
|
||||||
|
.onChange(e => {
|
||||||
|
'worklet'
|
||||||
|
transX.value = e.translationX
|
||||||
|
|
||||||
|
if (e.translationX < 0) {
|
||||||
|
// Left side
|
||||||
|
if (actions.leftSecond) {
|
||||||
|
if (
|
||||||
|
e.translationX <= -actions.leftSecond.threshold &&
|
||||||
|
!hitSecond.value
|
||||||
|
) {
|
||||||
|
runPopAnimation()
|
||||||
|
runOnJS(haptic)()
|
||||||
|
hitSecond.value = true
|
||||||
|
} else if (
|
||||||
|
hitSecond.value &&
|
||||||
|
e.translationX > -actions.leftSecond.threshold
|
||||||
|
) {
|
||||||
|
runPopAnimation()
|
||||||
|
hitSecond.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hitSecond.value && actions.leftFirst) {
|
||||||
|
if (
|
||||||
|
e.translationX <= -actions.leftFirst.threshold &&
|
||||||
|
!hitFirst.value
|
||||||
|
) {
|
||||||
|
runPopAnimation()
|
||||||
|
runOnJS(haptic)()
|
||||||
|
hitFirst.value = true
|
||||||
|
} else if (
|
||||||
|
hitFirst.value &&
|
||||||
|
e.translationX > -actions.leftFirst.threshold
|
||||||
|
) {
|
||||||
|
hitFirst.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (e.translationX > 0) {
|
||||||
|
// Right side
|
||||||
|
if (actions.rightSecond) {
|
||||||
|
if (
|
||||||
|
e.translationX >= actions.rightSecond.threshold &&
|
||||||
|
!hitSecond.value
|
||||||
|
) {
|
||||||
|
runPopAnimation()
|
||||||
|
runOnJS(haptic)()
|
||||||
|
hitSecond.value = true
|
||||||
|
} else if (
|
||||||
|
hitSecond.value &&
|
||||||
|
e.translationX < actions.rightSecond.threshold
|
||||||
|
) {
|
||||||
|
runPopAnimation()
|
||||||
|
hitSecond.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hitSecond.value && actions.rightFirst) {
|
||||||
|
if (
|
||||||
|
e.translationX >= actions.rightFirst.threshold &&
|
||||||
|
!hitFirst.value
|
||||||
|
) {
|
||||||
|
runPopAnimation()
|
||||||
|
runOnJS(haptic)()
|
||||||
|
hitFirst.value = true
|
||||||
|
} else if (
|
||||||
|
hitFirst.value &&
|
||||||
|
e.translationX < actions.rightFirst.threshold
|
||||||
|
) {
|
||||||
|
hitFirst.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.onEnd(e => {
|
||||||
|
'worklet'
|
||||||
|
if (e.translationX < 0) {
|
||||||
|
if (hitSecond.value && actions.leftSecond) {
|
||||||
|
runOnJS(actions.leftSecond.action)()
|
||||||
|
} else if (hitFirst.value && actions.leftFirst) {
|
||||||
|
runOnJS(actions.leftFirst.action)()
|
||||||
|
}
|
||||||
|
} else if (e.translationX > 0) {
|
||||||
|
if (hitSecond.value && actions.rightSecond) {
|
||||||
|
runOnJS(actions.rightSecond.action)()
|
||||||
|
} else if (hitSecond.value && actions.rightFirst) {
|
||||||
|
runOnJS(actions.rightFirst.action)()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
transX.value = withTiming(0, {duration: 200})
|
||||||
|
hitFirst.value = false
|
||||||
|
hitSecond.value = false
|
||||||
|
isActive.value = false
|
||||||
|
})
|
||||||
|
|
||||||
|
const composedGesture = Gesture.Simultaneous(panGesture)
|
||||||
|
|
||||||
|
const animatedSliderStyle = useAnimatedStyle(() => {
|
||||||
|
return {
|
||||||
|
transform: [{translateX: clampedTransX.value}],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const leftSideInterpolation = React.useMemo(() => {
|
||||||
|
return createInterpolation({
|
||||||
|
firstColor: actions.leftFirst?.color,
|
||||||
|
secondColor: actions.leftSecond?.color,
|
||||||
|
firstThreshold: actions.leftFirst?.threshold,
|
||||||
|
secondThreshold: actions.leftSecond?.threshold,
|
||||||
|
side: 'left',
|
||||||
|
})
|
||||||
|
}, [actions.leftFirst, actions.leftSecond])
|
||||||
|
|
||||||
|
const rightSideInterpolation = React.useMemo(() => {
|
||||||
|
return createInterpolation({
|
||||||
|
firstColor: actions.rightFirst?.color,
|
||||||
|
secondColor: actions.rightSecond?.color,
|
||||||
|
firstThreshold: actions.rightFirst?.threshold,
|
||||||
|
secondThreshold: actions.rightSecond?.threshold,
|
||||||
|
side: 'right',
|
||||||
|
})
|
||||||
|
}, [actions.rightFirst, actions.rightSecond])
|
||||||
|
|
||||||
|
const interpolation = React.useMemo<{
|
||||||
|
inputRange: number[]
|
||||||
|
outputRange: ColorValue[]
|
||||||
|
}>(() => {
|
||||||
|
if (!actions.leftFirst) {
|
||||||
|
return rightSideInterpolation!
|
||||||
|
} else if (!actions.rightFirst) {
|
||||||
|
return leftSideInterpolation!
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
inputRange: [
|
||||||
|
...leftSideInterpolation.inputRange,
|
||||||
|
...rightSideInterpolation.inputRange,
|
||||||
|
],
|
||||||
|
outputRange: [
|
||||||
|
...leftSideInterpolation.outputRange,
|
||||||
|
...rightSideInterpolation.outputRange,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
leftSideInterpolation,
|
||||||
|
rightSideInterpolation,
|
||||||
|
actions.leftFirst,
|
||||||
|
actions.rightFirst,
|
||||||
|
])
|
||||||
|
|
||||||
|
const animatedBackgroundStyle = useAnimatedStyle(() => {
|
||||||
|
return {
|
||||||
|
backgroundColor: interpolateColor(
|
||||||
|
clampedTransX.value,
|
||||||
|
interpolation.inputRange,
|
||||||
|
// @ts-expect-error - Weird type expected by reanimated, but this is okay
|
||||||
|
interpolation.outputRange,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const animatedIconStyle = useAnimatedStyle(() => {
|
||||||
|
const absTransX = Math.abs(clampedTransX.value)
|
||||||
|
return {
|
||||||
|
opacity: interpolate(absTransX, [0, 75], [0.15, 1]),
|
||||||
|
transform: [{scale: iconScale.value}],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GestureDetector gesture={composedGesture}>
|
||||||
|
<View>
|
||||||
|
<Animated.View
|
||||||
|
style={[StyleSheet.absoluteFill, animatedBackgroundStyle]}>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
marginHorizontal: 12,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems:
|
||||||
|
activeAction === 'leftFirst' || activeAction === 'leftSecond'
|
||||||
|
? 'flex-end'
|
||||||
|
: 'flex-start',
|
||||||
|
}}>
|
||||||
|
<Animated.View style={[animatedIconStyle]}>
|
||||||
|
{activeAction === 'leftFirst' && actions.leftFirst?.icon ? (
|
||||||
|
<actions.leftFirst.icon
|
||||||
|
height={ICON_SIZE}
|
||||||
|
width={ICON_SIZE}
|
||||||
|
style={{
|
||||||
|
color: 'white',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : activeAction === 'leftSecond' && actions.leftSecond?.icon ? (
|
||||||
|
<actions.leftSecond.icon
|
||||||
|
height={ICON_SIZE}
|
||||||
|
width={ICON_SIZE}
|
||||||
|
style={{color: 'white'}}
|
||||||
|
/>
|
||||||
|
) : activeAction === 'rightFirst' && actions.rightFirst?.icon ? (
|
||||||
|
<actions.rightFirst.icon
|
||||||
|
height={ICON_SIZE}
|
||||||
|
width={ICON_SIZE}
|
||||||
|
style={{color: 'white'}}
|
||||||
|
/>
|
||||||
|
) : activeAction === 'rightSecond' &&
|
||||||
|
actions.rightSecond?.icon ? (
|
||||||
|
<actions.rightSecond.icon
|
||||||
|
height={ICON_SIZE}
|
||||||
|
width={ICON_SIZE}
|
||||||
|
style={{color: 'white'}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</Animated.View>
|
||||||
|
</View>
|
||||||
|
</Animated.View>
|
||||||
|
<Animated.View style={animatedSliderStyle}>{children}</Animated.View>
|
||||||
|
</View>
|
||||||
|
</GestureDetector>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function createInterpolation({
|
||||||
|
firstColor,
|
||||||
|
secondColor,
|
||||||
|
firstThreshold,
|
||||||
|
secondThreshold,
|
||||||
|
side,
|
||||||
|
}: {
|
||||||
|
firstColor?: ColorValue
|
||||||
|
secondColor?: ColorValue
|
||||||
|
firstThreshold?: number
|
||||||
|
secondThreshold?: number
|
||||||
|
side: 'left' | 'right'
|
||||||
|
}): {
|
||||||
|
inputRange: number[]
|
||||||
|
outputRange: ColorValue[]
|
||||||
|
} {
|
||||||
|
if ((secondThreshold && !secondColor) || (!secondThreshold && secondColor)) {
|
||||||
|
throw new Error(
|
||||||
|
'You must provide a second color if you provide a second threshold',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!firstThreshold) {
|
||||||
|
return {
|
||||||
|
inputRange: [0],
|
||||||
|
outputRange: ['transparent'],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const offset = side === 'left' ? -20 : 20
|
||||||
|
|
||||||
|
if (side === 'left') {
|
||||||
|
firstThreshold = -firstThreshold
|
||||||
|
|
||||||
|
if (secondThreshold) {
|
||||||
|
secondThreshold = -secondThreshold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let res
|
||||||
|
if (secondThreshold) {
|
||||||
|
res = {
|
||||||
|
inputRange: [
|
||||||
|
0,
|
||||||
|
firstThreshold,
|
||||||
|
firstThreshold + offset - 20,
|
||||||
|
secondThreshold,
|
||||||
|
],
|
||||||
|
outputRange: ['transparent', firstColor!, firstColor!, secondColor!],
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res = {
|
||||||
|
inputRange: [0, firstThreshold],
|
||||||
|
outputRange: ['transparent', firstColor!],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (side === 'left') {
|
||||||
|
// Reverse the input/output ranges
|
||||||
|
res.inputRange.reverse()
|
||||||
|
res.outputRange.reverse()
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export function GestureActionView({children}: {children: React.ReactNode}) {
|
||||||
|
return children
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {GestureActionView} from '#/lib/custom-animations/GestureActionView'
|
||||||
import {useHaptics} from '#/lib/haptics'
|
import {useHaptics} from '#/lib/haptics'
|
||||||
import {decrementBadgeCount} from '#/lib/notifications/notifications'
|
import {decrementBadgeCount} from '#/lib/notifications/notifications'
|
||||||
import {logEvent} from '#/lib/statsig/statsig'
|
import {logEvent} from '#/lib/statsig/statsig'
|
||||||
@@ -22,13 +23,18 @@ import {
|
|||||||
import {isNative} from '#/platform/detection'
|
import {isNative} from '#/platform/detection'
|
||||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
|
import {useMarkAsReadMutation} from '#/state/queries/messages/conversation'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {TimeElapsed} from '#/view/com/util/TimeElapsed'
|
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 {Envelope_Open_Stroke2_Corner0_Rounded as EnvelopeOpen} from '#/components/icons/EnveopeOpen'
|
||||||
|
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'
|
||||||
@@ -74,15 +80,18 @@ 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 {mutate: markAsRead} = useMarkAsReadMutation()
|
||||||
const moderation = React.useMemo(
|
const moderation = React.useMemo(
|
||||||
() => moderateProfile(profile, moderationOpts),
|
() => moderateProfile(profile, moderationOpts),
|
||||||
[profile, moderationOpts],
|
[profile, moderationOpts],
|
||||||
)
|
)
|
||||||
const playHaptic = useHaptics()
|
const playHaptic = useHaptics()
|
||||||
|
const isUnread = convo.unreadCount > 0
|
||||||
|
|
||||||
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 +112,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
|
||||||
|
|
||||||
@@ -196,183 +205,220 @@ function ChatListItemReady({
|
|||||||
menuControl.open()
|
menuControl.open()
|
||||||
}, [playHaptic, menuControl])
|
}, [playHaptic, menuControl])
|
||||||
|
|
||||||
|
const markReadAction = {
|
||||||
|
threshold: 120,
|
||||||
|
color: t.palette.primary_500,
|
||||||
|
icon: EnvelopeOpen,
|
||||||
|
action: () => {
|
||||||
|
markAsRead({
|
||||||
|
convoId: convo.id,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteAction = {
|
||||||
|
threshold: 225,
|
||||||
|
color: t.palette.negative_500,
|
||||||
|
icon: Trash_Stroke2_Corner0_Rounded,
|
||||||
|
action: () => {
|
||||||
|
leaveConvoControl.open()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = isUnread
|
||||||
|
? {
|
||||||
|
leftFirst: markReadAction,
|
||||||
|
leftSecond: deleteAction,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
leftFirst: deleteAction,
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<GestureActionView actions={actions}>
|
||||||
// @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'}),
|
||||||
|
]}>
|
||||||
|
{' '}
|
||||||
|
· {timeElapsed}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</TimeElapsed>
|
||||||
|
)}
|
||||||
|
{(convo.muted || moderation.blocked) && (
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.text_sm,
|
||||||
|
{lineHeight: 21},
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
web({whiteSpace: 'preserve nowrap'}),
|
||||||
|
]}>
|
||||||
|
{' '}
|
||||||
|
·{' '}
|
||||||
|
<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'}),
|
|
||||||
]}>
|
|
||||||
{' '}
|
|
||||||
· {timeElapsed}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</TimeElapsed>
|
|
||||||
)}
|
|
||||||
{(convo.muted || moderation.blocked) && (
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.text_sm,
|
|
||||||
{lineHeight: 21},
|
|
||||||
t.atoms.text_contrast_medium,
|
|
||||||
web({whiteSpace: 'preserve nowrap'}),
|
|
||||||
]}>
|
|
||||||
{' '}
|
|
||||||
·{' '}
|
|
||||||
<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"
|
</GestureActionView>
|
||||||
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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user