diff options
Diffstat (limited to 'src/view/com/composer/text-input/TextInput.tsx')
-rw-r--r-- | src/view/com/composer/text-input/TextInput.tsx | 330 |
1 files changed, 164 insertions, 166 deletions
diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx index 32fdb4aa2..c5d094ea5 100644 --- a/src/view/com/composer/text-input/TextInput.tsx +++ b/src/view/com/composer/text-input/TextInput.tsx @@ -51,181 +51,179 @@ interface Selection { end: number } -export const TextInput = forwardRef( - ( - { - richtext, - placeholder, - suggestedLinks, - autocompleteView, - setRichText, - onPhotoPasted, - onSuggestedLinksChanged, - onError, - ...props - }: TextInputProps, - ref, - ) => { - const pal = usePalette('default') - const textInput = useRef<PasteInputRef>(null) - const textInputSelection = useRef<Selection>({start: 0, end: 0}) - const theme = useTheme() - - React.useImperativeHandle(ref, () => ({ - focus: () => textInput.current?.focus(), - blur: () => { - textInput.current?.blur() - }, - })) - - const onChangeText = useCallback( - (newText: string) => { - /* - * This is a hack to bump the rendering of our styled - * `textDecorated` to _after_ whatever processing is happening - * within the `PasteInput` library. Without this, the elements in - * `textDecorated` are not correctly painted to screen. - * - * NB: we tried a `0` timeout as well, but only positive values worked. - * - * @see https://github.com/bluesky-social/social-app/issues/929 - */ - setTimeout(async () => { - const newRt = new RichText({text: newText}) - newRt.detectFacetsWithoutResolution() - setRichText(newRt) - - const prefix = getMentionAt( - newText, - textInputSelection.current?.start || 0, - ) - if (prefix) { - autocompleteView.setActive(true) - autocompleteView.setPrefix(prefix.value) - } else { - autocompleteView.setActive(false) - } +export const TextInput = forwardRef(function TextInputImpl( + { + richtext, + placeholder, + suggestedLinks, + autocompleteView, + setRichText, + onPhotoPasted, + onSuggestedLinksChanged, + onError, + ...props + }: TextInputProps, + ref, +) { + const pal = usePalette('default') + const textInput = useRef<PasteInputRef>(null) + const textInputSelection = useRef<Selection>({start: 0, end: 0}) + const theme = useTheme() + + React.useImperativeHandle(ref, () => ({ + focus: () => textInput.current?.focus(), + blur: () => { + textInput.current?.blur() + }, + })) + + const onChangeText = useCallback( + (newText: string) => { + /* + * This is a hack to bump the rendering of our styled + * `textDecorated` to _after_ whatever processing is happening + * within the `PasteInput` library. Without this, the elements in + * `textDecorated` are not correctly painted to screen. + * + * NB: we tried a `0` timeout as well, but only positive values worked. + * + * @see https://github.com/bluesky-social/social-app/issues/929 + */ + setTimeout(async () => { + const newRt = new RichText({text: newText}) + newRt.detectFacetsWithoutResolution() + setRichText(newRt) + + const prefix = getMentionAt( + newText, + textInputSelection.current?.start || 0, + ) + if (prefix) { + autocompleteView.setActive(true) + autocompleteView.setPrefix(prefix.value) + } else { + autocompleteView.setActive(false) + } - const set: Set<string> = new Set() - - if (newRt.facets) { - for (const facet of newRt.facets) { - for (const feature of facet.features) { - if (AppBskyRichtextFacet.isLink(feature)) { - if (isUriImage(feature.uri)) { - const res = await downloadAndResize({ - uri: feature.uri, - width: POST_IMG_MAX.width, - height: POST_IMG_MAX.height, - mode: 'contain', - maxSize: POST_IMG_MAX.size, - timeout: 15e3, - }) - - if (res !== undefined) { - onPhotoPasted(res.path) - } - } else { - set.add(feature.uri) + const set: Set<string> = new Set() + + if (newRt.facets) { + for (const facet of newRt.facets) { + for (const feature of facet.features) { + if (AppBskyRichtextFacet.isLink(feature)) { + if (isUriImage(feature.uri)) { + const res = await downloadAndResize({ + uri: feature.uri, + width: POST_IMG_MAX.width, + height: POST_IMG_MAX.height, + mode: 'contain', + maxSize: POST_IMG_MAX.size, + timeout: 15e3, + }) + + if (res !== undefined) { + onPhotoPasted(res.path) } + } else { + set.add(feature.uri) } } } } - - if (!isEqual(set, suggestedLinks)) { - onSuggestedLinksChanged(set) - } - }, 1) - }, - [ - setRichText, - autocompleteView, - suggestedLinks, - onSuggestedLinksChanged, - onPhotoPasted, - ], - ) - - const onPaste = useCallback( - async (err: string | undefined, files: PastedFile[]) => { - if (err) { - return onError(cleanError(err)) } - const uris = files.map(f => f.uri) - const uri = uris.find(isUriImage) - - if (uri) { - onPhotoPasted(uri) + if (!isEqual(set, suggestedLinks)) { + onSuggestedLinksChanged(set) } - }, - [onError, onPhotoPasted], - ) - - const onSelectionChange = useCallback( - (evt: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => { - // NOTE we track the input selection using a ref to avoid excessive renders -prf - textInputSelection.current = evt.nativeEvent.selection - }, - [textInputSelection], - ) - - const onSelectAutocompleteItem = useCallback( - (item: string) => { - onChangeText( - insertMentionAt( - richtext.text, - textInputSelection.current?.start || 0, - item, - ), - ) - autocompleteView.setActive(false) - }, - [onChangeText, richtext, autocompleteView], - ) - - const textDecorated = useMemo(() => { - let i = 0 - - return Array.from(richtext.segments()).map(segment => ( - <Text - key={i++} - style={[ - !segment.facet ? pal.text : pal.link, - styles.textInputFormatting, - ]}> - {segment.text} - </Text> - )) - }, [richtext, pal.link, pal.text]) - - return ( - <View style={styles.container}> - <PasteInput - testID="composerTextInput" - ref={textInput} - onChangeText={onChangeText} - onPaste={onPaste} - onSelectionChange={onSelectionChange} - placeholder={placeholder} - placeholderTextColor={pal.colors.textLight} - keyboardAppearance={theme.colorScheme} - autoFocus={true} - allowFontScaling - multiline - style={[pal.text, styles.textInput, styles.textInputFormatting]} - {...props}> - {textDecorated} - </PasteInput> - <Autocomplete - view={autocompleteView} - onSelect={onSelectAutocompleteItem} - /> - </View> - ) - }, -) + }, 1) + }, + [ + setRichText, + autocompleteView, + suggestedLinks, + onSuggestedLinksChanged, + onPhotoPasted, + ], + ) + + const onPaste = useCallback( + async (err: string | undefined, files: PastedFile[]) => { + if (err) { + return onError(cleanError(err)) + } + + const uris = files.map(f => f.uri) + const uri = uris.find(isUriImage) + + if (uri) { + onPhotoPasted(uri) + } + }, + [onError, onPhotoPasted], + ) + + const onSelectionChange = useCallback( + (evt: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => { + // NOTE we track the input selection using a ref to avoid excessive renders -prf + textInputSelection.current = evt.nativeEvent.selection + }, + [textInputSelection], + ) + + const onSelectAutocompleteItem = useCallback( + (item: string) => { + onChangeText( + insertMentionAt( + richtext.text, + textInputSelection.current?.start || 0, + item, + ), + ) + autocompleteView.setActive(false) + }, + [onChangeText, richtext, autocompleteView], + ) + + const textDecorated = useMemo(() => { + let i = 0 + + return Array.from(richtext.segments()).map(segment => ( + <Text + key={i++} + style={[ + !segment.facet ? pal.text : pal.link, + styles.textInputFormatting, + ]}> + {segment.text} + </Text> + )) + }, [richtext, pal.link, pal.text]) + + return ( + <View style={styles.container}> + <PasteInput + testID="composerTextInput" + ref={textInput} + onChangeText={onChangeText} + onPaste={onPaste} + onSelectionChange={onSelectionChange} + placeholder={placeholder} + placeholderTextColor={pal.colors.textLight} + keyboardAppearance={theme.colorScheme} + autoFocus={true} + allowFontScaling + multiline + style={[pal.text, styles.textInput, styles.textInputFormatting]} + {...props}> + {textDecorated} + </PasteInput> + <Autocomplete + view={autocompleteView} + onSelect={onSelectAutocompleteItem} + /> + </View> + ) +}) const styles = StyleSheet.create({ container: { |