Add ESLint React plugin (#1412)

* Add eslint-plugin-react

* Enable display name rule
zio/stable
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

@ -2,12 +2,14 @@ module.exports = {
root: true, root: true,
extends: [ extends: [
'@react-native-community', '@react-native-community',
'plugin:react/recommended',
'plugin:react-native-a11y/ios', 'plugin:react-native-a11y/ios',
'prettier', 'prettier',
], ],
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'detox'], plugins: ['@typescript-eslint', 'detox', 'react'],
rules: { rules: {
'react/no-unescaped-entities': 0,
'react-native/no-inline-styles': 0, 'react-native/no-inline-styles': 0,
}, },
ignorePatterns: [ ignorePatterns: [

View File

@ -181,6 +181,7 @@
"eslint": "^8.19.0", "eslint": "^8.19.0",
"eslint-plugin-detox": "^1.0.0", "eslint-plugin-detox": "^1.0.0",
"eslint-plugin-ft-flow": "^2.0.3", "eslint-plugin-ft-flow": "^2.0.3",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-native-a11y": "^3.3.0", "eslint-plugin-react-native-a11y": "^3.3.0",
"html-webpack-plugin": "^5.5.0", "html-webpack-plugin": "^5.5.0",
"husky": "^8.0.3", "husky": "^8.0.3",

View File

@ -51,181 +51,179 @@ interface Selection {
end: number end: number
} }
export const TextInput = forwardRef( export const TextInput = forwardRef(function TextInputImpl(
( {
{ richtext,
richtext, placeholder,
placeholder, suggestedLinks,
suggestedLinks, autocompleteView,
autocompleteView, setRichText,
setRichText, onPhotoPasted,
onPhotoPasted, onSuggestedLinksChanged,
onSuggestedLinksChanged, onError,
onError, ...props
...props }: TextInputProps,
}: TextInputProps, ref,
ref, ) {
) => { const pal = usePalette('default')
const pal = usePalette('default') const textInput = useRef<PasteInputRef>(null)
const textInput = useRef<PasteInputRef>(null) const textInputSelection = useRef<Selection>({start: 0, end: 0})
const textInputSelection = useRef<Selection>({start: 0, end: 0}) const theme = useTheme()
const theme = useTheme()
React.useImperativeHandle(ref, () => ({ React.useImperativeHandle(ref, () => ({
focus: () => textInput.current?.focus(), focus: () => textInput.current?.focus(),
blur: () => { blur: () => {
textInput.current?.blur() textInput.current?.blur()
}, },
})) }))
const onChangeText = useCallback( const onChangeText = useCallback(
(newText: string) => { (newText: string) => {
/* /*
* This is a hack to bump the rendering of our styled * This is a hack to bump the rendering of our styled
* `textDecorated` to _after_ whatever processing is happening * `textDecorated` to _after_ whatever processing is happening
* within the `PasteInput` library. Without this, the elements in * within the `PasteInput` library. Without this, the elements in
* `textDecorated` are not correctly painted to screen. * `textDecorated` are not correctly painted to screen.
* *
* NB: we tried a `0` timeout as well, but only positive values worked. * NB: we tried a `0` timeout as well, but only positive values worked.
* *
* @see https://github.com/bluesky-social/social-app/issues/929 * @see https://github.com/bluesky-social/social-app/issues/929
*/ */
setTimeout(async () => { setTimeout(async () => {
const newRt = new RichText({text: newText}) const newRt = new RichText({text: newText})
newRt.detectFacetsWithoutResolution() newRt.detectFacetsWithoutResolution()
setRichText(newRt) setRichText(newRt)
const prefix = getMentionAt( const prefix = getMentionAt(
newText, newText,
textInputSelection.current?.start || 0, textInputSelection.current?.start || 0,
) )
if (prefix) { if (prefix) {
autocompleteView.setActive(true) autocompleteView.setActive(true)
autocompleteView.setPrefix(prefix.value) autocompleteView.setPrefix(prefix.value)
} else { } else {
autocompleteView.setActive(false) autocompleteView.setActive(false)
} }
const set: Set<string> = new Set() const set: Set<string> = new Set()
if (newRt.facets) { if (newRt.facets) {
for (const facet of newRt.facets) { for (const facet of newRt.facets) {
for (const feature of facet.features) { for (const feature of facet.features) {
if (AppBskyRichtextFacet.isLink(feature)) { if (AppBskyRichtextFacet.isLink(feature)) {
if (isUriImage(feature.uri)) { if (isUriImage(feature.uri)) {
const res = await downloadAndResize({ const res = await downloadAndResize({
uri: feature.uri, uri: feature.uri,
width: POST_IMG_MAX.width, width: POST_IMG_MAX.width,
height: POST_IMG_MAX.height, height: POST_IMG_MAX.height,
mode: 'contain', mode: 'contain',
maxSize: POST_IMG_MAX.size, maxSize: POST_IMG_MAX.size,
timeout: 15e3, timeout: 15e3,
}) })
if (res !== undefined) { if (res !== undefined) {
onPhotoPasted(res.path) onPhotoPasted(res.path)
}
} else {
set.add(feature.uri)
} }
} 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) if (!isEqual(set, suggestedLinks)) {
const uri = uris.find(isUriImage) onSuggestedLinksChanged(set)
if (uri) {
onPhotoPasted(uri)
} }
}, }, 1)
[onError, onPhotoPasted], },
) [
setRichText,
autocompleteView,
suggestedLinks,
onSuggestedLinksChanged,
onPhotoPasted,
],
)
const onSelectionChange = useCallback( const onPaste = useCallback(
(evt: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => { async (err: string | undefined, files: PastedFile[]) => {
// NOTE we track the input selection using a ref to avoid excessive renders -prf if (err) {
textInputSelection.current = evt.nativeEvent.selection return onError(cleanError(err))
}, }
[textInputSelection],
)
const onSelectAutocompleteItem = useCallback( const uris = files.map(f => f.uri)
(item: string) => { const uri = uris.find(isUriImage)
onChangeText(
insertMentionAt(
richtext.text,
textInputSelection.current?.start || 0,
item,
),
)
autocompleteView.setActive(false)
},
[onChangeText, richtext, autocompleteView],
)
const textDecorated = useMemo(() => { if (uri) {
let i = 0 onPhotoPasted(uri)
}
},
[onError, onPhotoPasted],
)
return Array.from(richtext.segments()).map(segment => ( const onSelectionChange = useCallback(
<Text (evt: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => {
key={i++} // NOTE we track the input selection using a ref to avoid excessive renders -prf
style={[ textInputSelection.current = evt.nativeEvent.selection
!segment.facet ? pal.text : pal.link, },
styles.textInputFormatting, [textInputSelection],
]}> )
{segment.text}
</Text>
))
}, [richtext, pal.link, pal.text])
return ( const onSelectAutocompleteItem = useCallback(
<View style={styles.container}> (item: string) => {
<PasteInput onChangeText(
testID="composerTextInput" insertMentionAt(
ref={textInput} richtext.text,
onChangeText={onChangeText} textInputSelection.current?.start || 0,
onPaste={onPaste} item,
onSelectionChange={onSelectionChange} ),
placeholder={placeholder} )
placeholderTextColor={pal.colors.textLight} autocompleteView.setActive(false)
keyboardAppearance={theme.colorScheme} },
autoFocus={true} [onChangeText, richtext, autocompleteView],
allowFontScaling )
multiline
style={[pal.text, styles.textInput, styles.textInputFormatting]} const textDecorated = useMemo(() => {
{...props}> let i = 0
{textDecorated}
</PasteInput> return Array.from(richtext.segments()).map(segment => (
<Autocomplete <Text
view={autocompleteView} key={i++}
onSelect={onSelectAutocompleteItem} style={[
/> !segment.facet ? pal.text : pal.link,
</View> 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({ const styles = StyleSheet.create({
container: { container: {

View File

@ -37,135 +37,130 @@ interface TextInputProps {
export const textInputWebEmitter = new EventEmitter() 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, extensions: [
placeholder, Document,
suggestedLinks, LinkDecorator,
autocompleteView, Mention.configure({
setRichText, HTMLAttributes: {
onPhotoPasted, class: 'mention',
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')
}
}, },
suggestion: createSuggestion({autocompleteView}),
}),
Paragraph,
Placeholder.configure({
placeholder,
}),
Text,
History,
Hardbreak,
],
editorProps: {
attributes: {
class: modeClass,
}, },
content: textToEditorJson(richtext.text.toString()), handlePaste: (_, event) => {
autofocus: 'end', const items = event.clipboardData?.items
editable: true,
injectCSS: true,
onUpdate({editor: editorProp}) {
const json = editorProp.getJSON()
const newRt = new RichText({text: editorJsonToText(json).trim()}) if (items === undefined) {
newRt.detectFacetsWithoutResolution() return
setRichText(newRt) }
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) { const newRt = new RichText({text: editorJsonToText(json).trim()})
for (const facet of newRt.facets) { newRt.detectFacetsWithoutResolution()
for (const feature of facet.features) { setRichText(newRt)
if (AppBskyRichtextFacet.isLink(feature)) {
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)) {
set.add(feature.uri)
} }
} }
} }
}
if (!isEqual(set, suggestedLinks)) { if (!isEqual(set, suggestedLinks)) {
onSuggestedLinksChanged(set) onSuggestedLinksChanged(set)
} }
},
}, },
[modeClass], },
) [modeClass],
)
const onEmojiInserted = React.useCallback( const onEmojiInserted = React.useCallback(
(emoji: Emoji) => { (emoji: Emoji) => {
editor?.chain().focus().insertContent(emoji.native).run() editor?.chain().focus().insertContent(emoji.native).run()
}, },
[editor], [editor],
) )
React.useEffect(() => { React.useEffect(() => {
textInputWebEmitter.addListener('emoji-inserted', onEmojiInserted) textInputWebEmitter.addListener('emoji-inserted', onEmojiInserted)
return () => { return () => {
textInputWebEmitter.removeListener('emoji-inserted', onEmojiInserted) textInputWebEmitter.removeListener('emoji-inserted', onEmojiInserted)
} }
}, [onEmojiInserted]) }, [onEmojiInserted])
React.useImperativeHandle(ref, () => ({ React.useImperativeHandle(ref, () => ({
focus: () => {}, // TODO focus: () => {}, // TODO
blur: () => {}, // TODO blur: () => {}, // TODO
})) }))
return ( return (
<View style={styles.container}> <View style={styles.container}>
<EditorContent editor={editor} /> <EditorContent editor={editor} />
</View> </View>
) )
}, })
)
function editorJsonToText(json: JSONContent): string { function editorJsonToText(json: JSONContent): string {
let text = '' let text = ''

View File

@ -94,7 +94,7 @@ export function createSuggestion({
} }
const MentionList = forwardRef<MentionListRef, SuggestionProps>( const MentionList = forwardRef<MentionListRef, SuggestionProps>(
(props: SuggestionProps, ref) => { function MentionListImpl(props: SuggestionProps, ref) {
const [selectedIndex, setSelectedIndex] = useState(0) const [selectedIndex, setSelectedIndex] = useState(0)
const pal = usePalette('default') const pal = usePalette('default')
const {getGraphemeString} = useGrapheme() const {getGraphemeString} = useGrapheme()

View File

@ -24,7 +24,7 @@ interface Props {
testID?: string testID?: string
} }
export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>( export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
( function PagerImpl(
{ {
children, children,
tabBarPosition = 'top', tabBarPosition = 'top',
@ -34,7 +34,7 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
testID, testID,
}: React.PropsWithChildren<Props>, }: React.PropsWithChildren<Props>,
ref, ref,
) => { ) {
const [selectedPage, setSelectedPage] = React.useState(0) const [selectedPage, setSelectedPage] = React.useState(0)
const pagerView = React.useRef<PagerView>(null) const pagerView = React.useRef<PagerView>(null)

View File

@ -14,51 +14,49 @@ interface Props {
renderTabBar: RenderTabBarFn renderTabBar: RenderTabBarFn
onPageSelected?: (index: number) => void onPageSelected?: (index: number) => void
} }
export const Pager = React.forwardRef( export const Pager = React.forwardRef(function PagerImpl(
( {
{ children,
children, tabBarPosition = 'top',
tabBarPosition = 'top', initialPage = 0,
initialPage = 0, renderTabBar,
renderTabBar, onPageSelected,
onPageSelected, }: React.PropsWithChildren<Props>,
}: React.PropsWithChildren<Props>, ref,
ref, ) {
) => { const [selectedPage, setSelectedPage] = React.useState(initialPage)
const [selectedPage, setSelectedPage] = React.useState(initialPage)
React.useImperativeHandle(ref, () => ({ React.useImperativeHandle(ref, () => ({
setPage: (index: number) => setSelectedPage(index), setPage: (index: number) => setSelectedPage(index),
})) }))
const onTabBarSelect = React.useCallback( const onTabBarSelect = React.useCallback(
(index: number) => { (index: number) => {
setSelectedPage(index) setSelectedPage(index)
onPageSelected?.(index) onPageSelected?.(index)
}, },
[setSelectedPage, onPageSelected], [setSelectedPage, onPageSelected],
) )
return ( return (
<View> <View>
{tabBarPosition === 'top' && {tabBarPosition === 'top' &&
renderTabBar({ renderTabBar({
selectedPage, selectedPage,
onSelect: onTabBarSelect, onSelect: onTabBarSelect,
})} })}
{React.Children.map(children, (child, i) => ( {React.Children.map(children, (child, i) => (
<View <View
style={selectedPage === i ? undefined : s.hidden} style={selectedPage === i ? undefined : s.hidden}
key={`page-${i}`}> key={`page-${i}`}>
{child} {child}
</View> </View>
))} ))}
{tabBarPosition === 'bottom' && {tabBarPosition === 'bottom' &&
renderTabBar({ renderTabBar({
selectedPage, selectedPage,
onSelect: onTabBarSelect, onSelect: onTabBarSelect,
})} })}
</View> </View>
) )
}, })
)

View File

@ -39,179 +39,177 @@ interface ProfileView {
type Item = Heading | RefWrapper | SuggestWrapper | ProfileView type Item = Heading | RefWrapper | SuggestWrapper | ProfileView
export const Suggestions = observer( export const Suggestions = observer(
forwardRef( forwardRef(function SuggestionsImpl(
( {
{ foafs,
foafs, suggestedActors,
suggestedActors, }: {
}: { foafs: FoafsModel
foafs: FoafsModel suggestedActors: SuggestedActorsModel
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,
})),
)
}
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 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>
)
}
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}
/>
)
}, },
), 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,
})),
)
}
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 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>
)
}
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({ const styles = StyleSheet.create({

View File

@ -12,34 +12,32 @@ interface PressableWithHover extends PressableProps {
hoverStyle: StyleProp<ViewStyle> hoverStyle: StyleProp<ViewStyle>
} }
export const PressableWithHover = forwardRef( export const PressableWithHover = forwardRef(function PressableWithHoverImpl(
( {
{ children,
children, style,
style, hoverStyle,
hoverStyle, ...props
...props }: PropsWithChildren<PressableWithHover>,
}: PropsWithChildren<PressableWithHover>, ref: Ref<any>,
ref: Ref<any>, ) {
) => { const [isHovering, setIsHovering] = useState(false)
const [isHovering, setIsHovering] = useState(false)
const onHoverIn = useCallback(() => setIsHovering(true), [setIsHovering]) const onHoverIn = useCallback(() => setIsHovering(true), [setIsHovering])
const onHoverOut = useCallback(() => setIsHovering(false), [setIsHovering]) const onHoverOut = useCallback(() => setIsHovering(false), [setIsHovering])
style = style =
typeof style !== 'function' && isHovering typeof style !== 'function' && isHovering
? addStyle(style, hoverStyle) ? addStyle(style, hoverStyle)
: style : style
return ( return (
<Pressable <Pressable
{...props} {...props}
style={style} style={style}
onHoverIn={onHoverIn} onHoverIn={onHoverIn}
onHoverOut={onHoverOut} onHoverOut={onHoverOut}
ref={ref}> ref={ref}>
{children} {children}
</Pressable> </Pressable>
) )
}, })
)

View File

@ -42,100 +42,98 @@ export const ViewSelector = React.forwardRef<
onRefresh?: () => void onRefresh?: () => void
onEndReached?: (info: {distanceFromEnd: number}) => void onEndReached?: (info: {distanceFromEnd: number}) => void
} }
>( >(function ViewSelectorImpl(
( {
{ sections,
sections, items,
items, refreshing,
refreshing, renderHeader,
renderHeader, renderItem,
renderItem, ListFooterComponent,
ListFooterComponent, onSelectView,
onSelectView, onScroll,
onScroll, onRefresh,
onRefresh, onEndReached,
onEndReached,
},
ref,
) => {
const pal = usePalette('default')
const [selectedIndex, setSelectedIndex] = useState<number>(0)
const flatListRef = React.useRef<FlatList>(null)
// events
// =
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])
React.useImperativeHandle(ref, () => ({
scrollToTop: () => {
flatListRef.current?.scrollToOffset({offset: 0})
},
}))
// 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)
}
},
[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
/>
)
}, },
) ref,
) {
const pal = usePalette('default')
const [selectedIndex, setSelectedIndex] = useState<number>(0)
const flatListRef = React.useRef<FlatList>(null)
// events
// =
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])
React.useImperativeHandle(ref, () => ({
scrollToTop: () => {
flatListRef.current?.scrollToOffset({offset: 0})
},
}))
// 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)
}
},
[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({ export function Selector({
selectedIndex, selectedIndex,

View File

@ -38,7 +38,7 @@ export function CenteredView({
return <View style={style} {...props} /> return <View style={style} {...props} />
} }
export const FlatList = React.forwardRef(function <ItemT>( export const FlatList = React.forwardRef(function FlatListImpl<ItemT>(
{ {
contentContainerStyle, contentContainerStyle,
style, 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>, {contentContainerStyle, ...props}: React.PropsWithChildren<ScrollViewProps>,
ref: React.Ref<RNScrollView>, ref: React.Ref<RNScrollView>,
) { ) {

View File

@ -26,7 +26,7 @@ type PropsInner = TriggerableAnimatedProps & {
export const TriggerableAnimated = React.forwardRef< export const TriggerableAnimated = React.forwardRef<
TriggerableAnimatedRef, TriggerableAnimatedRef,
TriggerableAnimatedProps TriggerableAnimatedProps
>(({children, ...props}, ref) => { >(function TriggerableAnimatedImpl({children, ...props}, ref) {
const [anim, setAnim] = React.useState<TriggeredAnimation | undefined>( const [anim, setAnim] = React.useState<TriggeredAnimation | undefined>(
undefined, undefined,
) )

View File

@ -2,13 +2,12 @@ import React from 'react'
import {isNative} from 'platform/detection' import {isNative} from 'platform/detection'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
export const withBreakpoints = export const withBreakpoints = <P extends object>(
<P extends object>( Mobile: React.ComponentType<P>,
Mobile: React.ComponentType<P>, Tablet: React.ComponentType<P>,
Tablet: React.ComponentType<P>, Desktop: React.ComponentType<P>,
Desktop: React.ComponentType<P>, ): React.FC<P> =>
): React.FC<P> => function WithBreakpoints(props: P) {
(props: P) => {
const {isMobile, isTabletOrMobile} = useWebMediaQueries() const {isMobile, isTabletOrMobile} = useWebMediaQueries()
if (isMobile || isNative) { if (isMobile || isNative) {

View File

@ -9906,7 +9906,7 @@ eslint-plugin-react-native@^4.0.0:
"@babel/traverse" "^7.7.4" "@babel/traverse" "^7.7.4"
eslint-plugin-react-native-globals "^0.1.1" 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" version "7.33.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz#69ee09443ffc583927eafe86ffebb470ee737608" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz#69ee09443ffc583927eafe86ffebb470ee737608"
integrity sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw== integrity sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==