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