mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React, {
2 forwardRef,
3 useCallback,
4 useRef,
5 useMemo,
6 useState,
7 ComponentProps,
8} from 'react'
9import {
10 NativeSyntheticEvent,
11 StyleSheet,
12 TextInput as RNTextInput,
13 TextInputSelectionChangeEventData,
14 View,
15} from 'react-native'
16import PasteInput, {
17 PastedFile,
18 PasteInputRef,
19} from '@mattermost/react-native-paste-input'
20import {AppBskyRichtextFacet, RichText} from '@atproto/api'
21import isEqual from 'lodash.isequal'
22import {Autocomplete} from './mobile/Autocomplete'
23import {Text} from 'view/com/util/text/Text'
24import {cleanError} from 'lib/strings/errors'
25import {getMentionAt, insertMentionAt} from 'lib/strings/mention-manip'
26import {usePalette} from 'lib/hooks/usePalette'
27import {useTheme} from 'lib/ThemeContext'
28import {isUriImage} from 'lib/media/util'
29import {downloadAndResize} from 'lib/media/manip'
30import {POST_IMG_MAX} from 'lib/constants'
31
32export interface TextInputRef {
33 focus: () => void
34 blur: () => void
35}
36
37interface TextInputProps extends ComponentProps<typeof RNTextInput> {
38 richtext: RichText
39 placeholder: string
40 suggestedLinks: Set<string>
41 setRichText: (v: RichText | ((v: RichText) => RichText)) => void
42 onPhotoPasted: (uri: string) => void
43 onPressPublish: (richtext: RichText) => Promise<void>
44 onSuggestedLinksChanged: (uris: Set<string>) => void
45 onError: (err: string) => void
46}
47
48interface Selection {
49 start: number
50 end: number
51}
52
53export const TextInput = forwardRef(function TextInputImpl(
54 {
55 richtext,
56 placeholder,
57 suggestedLinks,
58 setRichText,
59 onPhotoPasted,
60 onSuggestedLinksChanged,
61 onError,
62 ...props
63 }: TextInputProps,
64 ref,
65) {
66 const pal = usePalette('default')
67 const textInput = useRef<PasteInputRef>(null)
68 const textInputSelection = useRef<Selection>({start: 0, end: 0})
69 const theme = useTheme()
70 const [autocompletePrefix, setAutocompletePrefix] = useState('')
71
72 React.useImperativeHandle(ref, () => ({
73 focus: () => textInput.current?.focus(),
74 blur: () => {
75 textInput.current?.blur()
76 },
77 }))
78
79 const onChangeText = useCallback(
80 (newText: string) => {
81 /*
82 * This is a hack to bump the rendering of our styled
83 * `textDecorated` to _after_ whatever processing is happening
84 * within the `PasteInput` library. Without this, the elements in
85 * `textDecorated` are not correctly painted to screen.
86 *
87 * NB: we tried a `0` timeout as well, but only positive values worked.
88 *
89 * @see https://github.com/bluesky-social/social-app/issues/929
90 */
91 setTimeout(async () => {
92 const newRt = new RichText({text: newText})
93 newRt.detectFacetsWithoutResolution()
94 setRichText(newRt)
95
96 const prefix = getMentionAt(
97 newText,
98 textInputSelection.current?.start || 0,
99 )
100 if (prefix) {
101 setAutocompletePrefix(prefix.value)
102 } else if (autocompletePrefix) {
103 setAutocompletePrefix('')
104 }
105
106 const set: Set<string> = new Set()
107
108 if (newRt.facets) {
109 for (const facet of newRt.facets) {
110 for (const feature of facet.features) {
111 if (AppBskyRichtextFacet.isLink(feature)) {
112 if (isUriImage(feature.uri)) {
113 const res = await downloadAndResize({
114 uri: feature.uri,
115 width: POST_IMG_MAX.width,
116 height: POST_IMG_MAX.height,
117 mode: 'contain',
118 maxSize: POST_IMG_MAX.size,
119 timeout: 15e3,
120 })
121
122 if (res !== undefined) {
123 onPhotoPasted(res.path)
124 }
125 } else {
126 set.add(feature.uri)
127 }
128 }
129 }
130 }
131 }
132
133 if (!isEqual(set, suggestedLinks)) {
134 onSuggestedLinksChanged(set)
135 }
136 }, 1)
137 },
138 [
139 setRichText,
140 autocompletePrefix,
141 setAutocompletePrefix,
142 suggestedLinks,
143 onSuggestedLinksChanged,
144 onPhotoPasted,
145 ],
146 )
147
148 const onPaste = useCallback(
149 async (err: string | undefined, files: PastedFile[]) => {
150 if (err) {
151 return onError(cleanError(err))
152 }
153
154 const uris = files.map(f => f.uri)
155 const uri = uris.find(isUriImage)
156
157 if (uri) {
158 onPhotoPasted(uri)
159 }
160 },
161 [onError, onPhotoPasted],
162 )
163
164 const onSelectionChange = useCallback(
165 (evt: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => {
166 // NOTE we track the input selection using a ref to avoid excessive renders -prf
167 textInputSelection.current = evt.nativeEvent.selection
168 },
169 [textInputSelection],
170 )
171
172 const onSelectAutocompleteItem = useCallback(
173 (item: string) => {
174 onChangeText(
175 insertMentionAt(
176 richtext.text,
177 textInputSelection.current?.start || 0,
178 item,
179 ),
180 )
181 setAutocompletePrefix('')
182 },
183 [onChangeText, richtext, setAutocompletePrefix],
184 )
185
186 const textDecorated = useMemo(() => {
187 let i = 0
188
189 return Array.from(richtext.segments()).map(segment => {
190 const isTag = AppBskyRichtextFacet.isTag(segment.facet?.features?.[0])
191 return (
192 <Text
193 key={i++}
194 style={[
195 segment.facet && !isTag ? pal.link : pal.text,
196 styles.textInputFormatting,
197 ]}>
198 {segment.text}
199 </Text>
200 )
201 })
202 }, [richtext, pal.link, pal.text])
203
204 return (
205 <View style={styles.container}>
206 <PasteInput
207 testID="composerTextInput"
208 ref={textInput}
209 onChangeText={onChangeText}
210 onPaste={onPaste}
211 onSelectionChange={onSelectionChange}
212 placeholder={placeholder}
213 placeholderTextColor={pal.colors.textLight}
214 keyboardAppearance={theme.colorScheme}
215 autoFocus={true}
216 allowFontScaling
217 multiline
218 style={[pal.text, styles.textInput, styles.textInputFormatting]}
219 {...props}>
220 {textDecorated}
221 </PasteInput>
222 <Autocomplete
223 prefix={autocompletePrefix}
224 onSelect={onSelectAutocompleteItem}
225 />
226 </View>
227 )
228})
229
230const styles = StyleSheet.create({
231 container: {
232 flex: 1,
233 },
234 textInput: {
235 flex: 1,
236 width: '100%',
237 padding: 5,
238 paddingBottom: 20,
239 marginLeft: 8,
240 alignSelf: 'flex-start',
241 },
242 textInputFormatting: {
243 fontSize: 18,
244 letterSpacing: 0.2,
245 fontWeight: '400',
246 lineHeight: 23.4, // 1.3*16
247 },
248})