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 | |
parent | 00595591c46db6ebfa9a8ee404f275b43493f7e0 (diff) | |
download | voidsky-a5b89dffa6713bb06c1c572bbdc00517cf5e9bc5.tar.zst |
Add ESLint React plugin (#1412)
* Add eslint-plugin-react * Enable display name rule
-rw-r--r-- | .eslintrc.js | 4 | ||||
-rw-r--r-- | package.json | 1 | ||||
-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 | ||||
-rw-r--r-- | src/view/com/pager/Pager.tsx | 4 | ||||
-rw-r--r-- | src/view/com/pager/Pager.web.tsx | 88 | ||||
-rw-r--r-- | src/view/com/search/Suggestions.tsx | 328 | ||||
-rw-r--r-- | src/view/com/util/PressableWithHover.tsx | 56 | ||||
-rw-r--r-- | src/view/com/util/ViewSelector.tsx | 170 | ||||
-rw-r--r-- | src/view/com/util/Views.web.tsx | 4 | ||||
-rw-r--r-- | src/view/com/util/anim/TriggerableAnimated.tsx | 2 | ||||
-rw-r--r-- | src/view/com/util/layouts/withBreakpoints.tsx | 13 | ||||
-rw-r--r-- | yarn.lock | 2 |
14 files changed, 605 insertions, 618 deletions
diff --git a/.eslintrc.js b/.eslintrc.js index a40b5da92..c7c987751 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2,12 +2,14 @@ module.exports = { root: true, extends: [ '@react-native-community', + 'plugin:react/recommended', 'plugin:react-native-a11y/ios', 'prettier', ], parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint', 'detox'], + plugins: ['@typescript-eslint', 'detox', 'react'], rules: { + 'react/no-unescaped-entities': 0, 'react-native/no-inline-styles': 0, }, ignorePatterns: [ diff --git a/package.json b/package.json index 59b9ba95c..a81f0afc5 100644 --- a/package.json +++ b/package.json @@ -181,6 +181,7 @@ "eslint": "^8.19.0", "eslint-plugin-detox": "^1.0.0", "eslint-plugin-ft-flow": "^2.0.3", + "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-native-a11y": "^3.3.0", "html-webpack-plugin": "^5.5.0", "husky": "^8.0.3", 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() diff --git a/src/view/com/pager/Pager.tsx b/src/view/com/pager/Pager.tsx index ad271da33..39ba29bda 100644 --- a/src/view/com/pager/Pager.tsx +++ b/src/view/com/pager/Pager.tsx @@ -24,7 +24,7 @@ interface Props { testID?: string } export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>( - ( + function PagerImpl( { children, tabBarPosition = 'top', @@ -34,7 +34,7 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>( testID, }: React.PropsWithChildren<Props>, ref, - ) => { + ) { const [selectedPage, setSelectedPage] = React.useState(0) const pagerView = React.useRef<PagerView>(null) diff --git a/src/view/com/pager/Pager.web.tsx b/src/view/com/pager/Pager.web.tsx index 7be2b11ec..fe4febbb7 100644 --- a/src/view/com/pager/Pager.web.tsx +++ b/src/view/com/pager/Pager.web.tsx @@ -14,51 +14,49 @@ interface Props { renderTabBar: RenderTabBarFn onPageSelected?: (index: number) => void } -export const Pager = React.forwardRef( - ( - { - children, - tabBarPosition = 'top', - initialPage = 0, - renderTabBar, - onPageSelected, - }: React.PropsWithChildren<Props>, - ref, - ) => { - const [selectedPage, setSelectedPage] = React.useState(initialPage) +export const Pager = React.forwardRef(function PagerImpl( + { + children, + tabBarPosition = 'top', + initialPage = 0, + renderTabBar, + onPageSelected, + }: React.PropsWithChildren<Props>, + ref, +) { + const [selectedPage, setSelectedPage] = React.useState(initialPage) - React.useImperativeHandle(ref, () => ({ - setPage: (index: number) => setSelectedPage(index), - })) + React.useImperativeHandle(ref, () => ({ + setPage: (index: number) => setSelectedPage(index), + })) - const onTabBarSelect = React.useCallback( - (index: number) => { - setSelectedPage(index) - onPageSelected?.(index) - }, - [setSelectedPage, onPageSelected], - ) + const onTabBarSelect = React.useCallback( + (index: number) => { + setSelectedPage(index) + onPageSelected?.(index) + }, + [setSelectedPage, onPageSelected], + ) - return ( - <View> - {tabBarPosition === 'top' && - renderTabBar({ - selectedPage, - onSelect: onTabBarSelect, - })} - {React.Children.map(children, (child, i) => ( - <View - style={selectedPage === i ? undefined : s.hidden} - key={`page-${i}`}> - {child} - </View> - ))} - {tabBarPosition === 'bottom' && - renderTabBar({ - selectedPage, - onSelect: onTabBarSelect, - })} - </View> - ) - }, -) + return ( + <View> + {tabBarPosition === 'top' && + renderTabBar({ + selectedPage, + onSelect: onTabBarSelect, + })} + {React.Children.map(children, (child, i) => ( + <View + style={selectedPage === i ? undefined : s.hidden} + key={`page-${i}`}> + {child} + </View> + ))} + {tabBarPosition === 'bottom' && + renderTabBar({ + selectedPage, + onSelect: onTabBarSelect, + })} + </View> + ) +}) diff --git a/src/view/com/search/Suggestions.tsx b/src/view/com/search/Suggestions.tsx index 440d912af..6f9fff52f 100644 --- a/src/view/com/search/Suggestions.tsx +++ b/src/view/com/search/Suggestions.tsx @@ -39,179 +39,177 @@ interface ProfileView { type Item = Heading | RefWrapper | SuggestWrapper | ProfileView export const Suggestions = observer( - forwardRef( - ( - { - foafs, - suggestedActors, - }: { - foafs: FoafsModel - suggestedActors: SuggestedActorsModel - }, - flatListRef: ForwardedRef<FlatList>, - ) => { - const pal = usePalette('default') - const [refreshing, setRefreshing] = React.useState(false) - const data = React.useMemo(() => { - let items: Item[] = [] + forwardRef(function SuggestionsImpl( + { + foafs, + suggestedActors, + }: { + foafs: FoafsModel + suggestedActors: SuggestedActorsModel + }, + flatListRef: ForwardedRef<FlatList>, + ) { + const pal = usePalette('default') + const [refreshing, setRefreshing] = React.useState(false) + const data = React.useMemo(() => { + let items: Item[] = [] - if (foafs.popular.length > 0) { - items = items - .concat([ - { - _reactKey: '__popular_heading__', - type: 'heading', - title: 'In Your Network', - }, - ]) - .concat( - foafs.popular.map(ref => ({ - _reactKey: `popular-${ref.did}`, - type: 'ref', - ref, - })), - ) - } - if (suggestedActors.hasContent) { - items = items - .concat([ - { - _reactKey: '__suggested_heading__', - type: 'heading', - title: 'Suggested Follows', - }, - ]) - .concat( - suggestedActors.suggestions.map(suggested => ({ - _reactKey: `suggested-${suggested.did}`, - type: 'suggested', - suggested, - })), - ) - } - for (const source of foafs.sources) { - const item = foafs.foafs.get(source) - if (!item || item.follows.length === 0) { - continue - } - items = items - .concat([ - { - _reactKey: `__${item.did}_heading__`, - type: 'heading', - title: `Followed by ${sanitizeDisplayName( - item.displayName || sanitizeHandle(item.handle), - )}`, - }, - ]) - .concat( - item.follows.slice(0, 10).map(view => ({ - _reactKey: `${item.did}-${view.did}`, - type: 'profile-view', - view, - })), - ) + if (foafs.popular.length > 0) { + items = items + .concat([ + { + _reactKey: '__popular_heading__', + type: 'heading', + title: 'In Your Network', + }, + ]) + .concat( + foafs.popular.map(ref => ({ + _reactKey: `popular-${ref.did}`, + type: 'ref', + ref, + })), + ) + } + if (suggestedActors.hasContent) { + items = items + .concat([ + { + _reactKey: '__suggested_heading__', + type: 'heading', + title: 'Suggested Follows', + }, + ]) + .concat( + suggestedActors.suggestions.map(suggested => ({ + _reactKey: `suggested-${suggested.did}`, + type: 'suggested', + suggested, + })), + ) + } + for (const source of foafs.sources) { + const item = foafs.foafs.get(source) + if (!item || item.follows.length === 0) { + continue } + items = items + .concat([ + { + _reactKey: `__${item.did}_heading__`, + type: 'heading', + title: `Followed by ${sanitizeDisplayName( + item.displayName || sanitizeHandle(item.handle), + )}`, + }, + ]) + .concat( + item.follows.slice(0, 10).map(view => ({ + _reactKey: `${item.did}-${view.did}`, + type: 'profile-view', + view, + })), + ) + } - return items - }, [ - foafs.popular, - suggestedActors.hasContent, - suggestedActors.suggestions, - foafs.sources, - foafs.foafs, - ]) + return items + }, [ + foafs.popular, + suggestedActors.hasContent, + suggestedActors.suggestions, + foafs.sources, + foafs.foafs, + ]) - const onRefresh = React.useCallback(async () => { - setRefreshing(true) - try { - await foafs.fetch() - } finally { - setRefreshing(false) - } - }, [foafs, setRefreshing]) + const onRefresh = React.useCallback(async () => { + setRefreshing(true) + try { + await foafs.fetch() + } finally { + setRefreshing(false) + } + }, [foafs, setRefreshing]) - const renderItem = React.useCallback( - ({item}: {item: Item}) => { - if (item.type === 'heading') { - return ( - <Text type="title" style={[styles.heading, pal.text]}> - {item.title} - </Text> - ) - } - if (item.type === 'ref') { - return ( - <View style={[styles.card, pal.view, pal.border]}> - <ProfileCardWithFollowBtn - key={item.ref.did} - profile={item.ref} - noBg - noBorder - followers={ - item.ref.followers - ? (item.ref.followers as AppBskyActorDefs.ProfileView[]) - : undefined - } - /> - </View> - ) - } - if (item.type === 'profile-view') { - return ( - <View style={[styles.card, pal.view, pal.border]}> - <ProfileCardWithFollowBtn - key={item.view.did} - profile={item.view} - noBg - noBorder - /> - </View> - ) - } - if (item.type === 'suggested') { - return ( - <View style={[styles.card, pal.view, pal.border]}> - <ProfileCardWithFollowBtn - key={item.suggested.did} - profile={item.suggested} - noBg - noBorder - /> - </View> - ) - } - return null - }, - [pal], - ) + const renderItem = React.useCallback( + ({item}: {item: Item}) => { + if (item.type === 'heading') { + return ( + <Text type="title" style={[styles.heading, pal.text]}> + {item.title} + </Text> + ) + } + if (item.type === 'ref') { + return ( + <View style={[styles.card, pal.view, pal.border]}> + <ProfileCardWithFollowBtn + key={item.ref.did} + profile={item.ref} + noBg + noBorder + followers={ + item.ref.followers + ? (item.ref.followers as AppBskyActorDefs.ProfileView[]) + : undefined + } + /> + </View> + ) + } + if (item.type === 'profile-view') { + return ( + <View style={[styles.card, pal.view, pal.border]}> + <ProfileCardWithFollowBtn + key={item.view.did} + profile={item.view} + noBg + noBorder + /> + </View> + ) + } + if (item.type === 'suggested') { + return ( + <View style={[styles.card, pal.view, pal.border]}> + <ProfileCardWithFollowBtn + key={item.suggested.did} + profile={item.suggested} + noBg + noBorder + /> + </View> + ) + } + return null + }, + [pal], + ) - if (foafs.isLoading || suggestedActors.isLoading) { - return ( - <CenteredView> - <ProfileCardFeedLoadingPlaceholder /> - </CenteredView> - ) - } + if (foafs.isLoading || suggestedActors.isLoading) { return ( - <FlatList - ref={flatListRef} - data={data} - keyExtractor={item => item._reactKey} - refreshControl={ - <RefreshControl - refreshing={refreshing} - onRefresh={onRefresh} - tintColor={pal.colors.text} - titleColor={pal.colors.text} - /> - } - renderItem={renderItem} - initialNumToRender={15} - /> + <CenteredView> + <ProfileCardFeedLoadingPlaceholder /> + </CenteredView> ) - }, - ), + } + return ( + <FlatList + ref={flatListRef} + data={data} + keyExtractor={item => item._reactKey} + refreshControl={ + <RefreshControl + refreshing={refreshing} + onRefresh={onRefresh} + tintColor={pal.colors.text} + titleColor={pal.colors.text} + /> + } + renderItem={renderItem} + initialNumToRender={15} + /> + ) + }), ) const styles = StyleSheet.create({ diff --git a/src/view/com/util/PressableWithHover.tsx b/src/view/com/util/PressableWithHover.tsx index 09ccb6a2d..77276f184 100644 --- a/src/view/com/util/PressableWithHover.tsx +++ b/src/view/com/util/PressableWithHover.tsx @@ -12,34 +12,32 @@ interface PressableWithHover extends PressableProps { hoverStyle: StyleProp<ViewStyle> } -export const PressableWithHover = forwardRef( - ( - { - children, - style, - hoverStyle, - ...props - }: PropsWithChildren<PressableWithHover>, - ref: Ref<any>, - ) => { - const [isHovering, setIsHovering] = useState(false) +export const PressableWithHover = forwardRef(function PressableWithHoverImpl( + { + children, + style, + hoverStyle, + ...props + }: PropsWithChildren<PressableWithHover>, + ref: Ref<any>, +) { + const [isHovering, setIsHovering] = useState(false) - const onHoverIn = useCallback(() => setIsHovering(true), [setIsHovering]) - const onHoverOut = useCallback(() => setIsHovering(false), [setIsHovering]) - style = - typeof style !== 'function' && isHovering - ? addStyle(style, hoverStyle) - : style + const onHoverIn = useCallback(() => setIsHovering(true), [setIsHovering]) + const onHoverOut = useCallback(() => setIsHovering(false), [setIsHovering]) + style = + typeof style !== 'function' && isHovering + ? addStyle(style, hoverStyle) + : style - return ( - <Pressable - {...props} - style={style} - onHoverIn={onHoverIn} - onHoverOut={onHoverOut} - ref={ref}> - {children} - </Pressable> - ) - }, -) + return ( + <Pressable + {...props} + style={style} + onHoverIn={onHoverIn} + onHoverOut={onHoverOut} + ref={ref}> + {children} + </Pressable> + ) +}) diff --git a/src/view/com/util/ViewSelector.tsx b/src/view/com/util/ViewSelector.tsx index 8d2a30506..6c0e4c6cc 100644 --- a/src/view/com/util/ViewSelector.tsx +++ b/src/view/com/util/ViewSelector.tsx @@ -42,100 +42,98 @@ export const ViewSelector = React.forwardRef< onRefresh?: () => void onEndReached?: (info: {distanceFromEnd: number}) => void } ->( - ( - { - sections, - items, - refreshing, - renderHeader, - renderItem, - ListFooterComponent, - onSelectView, - onScroll, - onRefresh, - onEndReached, - }, - ref, - ) => { - const pal = usePalette('default') - const [selectedIndex, setSelectedIndex] = useState<number>(0) - const flatListRef = React.useRef<FlatList>(null) +>(function ViewSelectorImpl( + { + sections, + items, + refreshing, + renderHeader, + renderItem, + ListFooterComponent, + onSelectView, + onScroll, + onRefresh, + onEndReached, + }, + ref, +) { + const pal = usePalette('default') + const [selectedIndex, setSelectedIndex] = useState<number>(0) + const flatListRef = React.useRef<FlatList>(null) - // events - // = + // events + // = - const keyExtractor = React.useCallback((item: any) => item._reactKey, []) + const keyExtractor = React.useCallback((item: any) => item._reactKey, []) - const onPressSelection = React.useCallback( - (index: number) => setSelectedIndex(clamp(index, 0, sections.length)), - [setSelectedIndex, sections], - ) - useEffect(() => { - onSelectView?.(selectedIndex) - }, [selectedIndex, onSelectView]) + const onPressSelection = React.useCallback( + (index: number) => setSelectedIndex(clamp(index, 0, sections.length)), + [setSelectedIndex, sections], + ) + useEffect(() => { + onSelectView?.(selectedIndex) + }, [selectedIndex, onSelectView]) - React.useImperativeHandle(ref, () => ({ - scrollToTop: () => { - flatListRef.current?.scrollToOffset({offset: 0}) - }, - })) + React.useImperativeHandle(ref, () => ({ + scrollToTop: () => { + flatListRef.current?.scrollToOffset({offset: 0}) + }, + })) - // rendering - // = + // rendering + // = - const renderItemInternal = React.useCallback( - ({item}: {item: any}) => { - if (item === HEADER_ITEM) { - if (renderHeader) { - return renderHeader() - } - return <View /> - } else if (item === SELECTOR_ITEM) { - return ( - <Selector - items={sections} - selectedIndex={selectedIndex} - onSelect={onPressSelection} - /> - ) - } else { - return renderItem(item) + const renderItemInternal = React.useCallback( + ({item}: {item: any}) => { + if (item === HEADER_ITEM) { + if (renderHeader) { + return renderHeader() } - }, - [sections, selectedIndex, onPressSelection, renderHeader, renderItem], - ) - - const data = React.useMemo( - () => [HEADER_ITEM, SELECTOR_ITEM, ...items], - [items], - ) - return ( - <FlatList - ref={flatListRef} - data={data} - keyExtractor={keyExtractor} - renderItem={renderItemInternal} - ListFooterComponent={ListFooterComponent} - // NOTE sticky header disabled on android due to major performance issues -prf - stickyHeaderIndices={isAndroid ? undefined : STICKY_HEADER_INDICES} - onScroll={onScroll} - onEndReached={onEndReached} - refreshControl={ - <RefreshControl - refreshing={refreshing!} - onRefresh={onRefresh} - tintColor={pal.colors.text} + return <View /> + } else if (item === SELECTOR_ITEM) { + return ( + <Selector + items={sections} + selectedIndex={selectedIndex} + onSelect={onPressSelection} /> - } - onEndReachedThreshold={0.6} - contentContainerStyle={s.contentContainer} - removeClippedSubviews={true} - scrollIndicatorInsets={{right: 1}} // fixes a bug where the scroll indicator is on the middle of the screen https://github.com/bluesky-social/social-app/pull/464 - /> - ) - }, -) + ) + } else { + return renderItem(item) + } + }, + [sections, selectedIndex, onPressSelection, renderHeader, renderItem], + ) + + const data = React.useMemo( + () => [HEADER_ITEM, SELECTOR_ITEM, ...items], + [items], + ) + return ( + <FlatList + ref={flatListRef} + data={data} + keyExtractor={keyExtractor} + renderItem={renderItemInternal} + ListFooterComponent={ListFooterComponent} + // NOTE sticky header disabled on android due to major performance issues -prf + stickyHeaderIndices={isAndroid ? undefined : STICKY_HEADER_INDICES} + onScroll={onScroll} + onEndReached={onEndReached} + refreshControl={ + <RefreshControl + refreshing={refreshing!} + onRefresh={onRefresh} + tintColor={pal.colors.text} + /> + } + onEndReachedThreshold={0.6} + contentContainerStyle={s.contentContainer} + removeClippedSubviews={true} + scrollIndicatorInsets={{right: 1}} // fixes a bug where the scroll indicator is on the middle of the screen https://github.com/bluesky-social/social-app/pull/464 + /> + ) +}) export function Selector({ selectedIndex, diff --git a/src/view/com/util/Views.web.tsx b/src/view/com/util/Views.web.tsx index 891d3f2ee..fda0a9b86 100644 --- a/src/view/com/util/Views.web.tsx +++ b/src/view/com/util/Views.web.tsx @@ -38,7 +38,7 @@ export function CenteredView({ return <View style={style} {...props} /> } -export const FlatList = React.forwardRef(function <ItemT>( +export const FlatList = React.forwardRef(function FlatListImpl<ItemT>( { contentContainerStyle, style, @@ -99,7 +99,7 @@ export const FlatList = React.forwardRef(function <ItemT>( ) }) -export const ScrollView = React.forwardRef(function ( +export const ScrollView = React.forwardRef(function ScrollViewImpl( {contentContainerStyle, ...props}: React.PropsWithChildren<ScrollViewProps>, ref: React.Ref<RNScrollView>, ) { diff --git a/src/view/com/util/anim/TriggerableAnimated.tsx b/src/view/com/util/anim/TriggerableAnimated.tsx index 2a3cbb957..eedeeda03 100644 --- a/src/view/com/util/anim/TriggerableAnimated.tsx +++ b/src/view/com/util/anim/TriggerableAnimated.tsx @@ -26,7 +26,7 @@ type PropsInner = TriggerableAnimatedProps & { export const TriggerableAnimated = React.forwardRef< TriggerableAnimatedRef, TriggerableAnimatedProps ->(({children, ...props}, ref) => { +>(function TriggerableAnimatedImpl({children, ...props}, ref) { const [anim, setAnim] = React.useState<TriggeredAnimation | undefined>( undefined, ) diff --git a/src/view/com/util/layouts/withBreakpoints.tsx b/src/view/com/util/layouts/withBreakpoints.tsx index dc3f50dc9..5746aa660 100644 --- a/src/view/com/util/layouts/withBreakpoints.tsx +++ b/src/view/com/util/layouts/withBreakpoints.tsx @@ -2,13 +2,12 @@ import React from 'react' import {isNative} from 'platform/detection' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -export const withBreakpoints = - <P extends object>( - Mobile: React.ComponentType<P>, - Tablet: React.ComponentType<P>, - Desktop: React.ComponentType<P>, - ): React.FC<P> => - (props: P) => { +export const withBreakpoints = <P extends object>( + Mobile: React.ComponentType<P>, + Tablet: React.ComponentType<P>, + Desktop: React.ComponentType<P>, +): React.FC<P> => + function WithBreakpoints(props: P) { const {isMobile, isTabletOrMobile} = useWebMediaQueries() if (isMobile || isNative) { diff --git a/yarn.lock b/yarn.lock index cd7ad17f8..31802f82b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9906,7 +9906,7 @@ eslint-plugin-react-native@^4.0.0: "@babel/traverse" "^7.7.4" eslint-plugin-react-native-globals "^0.1.1" -eslint-plugin-react@^7.27.1, eslint-plugin-react@^7.30.1: +eslint-plugin-react@^7.27.1, eslint-plugin-react@^7.30.1, eslint-plugin-react@^7.33.2: version "7.33.2" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz#69ee09443ffc583927eafe86ffebb470ee737608" integrity sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw== |