Bluesky app fork with some witchin' additions 💫 forked from jollywhoppers.com/witchsky.app
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260import { useCallback, useImperativeHandle, useMemo, useRef, useState,} from 'react'import { type NativeSyntheticEvent, Text as RNText, type TextInputSelectionChangeEventData, View,} from 'react-native'import {AppBskyRichtextFacet, RichText} from '@atproto/api'import PasteInput, { type PastedFile, type PasteInputRef, // @ts-expect-error no types when installing from github} from '@mattermost/react-native-paste-input'
import {POST_IMG_MAX} from '#/lib/constants'import {downloadAndResize} from '#/lib/media/manip'import {isUriImage} from '#/lib/media/util'import {cleanError} from '#/lib/strings/errors'import {getMentionAt, insertMentionAt} from '#/lib/strings/mention-manip'import {useTheme} from '#/lib/ThemeContext'import {isAndroid, isNative} from '#/platform/detection'import { type LinkFacetMatch, suggestLinkCardUri,} from '#/view/com/composer/text-input/text-input-util'import {atoms as a, useAlf} from '#/alf'import {normalizeTextStyles} from '#/alf/typography'import {Autocomplete} from './mobile/Autocomplete'import {type TextInputProps} from './TextInput.types'
interface Selection { start: number end: number}
export function TextInput({ ref, richtext, placeholder, hasRightPadding, setRichText, onPhotoPasted, onNewLink, onError, ...props}: TextInputProps) { const {theme: t, fonts} = useAlf() const textInput = useRef<PasteInputRef>(null) const textInputSelection = useRef<Selection>({start: 0, end: 0}) const theme = useTheme() const [autocompletePrefix, setAutocompletePrefix] = useState('') const prevLength = useRef(richtext.length)
useImperativeHandle(ref, () => ({ focus: () => textInput.current?.focus(), blur: () => { textInput.current?.blur() }, getCursorPosition: () => undefined, // Not implemented on native maybeClosePopup: () => false, // Not needed on native }))
const pastSuggestedUris = useRef(new Set<string>()) const prevDetectedUris = useRef(new Map<string, LinkFacetMatch>()) const onChangeText = useCallback( async (newText: string) => { const mayBePaste = newText.length > prevLength.current + 1
const newRt = new RichText({text: newText}) newRt.detectFacetsWithoutResolution() setRichText(newRt)
// NOTE: BinaryFiddler // onChangeText happens before onSelectionChange, cursorPos is out of bound if the user deletes characters, const cursorPos = textInputSelection.current?.start ?? 0 const prefix = getMentionAt(newText, Math.min(cursorPos, newText.length))
if (prefix) { setAutocompletePrefix(prefix.value) } else if (autocompletePrefix) { setAutocompletePrefix('') }
const nextDetectedUris = new Map<string, LinkFacetMatch>() 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 { nextDetectedUris.set(feature.uri, {facet, rt: newRt}) } } } } } const suggestedUri = suggestLinkCardUri( mayBePaste, nextDetectedUris, prevDetectedUris.current, pastSuggestedUris.current, ) prevDetectedUris.current = nextDetectedUris if (suggestedUri) { onNewLink(suggestedUri) } prevLength.current = newText.length }, [setRichText, autocompletePrefix, onPhotoPasted, onNewLink], )
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, ), ) setAutocompletePrefix('') }, [onChangeText, richtext, setAutocompletePrefix], )
const inputTextStyle = useMemo(() => { const style = normalizeTextStyles( [a.text_lg, a.leading_snug, t.atoms.text], { fontScale: fonts.scaleMultiplier, fontFamily: fonts.family, flags: {}, }, )
/** * PasteInput doesn't like `lineHeight`, results in jumpiness */ if (isNative) { style.lineHeight = undefined }
/* * Android impl of `PasteInput` doesn't support the array syntax for `fontVariant` */ if (isAndroid) { // @ts-ignore style.fontVariant = style.fontVariant ? style.fontVariant.join(' ') : undefined } return style }, [t, fonts])
const textDecorated = useMemo(() => { let i = 0
return Array.from(richtext.segments()).map(segment => { return ( <RNText key={i++} style={[ inputTextStyle, { color: segment.facet ? t.palette.primary_500 : t.atoms.text.color, marginTop: -1, }, ]}> {segment.text} </RNText> ) }) }, [t, richtext, inputTextStyle])
return ( <View style={[a.flex_1, a.pl_md, hasRightPadding && a.pr_4xl]}> <PasteInput testID="composerTextInput" ref={textInput} onChangeText={onChangeText} onPaste={onPaste} onSelectionChange={onSelectionChange} placeholder={placeholder} placeholderTextColor={t.atoms.text_contrast_medium.color} keyboardAppearance={theme.colorScheme} autoFocus={true} allowFontScaling multiline scrollEnabled={false} numberOfLines={2} // Note: should be the default value, but as of v1.104 // it switched to "none" on Android autoCapitalize="sentences" {...props} style={[ inputTextStyle, a.w_full, !autocompletePrefix && a.h_full, { textAlignVertical: 'top', minHeight: 60, includeFontPadding: false, }, { borderWidth: 1, borderColor: 'transparent', }, props.style, ]}> {textDecorated} </PasteInput> <Autocomplete prefix={autocompletePrefix} onSelect={onSelectAutocompleteItem} /> </View> )}