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