diff options
author | dan <dan.abramov@gmail.com> | 2023-09-08 00:38:57 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-09-07 16:38:57 -0700 |
commit | a5b89dffa6713bb06c1c572bbdc00517cf5e9bc5 (patch) | |
tree | ac8aa2deec7cc24d9a3553bab0c48335d2e8677c /src/view/com/composer/text-input | |
parent | 00595591c46db6ebfa9a8ee404f275b43493f7e0 (diff) | |
download | voidsky-a5b89dffa6713bb06c1c572bbdc00517cf5e9bc5.tar.zst |
Add ESLint React plugin (#1412)
* Add eslint-plugin-react * Enable display name rule
Diffstat (limited to 'src/view/com/composer/text-input')
-rw-r--r-- | src/view/com/composer/text-input/TextInput.tsx | 330 | ||||
-rw-r--r-- | src/view/com/composer/text-input/TextInput.web.tsx | 219 | ||||
-rw-r--r-- | src/view/com/composer/text-input/web/Autocomplete.tsx | 2 |
3 files changed, 272 insertions, 279 deletions
diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx index 32fdb4aa2..c5d094ea5 100644 --- a/src/view/com/composer/text-input/TextInput.tsx +++ b/src/view/com/composer/text-input/TextInput.tsx @@ -51,181 +51,179 @@ interface Selection { end: number } -export const TextInput = forwardRef( - ( - { - richtext, - placeholder, - suggestedLinks, - autocompleteView, - setRichText, - onPhotoPasted, - onSuggestedLinksChanged, - onError, - ...props - }: TextInputProps, - ref, - ) => { - const pal = usePalette('default') - const textInput = useRef<PasteInputRef>(null) - const textInputSelection = useRef<Selection>({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) - } +export const TextInput = forwardRef(function TextInputImpl( + { + richtext, + placeholder, + suggestedLinks, + autocompleteView, + setRichText, + onPhotoPasted, + onSuggestedLinksChanged, + onError, + ...props + }: TextInputProps, + ref, +) { + const pal = usePalette('default') + const textInput = useRef<PasteInputRef>(null) + const textInputSelection = useRef<Selection>({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<string> = 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) + const set: Set<string> = 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) + if (!isEqual(set, suggestedLinks)) { + onSuggestedLinksChanged(set) } - }, - [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, - ), - ) - autocompleteView.setActive(false) - }, - [onChangeText, richtext, autocompleteView], - ) - - const textDecorated = useMemo(() => { - let i = 0 - - return Array.from(richtext.segments()).map(segment => ( - <Text - key={i++} - style={[ - !segment.facet ? pal.text : pal.link, - styles.textInputFormatting, - ]}> - {segment.text} - </Text> - )) - }, [richtext, pal.link, pal.text]) - - return ( - <View style={styles.container}> - <PasteInput - testID="composerTextInput" - ref={textInput} - onChangeText={onChangeText} - onPaste={onPaste} - onSelectionChange={onSelectionChange} - placeholder={placeholder} - placeholderTextColor={pal.colors.textLight} - keyboardAppearance={theme.colorScheme} - autoFocus={true} - allowFontScaling - multiline - style={[pal.text, styles.textInput, styles.textInputFormatting]} - {...props}> - {textDecorated} - </PasteInput> - <Autocomplete - view={autocompleteView} - onSelect={onSelectAutocompleteItem} - /> - </View> - ) - }, -) + }, 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<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, + ), + ) + autocompleteView.setActive(false) + }, + [onChangeText, richtext, autocompleteView], + ) + + const textDecorated = useMemo(() => { + let i = 0 + + return Array.from(richtext.segments()).map(segment => ( + <Text + key={i++} + style={[ + !segment.facet ? pal.text : pal.link, + styles.textInputFormatting, + ]}> + {segment.text} + </Text> + )) + }, [richtext, pal.link, pal.text]) + + return ( + <View style={styles.container}> + <PasteInput + testID="composerTextInput" + ref={textInput} + onChangeText={onChangeText} + onPaste={onPaste} + onSelectionChange={onSelectionChange} + placeholder={placeholder} + placeholderTextColor={pal.colors.textLight} + keyboardAppearance={theme.colorScheme} + autoFocus={true} + allowFontScaling + multiline + style={[pal.text, styles.textInput, styles.textInputFormatting]} + {...props}> + {textDecorated} + </PasteInput> + <Autocomplete + view={autocompleteView} + onSelect={onSelectAutocompleteItem} + /> + </View> + ) +}) const styles = StyleSheet.create({ container: { diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index 77f634930..e90298817 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -37,135 +37,130 @@ interface TextInputProps { export const textInputWebEmitter = new EventEmitter() -export const TextInput = React.forwardRef( - ( - { - richtext, - placeholder, - suggestedLinks, - autocompleteView, - setRichText, - onPhotoPasted, - onPressPublish, - onSuggestedLinksChanged, - }: // onError, TODO - TextInputProps, - ref, - ) => { - const modeClass = useColorSchemeStyle( - 'ProseMirror-light', - 'ProseMirror-dark', - ) +export const TextInput = React.forwardRef(function TextInputImpl( + { + richtext, + placeholder, + suggestedLinks, + autocompleteView, + setRichText, + onPhotoPasted, + onPressPublish, + onSuggestedLinksChanged, + }: // onError, TODO + TextInputProps, + ref, +) { + const modeClass = useColorSchemeStyle('ProseMirror-light', 'ProseMirror-dark') - React.useEffect(() => { - textInputWebEmitter.addListener('publish', onPressPublish) - return () => { - textInputWebEmitter.removeListener('publish', onPressPublish) - } - }, [onPressPublish]) - React.useEffect(() => { - textInputWebEmitter.addListener('photo-pasted', onPhotoPasted) - return () => { - textInputWebEmitter.removeListener('photo-pasted', onPhotoPasted) - } - }, [onPhotoPasted]) + React.useEffect(() => { + textInputWebEmitter.addListener('publish', onPressPublish) + return () => { + textInputWebEmitter.removeListener('publish', onPressPublish) + } + }, [onPressPublish]) + React.useEffect(() => { + textInputWebEmitter.addListener('photo-pasted', onPhotoPasted) + return () => { + textInputWebEmitter.removeListener('photo-pasted', onPhotoPasted) + } + }, [onPhotoPasted]) - const editor = useEditor( - { - extensions: [ - Document, - LinkDecorator, - Mention.configure({ - HTMLAttributes: { - class: 'mention', - }, - suggestion: createSuggestion({autocompleteView}), - }), - Paragraph, - Placeholder.configure({ - placeholder, - }), - Text, - History, - Hardbreak, - ], - editorProps: { - attributes: { - class: modeClass, + const editor = useEditor( + { + extensions: [ + Document, + LinkDecorator, + Mention.configure({ + HTMLAttributes: { + class: 'mention', }, - handlePaste: (_, event) => { - const items = event.clipboardData?.items + suggestion: createSuggestion({autocompleteView}), + }), + Paragraph, + Placeholder.configure({ + placeholder, + }), + Text, + History, + Hardbreak, + ], + editorProps: { + attributes: { + class: modeClass, + }, + handlePaste: (_, event) => { + const items = event.clipboardData?.items - if (items === undefined) { - return - } + if (items === undefined) { + return + } - getImageFromUri(items, (uri: string) => { - textInputWebEmitter.emit('photo-pasted', uri) - }) - }, - handleKeyDown: (_, event) => { - if ((event.metaKey || event.ctrlKey) && event.code === 'Enter') { - textInputWebEmitter.emit('publish') - } - }, + getImageFromUri(items, (uri: string) => { + textInputWebEmitter.emit('photo-pasted', uri) + }) }, - content: textToEditorJson(richtext.text.toString()), - autofocus: 'end', - editable: true, - injectCSS: true, - onUpdate({editor: editorProp}) { - const json = editorProp.getJSON() + handleKeyDown: (_, event) => { + if ((event.metaKey || event.ctrlKey) && event.code === 'Enter') { + textInputWebEmitter.emit('publish') + } + }, + }, + content: textToEditorJson(richtext.text.toString()), + autofocus: 'end', + editable: true, + injectCSS: true, + onUpdate({editor: editorProp}) { + const json = editorProp.getJSON() - const newRt = new RichText({text: editorJsonToText(json).trim()}) - newRt.detectFacetsWithoutResolution() - setRichText(newRt) + const newRt = new RichText({text: editorJsonToText(json).trim()}) + newRt.detectFacetsWithoutResolution() + setRichText(newRt) - const set: Set<string> = new Set() + const set: Set<string> = new Set() - if (newRt.facets) { - for (const facet of newRt.facets) { - for (const feature of facet.features) { - if (AppBskyRichtextFacet.isLink(feature)) { - set.add(feature.uri) - } + if (newRt.facets) { + for (const facet of newRt.facets) { + for (const feature of facet.features) { + if (AppBskyRichtextFacet.isLink(feature)) { + set.add(feature.uri) } } } + } - if (!isEqual(set, suggestedLinks)) { - onSuggestedLinksChanged(set) - } - }, + if (!isEqual(set, suggestedLinks)) { + onSuggestedLinksChanged(set) + } }, - [modeClass], - ) + }, + [modeClass], + ) - const onEmojiInserted = React.useCallback( - (emoji: Emoji) => { - editor?.chain().focus().insertContent(emoji.native).run() - }, - [editor], - ) - React.useEffect(() => { - textInputWebEmitter.addListener('emoji-inserted', onEmojiInserted) - return () => { - textInputWebEmitter.removeListener('emoji-inserted', onEmojiInserted) - } - }, [onEmojiInserted]) + const onEmojiInserted = React.useCallback( + (emoji: Emoji) => { + editor?.chain().focus().insertContent(emoji.native).run() + }, + [editor], + ) + React.useEffect(() => { + textInputWebEmitter.addListener('emoji-inserted', onEmojiInserted) + return () => { + textInputWebEmitter.removeListener('emoji-inserted', onEmojiInserted) + } + }, [onEmojiInserted]) - React.useImperativeHandle(ref, () => ({ - focus: () => {}, // TODO - blur: () => {}, // TODO - })) + React.useImperativeHandle(ref, () => ({ + focus: () => {}, // TODO + blur: () => {}, // TODO + })) - return ( - <View style={styles.container}> - <EditorContent editor={editor} /> - </View> - ) - }, -) + return ( + <View style={styles.container}> + <EditorContent editor={editor} /> + </View> + ) +}) function editorJsonToText(json: JSONContent): string { let text = '' diff --git a/src/view/com/composer/text-input/web/Autocomplete.tsx b/src/view/com/composer/text-input/web/Autocomplete.tsx index 87820b97b..bbed26d48 100644 --- a/src/view/com/composer/text-input/web/Autocomplete.tsx +++ b/src/view/com/composer/text-input/web/Autocomplete.tsx @@ -94,7 +94,7 @@ export function createSuggestion({ } const MentionList = forwardRef<MentionListRef, SuggestionProps>( - (props: SuggestionProps, ref) => { + function MentionListImpl(props: SuggestionProps, ref) { const [selectedIndex, setSelectedIndex] = useState(0) const pal = usePalette('default') const {getGraphemeString} = useGrapheme() |