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