import React, { forwardRef, useCallback, useRef, useMemo, ComponentProps, } from 'react' import { NativeSyntheticEvent, StyleSheet, TextInput as RNTextInput, TextInputSelectionChangeEventData, View, } from 'react-native' import PasteInput, { PastedFile, PasteInputRef, } from '@mattermost/react-native-paste-input' import {AppBskyRichtextFacet, RichText} from '@atproto/api' import isEqual from 'lodash.isequal' import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete' import {Autocomplete} from './mobile/Autocomplete' import {Text} from 'view/com/util/text/Text' import {cleanError} from 'lib/strings/errors' import {getMentionAt, insertMentionAt} from 'lib/strings/mention-manip' import {usePalette} from 'lib/hooks/usePalette' import {useTheme} from 'lib/ThemeContext' import {isUriImage} from 'lib/media/util' import {downloadAndResize} from 'lib/media/manip' import {POST_IMG_MAX} from 'lib/constants' export interface TextInputRef { focus: () => void blur: () => void } interface TextInputProps extends ComponentProps { richtext: RichText placeholder: string suggestedLinks: Set autocompleteView: UserAutocompleteModel setRichText: (v: RichText | ((v: RichText) => RichText)) => void onPhotoPasted: (uri: string) => void onPressPublish: (richtext: RichText) => Promise onSuggestedLinksChanged: (uris: Set) => void onError: (err: string) => void } interface Selection { start: number end: number } export const TextInput = forwardRef( ( { richtext, placeholder, suggestedLinks, autocompleteView, setRichText, onPhotoPasted, onSuggestedLinksChanged, onError, ...props }: TextInputProps, ref, ) => { const pal = usePalette('default') const textInput = useRef(null) const textInputSelection = useRef({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 = 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) } }, [onError, onPhotoPasted], ) const onSelectionChange = useCallback( (evt: NativeSyntheticEvent) => { // 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 => ( {segment.text} )) }, [richtext, pal.link, pal.text]) return ( {textDecorated} ) }, ) const styles = StyleSheet.create({ container: { flex: 1, }, textInput: { flex: 1, width: '100%', padding: 5, paddingBottom: 20, marginLeft: 8, alignSelf: 'flex-start', }, textInputFormatting: { fontSize: 18, letterSpacing: 0.2, fontWeight: '400', lineHeight: 23.4, // 1.3*16 }, })