forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
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}