[🐴] Fully implement keyboard controller (#4106)

* Revert "[🐴] Ensure keyboard gets dismissed when leaving screen (#4104)"

This reverts commit 3ca671d9aa.

* getting somewhere

* remove some now nuneeded code

* fully implement keyboard controller

* onStartReached check

* fix new messages pill alignment

* scroll to end on press

* simplify pill scroll logic

* update comment

* adjust logic on when to hide the pill

* fix backgrounding jank

* improve look of deleting messages

* add double tap on messages

* better onStartReached logic

* nit

* add hit slop to the gesture

* better gestures for press and hold

* nits
This commit is contained in:
Hailey 2024-05-19 19:25:49 -07:00 committed by GitHub
parent 7de0b0a58c
commit 52beb29a0d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 419 additions and 356 deletions

View file

@ -1,5 +1,6 @@
import React from 'react'
import {Keyboard, Pressable, View} from 'react-native'
import {Keyboard} from 'react-native'
import {Gesture, GestureDetector} from 'react-native-gesture-handler'
import Animated, {
cancelAnimation,
runOnJS,
@ -15,8 +16,6 @@ import {atoms as a} from '#/alf'
import {MessageMenu} from '#/components/dms/MessageMenu'
import {useMenuControl} from '#/components/Menu'
const AnimatedPressable = Animated.createAnimatedComponent(Pressable)
export function ActionsWrapper({
message,
isFromSelf,
@ -30,56 +29,59 @@ export function ActionsWrapper({
const menuControl = useMenuControl()
const scale = useSharedValue(1)
const animationDidComplete = useSharedValue(false)
const animatedStyle = useAnimatedStyle(() => ({
transform: [{scale: scale.value}],
}))
// Reanimated's `runOnJS` doesn't like refs, so we can't use `runOnJS(menuControl.open)()`. Instead, we'll use this
// function
const open = React.useCallback(() => {
playHaptic()
Keyboard.dismiss()
menuControl.open()
}, [menuControl])
}, [menuControl, playHaptic])
const shrink = React.useCallback(() => {
'worklet'
cancelAnimation(scale)
scale.value = withTiming(1, {duration: 200}, () => {
animationDidComplete.value = false
})
}, [animationDidComplete, scale])
scale.value = withTiming(1, {duration: 200})
}, [scale])
const grow = React.useCallback(() => {
'worklet'
scale.value = withTiming(1.05, {duration: 450}, finished => {
if (!finished) return
animationDidComplete.value = true
runOnJS(playHaptic)()
runOnJS(open)()
const doubleTapGesture = Gesture.Tap()
.numberOfTaps(2)
.hitSlop(HITSLOP_10)
.onEnd(open)
shrink()
const pressAndHoldGesture = Gesture.LongPress()
.onStart(() => {
scale.value = withTiming(1.05, {duration: 200}, finished => {
if (!finished) return
runOnJS(open)()
shrink()
})
})
}, [scale, animationDidComplete, playHaptic, shrink, open])
.onTouchesUp(shrink)
.onTouchesMove(shrink)
.cancelsTouchesInView(false)
.runOnJS(true)
const composedGestures = Gesture.Exclusive(
doubleTapGesture,
pressAndHoldGesture,
)
return (
<View
style={[
{
maxWidth: '80%',
},
isFromSelf ? a.self_end : a.self_start,
]}>
<AnimatedPressable
style={animatedStyle}
unstable_pressDelay={200}
onPressIn={grow}
onTouchEnd={shrink}
hitSlop={HITSLOP_10}>
<GestureDetector gesture={composedGestures}>
<Animated.View
style={[
{
maxWidth: '80%',
},
isFromSelf ? a.self_end : a.self_start,
animatedStyle,
]}>
{children}
</AnimatedPressable>
<MessageMenu message={message} control={menuControl} />
</View>
<MessageMenu message={message} control={menuControl} />
</Animated.View>
</GestureDetector>
)
}

View file

@ -1,5 +1,5 @@
import React, {useCallback} from 'react'
import {Keyboard, TouchableOpacity, View} from 'react-native'
import {TouchableOpacity, View} from 'react-native'
import {
AppBskyActorDefs,
ModerationCause,
@ -46,7 +46,6 @@ export let MessagesListHeader = ({
if (isWeb) {
navigation.replace('Messages', {})
} else {
Keyboard.dismiss()
navigation.goBack()
}
}, [navigation])

View file

@ -1,47 +1,97 @@
import React from 'react'
import {View} from 'react-native'
import Animated from 'react-native-reanimated'
import {Pressable, View} from 'react-native'
import Animated, {
runOnJS,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {Trans} from '@lingui/macro'
import {
ScaleAndFadeIn,
ScaleAndFadeOut,
} from 'lib/custom-animations/ScaleAndFade'
import {useHaptics} from 'lib/haptics'
import {isAndroid, isIOS, isWeb} from 'platform/detection'
import {atoms as a, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
export function NewMessagesPill() {
const t = useTheme()
const AnimatedPressable = Animated.createAnimatedComponent(Pressable)
React.useEffect(() => {}, [])
export function NewMessagesPill({
onPress: onPressInner,
}: {
onPress: () => void
}) {
const t = useTheme()
const playHaptic = useHaptics()
const {bottom: bottomInset} = useSafeAreaInsets()
const bottomBarHeight = isIOS ? 42 : isAndroid ? 60 : 0
const bottomOffset = isWeb ? 0 : bottomInset + bottomBarHeight
const scale = useSharedValue(1)
const onPressIn = React.useCallback(() => {
if (isWeb) return
scale.value = withTiming(1.075, {duration: 100})
}, [scale])
const onPressOut = React.useCallback(() => {
if (isWeb) return
scale.value = withTiming(1, {duration: 100})
}, [scale])
const onPress = React.useCallback(() => {
runOnJS(playHaptic)()
onPressInner?.()
}, [onPressInner, playHaptic])
const animatedStyle = useAnimatedStyle(() => ({
transform: [{scale: scale.value}],
}))
return (
<Animated.View
<View
style={[
a.py_sm,
a.rounded_full,
a.shadow_sm,
a.border,
t.atoms.bg_contrast_50,
t.atoms.border_contrast_medium,
a.absolute,
a.w_full,
a.z_10,
a.align_center,
{
position: 'absolute',
bottom: 70,
width: '40%',
left: '30%',
alignItems: 'center',
shadowOpacity: 0.125,
shadowRadius: 12,
shadowOffset: {width: 0, height: 5},
bottom: bottomOffset + 70,
// Don't prevent scrolling in this area _except_ for in the pill itself
pointerEvents: 'box-none',
},
]}
entering={ScaleAndFadeIn}
exiting={ScaleAndFadeOut}>
<View style={{flex: 1}}>
]}>
<AnimatedPressable
style={[
a.py_sm,
a.rounded_full,
a.shadow_sm,
a.border,
t.atoms.bg_contrast_50,
t.atoms.border_contrast_medium,
{
width: 160,
alignItems: 'center',
shadowOpacity: 0.125,
shadowRadius: 12,
shadowOffset: {width: 0, height: 5},
pointerEvents: 'box-only',
},
animatedStyle,
]}
entering={ScaleAndFadeIn}
exiting={ScaleAndFadeOut}
onPress={onPress}
onPressIn={onPressIn}
onPressOut={onPressOut}>
<Text style={[a.font_bold]}>
<Trans>New messages</Trans>
</Text>
</View>
</Animated.View>
</AnimatedPressable>
</View>
)
}