Add ESLint React plugin (#1412)

* Add eslint-plugin-react

* Enable display name rule
This commit is contained in:
dan 2023-09-08 00:38:57 +01:00 committed by GitHub
parent 00595591c4
commit a5b89dffa6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 612 additions and 625 deletions

View file

@ -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()
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()
},
}))
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 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 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()
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 (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 (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],
)
}, 1)
},
[
setRichText,
autocompleteView,
suggestedLinks,
onSuggestedLinksChanged,
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 onPaste = useCallback(
async (err: string | undefined, files: PastedFile[]) => {
if (err) {
return onError(cleanError(err))
}
const onSelectAutocompleteItem = useCallback(
(item: string) => {
onChangeText(
insertMentionAt(
richtext.text,
textInputSelection.current?.start || 0,
item,
),
)
autocompleteView.setActive(false)
},
[onChangeText, richtext, autocompleteView],
)
const uris = files.map(f => f.uri)
const uri = uris.find(isUriImage)
const textDecorated = useMemo(() => {
let i = 0
if (uri) {
onPhotoPasted(uri)
}
},
[onError, onPhotoPasted],
)
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])
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],
)
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 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: {

View file

@ -37,135 +37,130 @@ interface TextInputProps {
export const textInputWebEmitter = new EventEmitter()
export const TextInput = React.forwardRef(
(
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])
const editor = useEditor(
{
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])
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,
},
handlePaste: (_, event) => {
const items = event.clipboardData?.items
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')
}
extensions: [
Document,
LinkDecorator,
Mention.configure({
HTMLAttributes: {
class: 'mention',
},
suggestion: createSuggestion({autocompleteView}),
}),
Paragraph,
Placeholder.configure({
placeholder,
}),
Text,
History,
Hardbreak,
],
editorProps: {
attributes: {
class: modeClass,
},
content: textToEditorJson(richtext.text.toString()),
autofocus: 'end',
editable: true,
injectCSS: true,
onUpdate({editor: editorProp}) {
const json = editorProp.getJSON()
handlePaste: (_, event) => {
const items = event.clipboardData?.items
const newRt = new RichText({text: editorJsonToText(json).trim()})
newRt.detectFacetsWithoutResolution()
setRichText(newRt)
if (items === undefined) {
return
}
const set: Set<string> = new Set()
getImageFromUri(items, (uri: string) => {
textInputWebEmitter.emit('photo-pasted', uri)
})
},
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()
if (newRt.facets) {
for (const facet of newRt.facets) {
for (const feature of facet.features) {
if (AppBskyRichtextFacet.isLink(feature)) {
set.add(feature.uri)
}
const newRt = new RichText({text: editorJsonToText(json).trim()})
newRt.detectFacetsWithoutResolution()
setRichText(newRt)
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 (!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 = ''

View file

@ -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()