mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
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}