Bluesky app fork with some witchin' additions ๐Ÿ’ซ

[๐Ÿด] Remove keyboard controller lib (#4038)

* remove library

* implement using just reanimated

* always return false for `keyboardIsOpening` on web

* undo comment

* handle input focus scroll more elegantly

* add back minimal shell toggle on mobile web

* adjust initialnumtorender

* oops

* nit

authored by hailey.at and committed by

GitHub b15b49a4 da2bdf5d

+89 -74
-1
package.json
··· 171 171 "react-native-get-random-values": "~1.11.0", 172 172 "react-native-image-crop-picker": "^0.38.1", 173 173 "react-native-ios-context-menu": "^1.15.3", 174 - "react-native-keyboard-controller": "^1.11.7", 175 174 "react-native-pager-view": "6.2.3", 176 175 "react-native-picker-select": "^8.1.0", 177 176 "react-native-progress": "bluesky-social/react-native-progress",
+1 -2
src/screens/Messages/Conversation/MessageInput.tsx
··· 65 65 const keyboardHeight = Keyboard.metrics()?.height ?? 0 66 66 const windowHeight = Dimensions.get('window').height 67 67 68 - const max = windowHeight - keyboardHeight - topInset - 100 68 + const max = windowHeight - keyboardHeight - topInset - 150 69 69 const availableSpace = max - e.nativeEvent.contentSize.height 70 70 71 71 setMaxHeight(max) ··· 108 108 keyboardAppearance={t.name === 'light' ? 'light' : 'dark'} 109 109 scrollEnabled={isInputScrollable} 110 110 blurOnSubmit={false} 111 - onFocus={scrollToEnd} 112 111 onContentSizeChange={onInputLayout} 113 112 ref={inputRef} 114 113 hitSlop={HITSLOP_10}
+60 -20
src/screens/Messages/Conversation/MessagesList.tsx
··· 1 1 import React, {useCallback, useRef} from 'react' 2 2 import {FlatList, View} from 'react-native' 3 - import {useKeyboardHandler} from 'react-native-keyboard-controller' 4 - import {runOnJS, useSharedValue} from 'react-native-reanimated' 3 + import Animated, { 4 + useAnimatedKeyboard, 5 + useAnimatedReaction, 6 + useAnimatedStyle, 7 + useSharedValue, 8 + } from 'react-native-reanimated' 5 9 import {ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/reanimated2/hook/commonTypes' 10 + import {useSafeAreaInsets} from 'react-native-safe-area-context' 6 11 import {AppBskyRichtextFacet, RichText} from '@atproto/api' 7 12 8 13 import {shortenLinks} from '#/lib/strings/rich-text-manip' 9 - import {isNative} from '#/platform/detection' 14 + import {isIOS, isNative} from '#/platform/detection' 10 15 import {useConvoActive} from '#/state/messages/convo' 11 16 import {ConvoItem} from '#/state/messages/convo/types' 12 17 import {useAgent} from '#/state/session' ··· 15 20 import {List} from 'view/com/util/List' 16 21 import {MessageInput} from '#/screens/Messages/Conversation/MessageInput' 17 22 import {MessageListError} from '#/screens/Messages/Conversation/MessageListError' 18 - import {atoms as a} from '#/alf' 23 + import {atoms as a, useBreakpoints, useTheme} from '#/alf' 19 24 import {MessageItem} from '#/components/dms/MessageItem' 20 25 import {Loader} from '#/components/Loader' 21 26 import {Text} from '#/components/Typography' ··· 55 60 } 56 61 57 62 export function MessagesList() { 63 + const t = useTheme() 58 64 const convo = useConvoActive() 59 65 const {getAgent} = useAgent() 60 66 const flatListRef = useRef<FlatList>(null) ··· 74 80 // We don't want to call `scrollToEnd` again if we are already scolling to the end, because this creates a bit of jank 75 81 // Instead, we use `onMomentumScrollEnd` and this value to determine if we need to start scrolling or not. 76 82 const isMomentumScrolling = useSharedValue(false) 77 - 78 83 const hasInitiallyScrolled = useSharedValue(false) 84 + const keyboardIsOpening = useSharedValue(false) 79 85 80 86 // Every time the content size changes, that means one of two things is happening: 81 87 // 1. New messages are being added from the log or from a message you have sent ··· 101 107 contentHeight.value = height 102 108 103 109 // This number _must_ be the height of the MaybeLoader component 104 - if (height <= 50 || !isAtBottom.value) { 110 + if (height <= 50 || (!isAtBottom.value && !keyboardIsOpening.value)) { 105 111 return 106 112 } 107 113 108 114 flatListRef.current?.scrollToOffset({ 109 - animated: hasInitiallyScrolled.value, 115 + animated: hasInitiallyScrolled.value && !keyboardIsOpening.value, 110 116 offset: height, 111 117 }) 112 118 isMomentumScrolling.value = true 113 119 }, 114 120 [ 115 121 contentHeight, 116 - hasInitiallyScrolled, 122 + hasInitiallyScrolled.value, 117 123 isAtBottom.value, 118 124 isAtTop.value, 119 125 isMomentumScrolling, 126 + keyboardIsOpening.value, 120 127 ], 121 128 ) 122 129 ··· 187 194 }) 188 195 }, [isMomentumScrolling]) 189 196 190 - // This is only used inside the useKeyboardHandler because the worklet won't work with a ref directly. 191 - const scrollToEndNow = React.useCallback(() => { 192 - flatListRef.current?.scrollToEnd({animated: false}) 193 - }, []) 197 + // -- Keyboard animation handling 198 + const animatedKeyboard = useAnimatedKeyboard() 199 + const {gtMobile} = useBreakpoints() 200 + const {bottom: bottomInset} = useSafeAreaInsets() 201 + const nativeBottomBarHeight = isIOS ? 42 : 60 202 + const bottomOffset = 203 + isWeb && gtMobile ? 0 : bottomInset + nativeBottomBarHeight 194 204 195 - useKeyboardHandler({ 196 - onMove: () => { 197 - 'worklet' 198 - runOnJS(scrollToEndNow)() 205 + // We need to keep track of when the keyboard is animating and when it isn't, since we want our `onContentSizeChanged` 206 + // callback to animate the scroll _only_ when the keyboard isn't animating. Any time the previous value of kb height 207 + // is different, we know that it is animating. When it finally settles, now will be equal to prev. 208 + useAnimatedReaction( 209 + () => animatedKeyboard.height.value, 210 + (now, prev) => { 211 + // This never applies on web 212 + if (isWeb) { 213 + keyboardIsOpening.value = false 214 + } else { 215 + keyboardIsOpening.value = now !== prev 216 + } 199 217 }, 200 - }) 218 + ) 219 + 220 + // This changes the size of the `ListFooterComponent`. Whenever this changes, the content size will change and our 221 + // `onContentSizeChange` function will handle scrolling to the appropriate offset. 222 + const animatedFooterStyle = useAnimatedStyle(() => ({ 223 + marginBottom: 224 + animatedKeyboard.height.value > bottomOffset 225 + ? animatedKeyboard.height.value 226 + : bottomOffset, 227 + })) 228 + 229 + // At a minimum we want the bottom to be whatever the height of our insets and bottom bar is. If the keyboard's height 230 + // is greater than that however, we use that value. 231 + const animatedInputStyle = useAnimatedStyle(() => ({ 232 + bottom: 233 + animatedKeyboard.height.value > bottomOffset 234 + ? animatedKeyboard.height.value 235 + : bottomOffset, 236 + })) 201 237 202 238 return ( 203 239 <> ··· 211 247 containWeb={true} 212 248 contentContainerStyle={[a.px_md]} 213 249 disableVirtualization={true} 214 - initialNumToRender={isNative ? 30 : 60} 215 - maxToRenderPerBatch={isWeb ? 30 : 60} 250 + // The extra two items account for the header and the footer components 251 + initialNumToRender={isNative ? 32 : 62} 252 + maxToRenderPerBatch={isWeb ? 32 : 62} 216 253 keyboardDismissMode="on-drag" 217 254 keyboardShouldPersistTaps="handled" 218 255 maintainVisibleContentPosition={{ ··· 227 264 ListHeaderComponent={ 228 265 <MaybeLoader isLoading={convo.isFetchingHistory} /> 229 266 } 267 + ListFooterComponent={<Animated.View style={[animatedFooterStyle]} />} 230 268 /> 231 269 </ScrollProvider> 232 - <MessageInput onSendMessage={onSendMessage} scrollToEnd={scrollToEnd} /> 270 + <Animated.View style={[a.relative, t.atoms.bg, animatedInputStyle]}> 271 + <MessageInput onSendMessage={onSendMessage} scrollToEnd={scrollToEnd} /> 272 + </Animated.View> 233 273 </> 234 274 ) 235 275 }
+28 -46
src/screens/Messages/Conversation/index.tsx
··· 1 1 import React, {useCallback} from 'react' 2 2 import {TouchableOpacity, View} from 'react-native' 3 - import {KeyboardProvider} from 'react-native-keyboard-controller' 4 - import {KeyboardAvoidingView} from 'react-native-keyboard-controller' 5 - import {useSafeAreaInsets} from 'react-native-safe-area-context' 6 3 import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api' 7 4 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 8 5 import {msg} from '@lingui/macro' ··· 18 15 import {useProfileQuery} from '#/state/queries/profile' 19 16 import {BACK_HITSLOP} from 'lib/constants' 20 17 import {sanitizeDisplayName} from 'lib/strings/display-names' 21 - import {isIOS, isNative, isWeb} from 'platform/detection' 18 + import {isWeb} from 'platform/detection' 22 19 import {ConvoProvider, isConvoActive, useConvo} from 'state/messages/convo' 23 20 import {ConvoStatus} from 'state/messages/convo/types' 24 21 import {useSetMinimalShellMode} from 'state/shell' ··· 39 36 > 40 37 export function MessagesConversationScreen({route}: Props) { 41 38 const gate = useGate() 42 - const setMinimalShellMode = useSetMinimalShellMode() 43 39 const {gtMobile} = useBreakpoints() 40 + const setMinimalShellMode = useSetMinimalShellMode() 44 41 45 42 const convoId = route.params.conversation 46 43 const {setCurrentConvoId} = useCurrentConvoId() ··· 57 54 setCurrentConvoId(undefined) 58 55 setMinimalShellMode(false) 59 56 } 60 - }, [convoId, gtMobile, setCurrentConvoId, setMinimalShellMode]), 57 + }, [gtMobile, convoId, setCurrentConvoId, setMinimalShellMode]), 61 58 ) 62 59 63 60 if (!gate('dms')) return <ClipClopGate /> ··· 76 73 77 74 const [hasInitiallyRendered, setHasInitiallyRendered] = React.useState(false) 78 75 79 - const {bottom: bottomInset, top: topInset} = useSafeAreaInsets() 80 - const nativeBottomBarHeight = isIOS ? 42 : 60 81 - 82 76 // HACK: Because we need to scroll to the bottom of the list once initial items are added to the list, we also have 83 77 // to take into account that scrolling to the end of the list on native will happen asynchronously. This will cause 84 78 // a little flicker when the items are first renedered at the top and immediately scrolled to the bottom. to prevent ··· 111 105 /* 112 106 * Any other convo states (atm) are "ready" states 113 107 */ 114 - 115 108 return ( 116 - <KeyboardProvider> 117 - <KeyboardAvoidingView 118 - style={[ 119 - a.flex_1, 120 - isNative && {marginBottom: bottomInset + nativeBottomBarHeight}, 121 - ]} 122 - keyboardVerticalOffset={isIOS ? topInset : 0} 123 - behavior="padding" 124 - contentContainerStyle={a.flex_1}> 125 - <CenteredView style={a.flex_1} sideBorders> 126 - <Header profile={convoState.recipients?.[0]} /> 127 - <View style={[a.flex_1]}> 128 - {isConvoActive(convoState) ? ( 129 - <MessagesList /> 130 - ) : ( 131 - <ListMaybePlaceholder isLoading /> 132 - )} 133 - {!hasInitiallyRendered && ( 134 - <View 135 - style={[ 136 - a.absolute, 137 - a.z_10, 138 - a.w_full, 139 - a.h_full, 140 - a.justify_center, 141 - a.align_center, 142 - t.atoms.bg, 143 - ]}> 144 - <View style={[{marginBottom: 75}]}> 145 - <Loader size="xl" /> 146 - </View> 147 - </View> 148 - )} 109 + <CenteredView style={[a.flex_1]} sideBorders> 110 + <Header profile={convoState.recipients?.[0]} /> 111 + <View style={[a.flex_1]}> 112 + {isConvoActive(convoState) ? ( 113 + <MessagesList /> 114 + ) : ( 115 + <ListMaybePlaceholder isLoading /> 116 + )} 117 + {!hasInitiallyRendered && ( 118 + <View 119 + style={[ 120 + a.absolute, 121 + a.z_10, 122 + a.w_full, 123 + a.h_full, 124 + a.justify_center, 125 + a.align_center, 126 + t.atoms.bg, 127 + ]}> 128 + <View style={[{marginBottom: 75}]}> 129 + <Loader size="xl" /> 130 + </View> 149 131 </View> 150 - </CenteredView> 151 - </KeyboardAvoidingView> 152 - </KeyboardProvider> 132 + )} 133 + </View> 134 + </CenteredView> 153 135 ) 154 136 } 155 137
-5
yarn.lock
··· 18496 18496 dependencies: 18497 18497 "@dominicstop/ts-event-emitter" "^1.1.0" 18498 18498 18499 - react-native-keyboard-controller@^1.11.7: 18500 - version "1.11.7" 18501 - resolved "https://registry.yarnpkg.com/react-native-keyboard-controller/-/react-native-keyboard-controller-1.11.7.tgz#85640374e4c3627c3b667256a1d308698ff80393" 18502 - integrity sha512-K2zlqVyWX4QO7r+dHMQgZT41G2dSEWtDYgBdht1WVyTaMQmwTMalZcHCWBVOnzyGaJq/hMKhF1kSPqJP1xqSFA== 18503 - 18504 18499 react-native-pager-view@6.2.3: 18505 18500 version "6.2.3" 18506 18501 resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-6.2.3.tgz#698f6387fdf06cecc3d8d4792604419cb89cb775"