mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at react-sdui 264 lines 7.3 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 (newText: string) => { 89 /* 90 * This is a hack to bump the rendering of our styled 91 * `textDecorated` to _after_ whatever processing is happening 92 * within the `PasteInput` library. Without this, the elements in 93 * `textDecorated` are not correctly painted to screen. 94 * 95 * NB: we tried a `0` timeout as well, but only positive values worked. 96 * 97 * @see https://github.com/bluesky-social/social-app/issues/929 98 */ 99 setTimeout(async () => { 100 const mayBePaste = newText.length > prevLength.current + 1 101 102 const newRt = new RichText({text: newText}) 103 newRt.detectFacetsWithoutResolution() 104 setRichText(newRt) 105 106 const prefix = getMentionAt( 107 newText, 108 textInputSelection.current?.start || 0, 109 ) 110 if (prefix) { 111 setAutocompletePrefix(prefix.value) 112 } else if (autocompletePrefix) { 113 setAutocompletePrefix('') 114 } 115 116 const nextDetectedUris = new Map<string, LinkFacetMatch>() 117 if (newRt.facets) { 118 for (const facet of newRt.facets) { 119 for (const feature of facet.features) { 120 if (AppBskyRichtextFacet.isLink(feature)) { 121 if (isUriImage(feature.uri)) { 122 const res = await downloadAndResize({ 123 uri: feature.uri, 124 width: POST_IMG_MAX.width, 125 height: POST_IMG_MAX.height, 126 mode: 'contain', 127 maxSize: POST_IMG_MAX.size, 128 timeout: 15e3, 129 }) 130 131 if (res !== undefined) { 132 onPhotoPasted(res.path) 133 } 134 } else { 135 nextDetectedUris.set(feature.uri, {facet, rt: newRt}) 136 } 137 } 138 } 139 } 140 } 141 const suggestedUri = suggestLinkCardUri( 142 mayBePaste, 143 nextDetectedUris, 144 prevDetectedUris.current, 145 pastSuggestedUris.current, 146 ) 147 prevDetectedUris.current = nextDetectedUris 148 if (suggestedUri) { 149 onNewLink(suggestedUri) 150 } 151 prevLength.current = newText.length 152 }, 1) 153 }, 154 [setRichText, autocompletePrefix, onPhotoPasted, onNewLink], 155 ) 156 157 const onPaste = useCallback( 158 async (err: string | undefined, files: PastedFile[]) => { 159 if (err) { 160 return onError(cleanError(err)) 161 } 162 163 const uris = files.map(f => f.uri) 164 const uri = uris.find(isUriImage) 165 166 if (uri) { 167 onPhotoPasted(uri) 168 } 169 }, 170 [onError, onPhotoPasted], 171 ) 172 173 const onSelectionChange = useCallback( 174 (evt: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => { 175 // NOTE we track the input selection using a ref to avoid excessive renders -prf 176 textInputSelection.current = evt.nativeEvent.selection 177 }, 178 [textInputSelection], 179 ) 180 181 const onSelectAutocompleteItem = useCallback( 182 (item: string) => { 183 onChangeText( 184 insertMentionAt( 185 richtext.text, 186 textInputSelection.current?.start || 0, 187 item, 188 ), 189 ) 190 setAutocompletePrefix('') 191 }, 192 [onChangeText, richtext, setAutocompletePrefix], 193 ) 194 195 const textDecorated = useMemo(() => { 196 let i = 0 197 198 return Array.from(richtext.segments()).map(segment => { 199 return ( 200 <Text 201 key={i++} 202 style={[ 203 segment.facet ? pal.link : pal.text, 204 styles.textInputFormatting, 205 ]}> 206 {segment.text} 207 </Text> 208 ) 209 }) 210 }, [richtext, pal.link, pal.text]) 211 212 return ( 213 <View style={styles.container}> 214 <PasteInput 215 testID="composerTextInput" 216 ref={textInput} 217 onChangeText={onChangeText} 218 onPaste={onPaste} 219 onSelectionChange={onSelectionChange} 220 placeholder={placeholder} 221 placeholderTextColor={pal.colors.textLight} 222 keyboardAppearance={theme.colorScheme} 223 autoFocus={true} 224 allowFontScaling 225 multiline 226 scrollEnabled={false} 227 numberOfLines={4} 228 style={[ 229 pal.text, 230 styles.textInput, 231 styles.textInputFormatting, 232 {textAlignVertical: 'top'}, 233 ]} 234 {...props}> 235 {textDecorated} 236 </PasteInput> 237 <Autocomplete 238 prefix={autocompletePrefix} 239 onSelect={onSelectAutocompleteItem} 240 /> 241 </View> 242 ) 243}) 244 245const styles = StyleSheet.create({ 246 container: { 247 flex: 1, 248 }, 249 textInput: { 250 flex: 1, 251 width: '100%', 252 padding: 5, 253 paddingBottom: 20, 254 marginLeft: 8, 255 alignSelf: 'flex-start', 256 }, 257 textInputFormatting: { 258 fontSize: 18, 259 letterSpacing: 0.2, 260 fontWeight: '400', 261 // This is broken on ios right now, so don't set it there. 262 lineHeight: isIOS ? undefined : 23.4, // 1.3*16 263 }, 264})