Add ESLint React plugin (#1412)
* Add eslint-plugin-react * Enable display name rule
This commit is contained in:
parent
00595591c4
commit
a5b89dffa6
14 changed files with 612 additions and 625 deletions
|
@ -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: {
|
||||
|
|
|
@ -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 = ''
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue