mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at remove-hackfix 208 lines 6.5 kB view raw
1import {useCallback, useState} from 'react' 2import {Pressable, TextInput, useWindowDimensions, View} from 'react-native' 3import { 4 useFocusedInputHandler, 5 useReanimatedKeyboardAnimation, 6} from 'react-native-keyboard-controller' 7import Animated, { 8 measure, 9 useAnimatedProps, 10 useAnimatedRef, 11 useAnimatedStyle, 12 useSharedValue, 13} from 'react-native-reanimated' 14import {useSafeAreaInsets} from 'react-native-safe-area-context' 15import {msg} from '@lingui/macro' 16import {useLingui} from '@lingui/react' 17import Graphemer from 'graphemer' 18 19import {HITSLOP_10, MAX_DM_GRAPHEME_LENGTH} from '#/lib/constants' 20import {useHaptics} from '#/lib/haptics' 21import {isIOS, isWeb} from '#/platform/detection' 22import {useEmail} from '#/state/email-verification' 23import { 24 useMessageDraft, 25 useSaveMessageDraft, 26} from '#/state/messages/message-drafts' 27import {type EmojiPickerPosition} from '#/view/com/composer/text-input/web/EmojiPicker' 28import * as Toast from '#/view/com/util/Toast' 29import {android, atoms as a, useTheme} from '#/alf' 30import {useSharedInputStyles} from '#/components/forms/TextField' 31import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane' 32import {useExtractEmbedFromFacets} from './MessageInputEmbed' 33 34const AnimatedTextInput = Animated.createAnimatedComponent(TextInput) 35 36export function MessageInput({ 37 onSendMessage, 38 hasEmbed, 39 setEmbed, 40 children, 41}: { 42 onSendMessage: (message: string) => void 43 hasEmbed: boolean 44 setEmbed: (embedUrl: string | undefined) => void 45 children?: React.ReactNode 46 openEmojiPicker?: (pos: EmojiPickerPosition) => void 47}) { 48 const {_} = useLingui() 49 const t = useTheme() 50 const playHaptic = useHaptics() 51 const {getDraft, clearDraft} = useMessageDraft() 52 53 // Input layout 54 const {top: topInset} = useSafeAreaInsets() 55 const {height: windowHeight} = useWindowDimensions() 56 const {height: keyboardHeight} = useReanimatedKeyboardAnimation() 57 const maxHeight = useSharedValue<undefined | number>(undefined) 58 const isInputScrollable = useSharedValue(false) 59 60 const inputStyles = useSharedInputStyles() 61 const [isFocused, setIsFocused] = useState(false) 62 const [message, setMessage] = useState(getDraft) 63 const inputRef = useAnimatedRef<TextInput>() 64 const [shouldEnforceClear, setShouldEnforceClear] = useState(false) 65 66 const {needsEmailVerification} = useEmail() 67 68 useSaveMessageDraft(message) 69 useExtractEmbedFromFacets(message, setEmbed) 70 71 const onSubmit = useCallback(() => { 72 if (needsEmailVerification) { 73 return 74 } 75 if (!hasEmbed && message.trim() === '') { 76 return 77 } 78 if (new Graphemer().countGraphemes(message) > MAX_DM_GRAPHEME_LENGTH) { 79 Toast.show(_(msg`Message is too long`), 'xmark') 80 return 81 } 82 clearDraft() 83 onSendMessage(message) 84 playHaptic() 85 setEmbed(undefined) 86 setMessage('') 87 if (isIOS) { 88 setShouldEnforceClear(true) 89 } 90 if (isWeb) { 91 // Pressing the send button causes the text input to lose focus, so we need to 92 // re-focus it after sending 93 setTimeout(() => { 94 inputRef.current?.focus() 95 }, 100) 96 } 97 }, [ 98 needsEmailVerification, 99 hasEmbed, 100 message, 101 clearDraft, 102 onSendMessage, 103 playHaptic, 104 setEmbed, 105 inputRef, 106 _, 107 ]) 108 109 useFocusedInputHandler( 110 { 111 onChangeText: () => { 112 'worklet' 113 const measurement = measure(inputRef) 114 if (!measurement) return 115 116 const max = windowHeight - -keyboardHeight.get() - topInset - 150 117 const availableSpace = max - measurement.height 118 119 maxHeight.set(max) 120 isInputScrollable.set(availableSpace < 30) 121 }, 122 }, 123 [windowHeight, topInset], 124 ) 125 126 const animatedStyle = useAnimatedStyle(() => ({ 127 maxHeight: maxHeight.get(), 128 })) 129 130 const animatedProps = useAnimatedProps(() => ({ 131 scrollEnabled: isInputScrollable.get(), 132 })) 133 134 return ( 135 <View style={[a.px_md, a.pb_sm, a.pt_xs]}> 136 {children} 137 <View 138 style={[ 139 a.w_full, 140 a.flex_row, 141 t.atoms.bg_contrast_25, 142 { 143 padding: a.p_sm.padding - 2, 144 paddingLeft: a.p_md.padding - 2, 145 borderWidth: 1, 146 borderRadius: 23, 147 borderColor: 'transparent', 148 }, 149 isFocused && inputStyles.chromeFocus, 150 ]}> 151 <AnimatedTextInput 152 accessibilityLabel={_(msg`Message input field`)} 153 accessibilityHint={_(msg`Type your message here`)} 154 placeholder={_(msg`Write a message`)} 155 placeholderTextColor={t.palette.contrast_500} 156 value={message} 157 onChange={evt => { 158 // bit of a hack: iOS automatically accepts autocomplete suggestions when you tap anywhere on the screen 159 // including the button we just pressed - and this overrides clearing the input! so we watch for the 160 // next change and double make sure the input is cleared. It should *always* send an onChange event after 161 // clearing via setMessage('') that happens in onSubmit() 162 // -sfn 163 if (isIOS && shouldEnforceClear) { 164 setShouldEnforceClear(false) 165 setMessage('') 166 return 167 } 168 const text = evt.nativeEvent.text 169 setMessage(text) 170 }} 171 multiline={true} 172 style={[ 173 a.flex_1, 174 a.text_md, 175 a.px_sm, 176 t.atoms.text, 177 android({paddingTop: 0}), 178 {paddingBottom: isIOS ? 5 : 0}, 179 animatedStyle, 180 ]} 181 keyboardAppearance={t.scheme} 182 submitBehavior="newline" 183 onFocus={() => setIsFocused(true)} 184 onBlur={() => setIsFocused(false)} 185 ref={inputRef} 186 hitSlop={HITSLOP_10} 187 animatedProps={animatedProps} 188 editable={!needsEmailVerification} 189 /> 190 <Pressable 191 accessibilityRole="button" 192 accessibilityLabel={_(msg`Send message`)} 193 accessibilityHint="" 194 hitSlop={HITSLOP_10} 195 style={[ 196 a.rounded_full, 197 a.align_center, 198 a.justify_center, 199 {height: 30, width: 30, backgroundColor: t.palette.primary_500}, 200 ]} 201 onPress={onSubmit} 202 disabled={needsEmailVerification}> 203 <PaperPlane fill={t.palette.white} style={[a.relative, {left: 1}]} /> 204 </Pressable> 205 </View> 206 </View> 207 ) 208}