Android fixes (#515)
* Fix profile screen performance on android and remove dead code * Correctly handle android hardware back btn * Fix EditProfile modal for android * Fix lintzio/stable
parent
eb6b36be61
commit
d35f7c1f1a
|
@ -13,6 +13,7 @@ import {RootStoreModel, setupState, RootStoreProvider} from './state'
|
||||||
import {Shell} from './view/shell'
|
import {Shell} from './view/shell'
|
||||||
import * as notifee from 'lib/notifee'
|
import * as notifee from 'lib/notifee'
|
||||||
import * as analytics from 'lib/analytics'
|
import * as analytics from 'lib/analytics'
|
||||||
|
import * as backHandler from 'lib/routes/back-handler'
|
||||||
import * as Toast from './view/com/util/Toast'
|
import * as Toast from './view/com/util/Toast'
|
||||||
import {handleLink} from './Navigation'
|
import {handleLink} from './Navigation'
|
||||||
|
|
||||||
|
@ -28,6 +29,7 @@ const App = observer(() => {
|
||||||
setRootStore(store)
|
setRootStore(store)
|
||||||
analytics.init(store)
|
analytics.init(store)
|
||||||
notifee.init(store)
|
notifee.init(store)
|
||||||
|
backHandler.init(store)
|
||||||
SplashScreen.hideAsync()
|
SplashScreen.hideAsync()
|
||||||
Linking.getInitialURL().then((url: string | null) => {
|
Linking.getInitialURL().then((url: string | null) => {
|
||||||
if (url) {
|
if (url) {
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
import {BackHandler} from 'react-native'
|
||||||
|
import {RootStoreModel} from 'state/index'
|
||||||
|
|
||||||
|
export function onBack(cb: () => boolean): () => void {
|
||||||
|
const subscription = BackHandler.addEventListener('hardwareBackPress', cb)
|
||||||
|
return () => subscription.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function init(store: RootStoreModel) {
|
||||||
|
onBack(() => store.shell.closeAnyActiveElement())
|
||||||
|
}
|
|
@ -194,6 +194,30 @@ export class ShellUiModel {
|
||||||
this.minimalShellMode = v
|
this.minimalShellMode = v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns true if something was closed
|
||||||
|
* (used by the android hardware back btn)
|
||||||
|
*/
|
||||||
|
closeAnyActiveElement(): boolean {
|
||||||
|
if (this.isLightboxActive) {
|
||||||
|
this.closeLightbox()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (this.isModalActive) {
|
||||||
|
this.closeModal()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (this.isComposerActive) {
|
||||||
|
this.closeComposer()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (this.isDrawerOpen) {
|
||||||
|
this.closeDrawer()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
openDrawer() {
|
openDrawer() {
|
||||||
this.isDrawerOpen = true
|
this.isDrawerOpen = true
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import ImageView from './ImageViewing'
|
import ImageView from './ImageViewing'
|
||||||
import {useStores} from 'state/index'
|
import {useStores} from 'state/index'
|
||||||
|
@ -48,6 +47,6 @@ export const Lightbox = observer(function Lightbox() {
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return <View />
|
return null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import React, {useState} from 'react'
|
import React, {useState, useCallback} from 'react'
|
||||||
import * as Toast from '../util/Toast'
|
import * as Toast from '../util/Toast'
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
|
KeyboardAvoidingView,
|
||||||
|
ScrollView,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
|
TextInput,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import LinearGradient from 'react-native-linear-gradient'
|
import LinearGradient from 'react-native-linear-gradient'
|
||||||
import {ScrollView, TextInput} from './util'
|
|
||||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||||
import {Text} from '../util/text/Text'
|
import {Text} from '../util/text/Text'
|
||||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||||
|
@ -24,7 +26,7 @@ import {useTheme} from 'lib/ThemeContext'
|
||||||
import {useAnalytics} from 'lib/analytics'
|
import {useAnalytics} from 'lib/analytics'
|
||||||
import {cleanError, isNetworkError} from 'lib/strings/errors'
|
import {cleanError, isNetworkError} from 'lib/strings/errors'
|
||||||
|
|
||||||
export const snapPoints = ['80%']
|
export const snapPoints = ['fullscreen']
|
||||||
|
|
||||||
export function Component({
|
export function Component({
|
||||||
profileView,
|
profileView,
|
||||||
|
@ -61,23 +63,26 @@ export function Component({
|
||||||
const onPressCancel = () => {
|
const onPressCancel = () => {
|
||||||
store.shell.closeModal()
|
store.shell.closeModal()
|
||||||
}
|
}
|
||||||
const onSelectNewAvatar = async (img: RNImage | null) => {
|
const onSelectNewAvatar = useCallback(
|
||||||
track('EditProfile:AvatarSelected')
|
async (img: RNImage | null) => {
|
||||||
try {
|
|
||||||
// if img is null, user selected "remove avatar"
|
|
||||||
if (!img) {
|
if (!img) {
|
||||||
setNewUserAvatar(null)
|
setNewUserAvatar(null)
|
||||||
setUserAvatar(null)
|
setUserAvatar(null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
track('EditProfile:AvatarSelected')
|
||||||
|
try {
|
||||||
const finalImg = await compressIfNeeded(img, 1000000)
|
const finalImg = await compressIfNeeded(img, 1000000)
|
||||||
setNewUserAvatar(finalImg)
|
setNewUserAvatar(finalImg)
|
||||||
setUserAvatar(finalImg.path)
|
setUserAvatar(finalImg.path)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setError(cleanError(e))
|
setError(cleanError(e))
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
const onSelectNewBanner = async (img: RNImage | null) => {
|
[track, setNewUserAvatar, setUserAvatar, setError],
|
||||||
|
)
|
||||||
|
const onSelectNewBanner = useCallback(
|
||||||
|
async (img: RNImage | null) => {
|
||||||
if (!img) {
|
if (!img) {
|
||||||
setNewUserBanner(null)
|
setNewUserBanner(null)
|
||||||
setUserBanner(null)
|
setUserBanner(null)
|
||||||
|
@ -91,8 +96,10 @@ export function Component({
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setError(cleanError(e))
|
setError(cleanError(e))
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
const onPressSave = async () => {
|
[track, setNewUserBanner, setUserBanner, setError],
|
||||||
|
)
|
||||||
|
const onPressSave = useCallback(async () => {
|
||||||
track('EditProfile:Save')
|
track('EditProfile:Save')
|
||||||
setProcessing(true)
|
setProcessing(true)
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@ -120,11 +127,23 @@ export function Component({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setProcessing(false)
|
setProcessing(false)
|
||||||
}
|
}, [
|
||||||
|
track,
|
||||||
|
setProcessing,
|
||||||
|
setError,
|
||||||
|
error,
|
||||||
|
profileView,
|
||||||
|
onUpdate,
|
||||||
|
store,
|
||||||
|
displayName,
|
||||||
|
description,
|
||||||
|
newUserAvatar,
|
||||||
|
newUserBanner,
|
||||||
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[s.flex1, pal.view]} testID="editProfileModal">
|
<KeyboardAvoidingView behavior="height">
|
||||||
<ScrollView style={styles.inner}>
|
<ScrollView style={[pal.view]} testID="editProfileModal">
|
||||||
<Text style={[styles.title, pal.text]}>Edit my profile</Text>
|
<Text style={[styles.title, pal.text]}>Edit my profile</Text>
|
||||||
<View style={styles.photos}>
|
<View style={styles.photos}>
|
||||||
<UserBanner
|
<UserBanner
|
||||||
|
@ -144,6 +163,7 @@ export function Component({
|
||||||
<ErrorMessage message={error} />
|
<ErrorMessage message={error} />
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
<View style={styles.form}>
|
||||||
<View>
|
<View>
|
||||||
<Text style={[styles.label, pal.text]}>Display Name</Text>
|
<Text style={[styles.label, pal.text]}>Display Name</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
@ -152,7 +172,9 @@ export function Component({
|
||||||
placeholder="e.g. Alice Roberts"
|
placeholder="e.g. Alice Roberts"
|
||||||
placeholderTextColor={colors.gray4}
|
placeholderTextColor={colors.gray4}
|
||||||
value={displayName}
|
value={displayName}
|
||||||
onChangeText={v => setDisplayName(enforceLen(v, MAX_DISPLAY_NAME))}
|
onChangeText={v =>
|
||||||
|
setDisplayName(enforceLen(v, MAX_DISPLAY_NAME))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={s.pb10}>
|
<View style={s.pb10}>
|
||||||
|
@ -194,15 +216,13 @@ export function Component({
|
||||||
<Text style={[s.black, s.bold, pal.text]}>Cancel</Text>
|
<Text style={[s.black, s.bold, pal.text]}>Cancel</Text>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</ScrollView>
|
|
||||||
</View>
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
inner: {
|
|
||||||
padding: 14,
|
|
||||||
},
|
|
||||||
title: {
|
title: {
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
|
@ -215,6 +235,9 @@ const styles = StyleSheet.create({
|
||||||
paddingBottom: 4,
|
paddingBottom: 4,
|
||||||
marginTop: 20,
|
marginTop: 20,
|
||||||
},
|
},
|
||||||
|
form: {
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
},
|
||||||
textInput: {
|
textInput: {
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
|
@ -243,7 +266,7 @@ const styles = StyleSheet.create({
|
||||||
avi: {
|
avi: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 80,
|
top: 80,
|
||||||
left: 10,
|
left: 24,
|
||||||
width: 84,
|
width: 84,
|
||||||
height: 84,
|
height: 84,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React, {useRef, useEffect} from 'react'
|
import React, {useRef, useEffect} from 'react'
|
||||||
import {StyleSheet} from 'react-native'
|
import {StyleSheet} from 'react-native'
|
||||||
|
import {SafeAreaView} from 'react-native-safe-area-context'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import BottomSheet from '@gorhom/bottom-sheet'
|
import BottomSheet from '@gorhom/bottom-sheet'
|
||||||
import {useStores} from 'state/index'
|
import {useStores} from 'state/index'
|
||||||
|
@ -92,13 +93,22 @@ export const ModalsContainer = observer(function ModalsContainer() {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (snapPoints[0] === 'fullscreen') {
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={[styles.fullscreenContainer, pal.view]}>
|
||||||
|
{element}
|
||||||
|
</SafeAreaView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BottomSheet
|
<BottomSheet
|
||||||
ref={bottomSheetRef}
|
ref={bottomSheetRef}
|
||||||
snapPoints={snapPoints}
|
snapPoints={snapPoints}
|
||||||
index={store.shell.isModalActive ? 0 : -1}
|
index={store.shell.isModalActive ? 0 : -1}
|
||||||
enablePanDownToClose
|
enablePanDownToClose
|
||||||
keyboardBehavior="fillParent"
|
keyboardBehavior="extend"
|
||||||
|
keyboardBlurBehavior="restore"
|
||||||
backdropComponent={
|
backdropComponent={
|
||||||
store.shell.isModalActive ? createCustomBackdrop(onClose) : undefined
|
store.shell.isModalActive ? createCustomBackdrop(onClose) : undefined
|
||||||
}
|
}
|
||||||
|
@ -115,4 +125,11 @@ const styles = StyleSheet.create({
|
||||||
borderTopLeftRadius: 10,
|
borderTopLeftRadius: 10,
|
||||||
borderTopRightRadius: 10,
|
borderTopRightRadius: 10,
|
||||||
},
|
},
|
||||||
|
fullscreenContainer: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -128,7 +128,7 @@ const styles = StyleSheet.create({
|
||||||
width: 24,
|
width: 24,
|
||||||
height: 24,
|
height: 24,
|
||||||
bottom: 8,
|
bottom: 8,
|
||||||
right: 8,
|
right: 24,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import React, {useEffect, useState} from 'react'
|
import React, {useEffect, useState} from 'react'
|
||||||
import {View} from 'react-native'
|
import {Pressable, StyleSheet, View} from 'react-native'
|
||||||
import {Selector} from './Selector'
|
|
||||||
import {HorzSwipe} from './gestures/HorzSwipe'
|
|
||||||
import {FlatList} from './Views'
|
import {FlatList} from './Views'
|
||||||
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
|
|
||||||
import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
|
import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
|
||||||
|
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
|
||||||
|
import {Text} from './text/Text'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {clamp} from 'lib/numbers'
|
import {clamp} from 'lib/numbers'
|
||||||
import {s} from 'lib/styles'
|
import {s, colors} from 'lib/styles'
|
||||||
|
import {isAndroid} from 'platform/detection'
|
||||||
|
|
||||||
const HEADER_ITEM = {_reactKey: '__header__'}
|
const HEADER_ITEM = {_reactKey: '__header__'}
|
||||||
const SELECTOR_ITEM = {_reactKey: '__selector__'}
|
const SELECTOR_ITEM = {_reactKey: '__selector__'}
|
||||||
|
@ -16,7 +17,6 @@ export function ViewSelector({
|
||||||
sections,
|
sections,
|
||||||
items,
|
items,
|
||||||
refreshing,
|
refreshing,
|
||||||
swipeEnabled,
|
|
||||||
renderHeader,
|
renderHeader,
|
||||||
renderItem,
|
renderItem,
|
||||||
ListFooterComponent,
|
ListFooterComponent,
|
||||||
|
@ -42,19 +42,12 @@ export function ViewSelector({
|
||||||
onEndReached?: (info: {distanceFromEnd: number}) => void
|
onEndReached?: (info: {distanceFromEnd: number}) => void
|
||||||
}) {
|
}) {
|
||||||
const [selectedIndex, setSelectedIndex] = useState<number>(0)
|
const [selectedIndex, setSelectedIndex] = useState<number>(0)
|
||||||
const panX = useAnimatedValue(0)
|
|
||||||
|
|
||||||
// events
|
// events
|
||||||
// =
|
// =
|
||||||
|
|
||||||
const onSwipeEnd = React.useCallback(
|
const keyExtractor = React.useCallback(item => item._reactKey, [])
|
||||||
(dx: number) => {
|
|
||||||
if (dx !== 0) {
|
|
||||||
setSelectedIndex(clamp(selectedIndex + dx, 0, sections.length))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[setSelectedIndex, selectedIndex, sections],
|
|
||||||
)
|
|
||||||
const onPressSelection = React.useCallback(
|
const onPressSelection = React.useCallback(
|
||||||
(index: number) => setSelectedIndex(clamp(index, 0, sections.length)),
|
(index: number) => setSelectedIndex(clamp(index, 0, sections.length)),
|
||||||
[setSelectedIndex, sections],
|
[setSelectedIndex, sections],
|
||||||
|
@ -77,7 +70,6 @@ export function ViewSelector({
|
||||||
return (
|
return (
|
||||||
<Selector
|
<Selector
|
||||||
items={sections}
|
items={sections}
|
||||||
panX={panX}
|
|
||||||
selectedIndex={selectedIndex}
|
selectedIndex={selectedIndex}
|
||||||
onSelect={onPressSelection}
|
onSelect={onPressSelection}
|
||||||
/>
|
/>
|
||||||
|
@ -86,7 +78,7 @@ export function ViewSelector({
|
||||||
return renderItem(item)
|
return renderItem(item)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[sections, panX, selectedIndex, onPressSelection, renderHeader, renderItem],
|
[sections, selectedIndex, onPressSelection, renderHeader, renderItem],
|
||||||
)
|
)
|
||||||
|
|
||||||
const data = React.useMemo(
|
const data = React.useMemo(
|
||||||
|
@ -94,19 +86,13 @@ export function ViewSelector({
|
||||||
[items],
|
[items],
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<HorzSwipe
|
|
||||||
hasPriority
|
|
||||||
panX={panX}
|
|
||||||
swipeEnabled={swipeEnabled || false}
|
|
||||||
canSwipeLeft={selectedIndex > 0}
|
|
||||||
canSwipeRight={selectedIndex < sections.length - 1}
|
|
||||||
onSwipeEnd={onSwipeEnd}>
|
|
||||||
<FlatList
|
<FlatList
|
||||||
data={data}
|
data={data}
|
||||||
keyExtractor={item => item._reactKey}
|
keyExtractor={keyExtractor}
|
||||||
renderItem={renderItemInternal}
|
renderItem={renderItemInternal}
|
||||||
ListFooterComponent={ListFooterComponent}
|
ListFooterComponent={ListFooterComponent}
|
||||||
stickyHeaderIndices={STICKY_HEADER_INDICES}
|
// NOTE sticky header disabled on android due to major performance issues -prf
|
||||||
|
stickyHeaderIndices={isAndroid ? undefined : STICKY_HEADER_INDICES}
|
||||||
refreshing={refreshing}
|
refreshing={refreshing}
|
||||||
onScroll={onScroll}
|
onScroll={onScroll}
|
||||||
onRefresh={onRefresh}
|
onRefresh={onRefresh}
|
||||||
|
@ -116,6 +102,82 @@ export function ViewSelector({
|
||||||
removeClippedSubviews={true}
|
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
|
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
|
||||||
/>
|
/>
|
||||||
</HorzSwipe>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function Selector({
|
||||||
|
selectedIndex,
|
||||||
|
items,
|
||||||
|
onSelect,
|
||||||
|
}: {
|
||||||
|
selectedIndex: number
|
||||||
|
items: string[]
|
||||||
|
onSelect?: (index: number) => void
|
||||||
|
}) {
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const borderColor = useColorSchemeStyle(
|
||||||
|
{borderColor: colors.black},
|
||||||
|
{borderColor: colors.white},
|
||||||
|
)
|
||||||
|
|
||||||
|
const onPressItem = (index: number) => {
|
||||||
|
onSelect?.(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[pal.view, styles.outer]}>
|
||||||
|
{items.map((item, i) => {
|
||||||
|
const selected = i === selectedIndex
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
testID={`selector-${i}`}
|
||||||
|
key={item}
|
||||||
|
onPress={() => onPressItem(i)}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.item,
|
||||||
|
selected && styles.itemSelected,
|
||||||
|
borderColor,
|
||||||
|
]}>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
selected
|
||||||
|
? [styles.labelSelected, pal.text]
|
||||||
|
: [styles.label, pal.textLight]
|
||||||
|
}>
|
||||||
|
{item}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
outer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
},
|
||||||
|
item: {
|
||||||
|
marginRight: 14,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
paddingTop: 8,
|
||||||
|
paddingBottom: 12,
|
||||||
|
},
|
||||||
|
itemSelected: {
|
||||||
|
borderBottomWidth: 3,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
labelSelected: {
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
underline: {
|
||||||
|
position: 'absolute',
|
||||||
|
height: 4,
|
||||||
|
bottom: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
|
@ -1,157 +0,0 @@
|
||||||
import React, {useState} from 'react'
|
|
||||||
import {
|
|
||||||
Animated,
|
|
||||||
GestureResponderEvent,
|
|
||||||
I18nManager,
|
|
||||||
PanResponder,
|
|
||||||
PanResponderGestureState,
|
|
||||||
useWindowDimensions,
|
|
||||||
View,
|
|
||||||
} from 'react-native'
|
|
||||||
import {clamp} from 'lodash'
|
|
||||||
import {s} from 'lib/styles'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
panX: Animated.Value
|
|
||||||
canSwipeLeft?: boolean
|
|
||||||
canSwipeRight?: boolean
|
|
||||||
swipeEnabled?: boolean
|
|
||||||
hasPriority?: boolean // if has priority, will not release control of the gesture to another gesture
|
|
||||||
distThresholdDivisor?: number
|
|
||||||
useNativeDriver?: boolean
|
|
||||||
onSwipeStart?: () => void
|
|
||||||
onSwipeStartDirection?: (dx: number) => void
|
|
||||||
onSwipeEnd?: (dx: number) => void
|
|
||||||
children: React.ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export function HorzSwipe({
|
|
||||||
panX,
|
|
||||||
canSwipeLeft = false,
|
|
||||||
canSwipeRight = false,
|
|
||||||
swipeEnabled = true,
|
|
||||||
hasPriority = false,
|
|
||||||
distThresholdDivisor = 1.75,
|
|
||||||
useNativeDriver = false,
|
|
||||||
onSwipeStart,
|
|
||||||
onSwipeStartDirection,
|
|
||||||
onSwipeEnd,
|
|
||||||
children,
|
|
||||||
}: Props) {
|
|
||||||
const winDim = useWindowDimensions()
|
|
||||||
const [dir, setDir] = useState<number>(0)
|
|
||||||
|
|
||||||
const swipeVelocityThreshold = 35
|
|
||||||
const swipeDistanceThreshold = winDim.width / distThresholdDivisor
|
|
||||||
|
|
||||||
const isMovingHorizontally = (
|
|
||||||
_: GestureResponderEvent,
|
|
||||||
gestureState: PanResponderGestureState,
|
|
||||||
) => {
|
|
||||||
return (
|
|
||||||
Math.abs(gestureState.dx) > Math.abs(gestureState.dy * 1.25) &&
|
|
||||||
Math.abs(gestureState.vx) > Math.abs(gestureState.vy * 1.25)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const canMoveScreen = (
|
|
||||||
event: GestureResponderEvent,
|
|
||||||
gestureState: PanResponderGestureState,
|
|
||||||
) => {
|
|
||||||
if (swipeEnabled === false) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const diffX = I18nManager.isRTL ? -gestureState.dx : gestureState.dx
|
|
||||||
const willHandle =
|
|
||||||
isMovingHorizontally(event, gestureState) &&
|
|
||||||
((diffX > 0 && canSwipeLeft) || (diffX < 0 && canSwipeRight))
|
|
||||||
return willHandle
|
|
||||||
}
|
|
||||||
|
|
||||||
const startGesture = () => {
|
|
||||||
setDir(0)
|
|
||||||
onSwipeStart?.()
|
|
||||||
|
|
||||||
panX.stopAnimation()
|
|
||||||
// @ts-expect-error: _value is private, but docs use it as well
|
|
||||||
panX.setOffset(panX._value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const respondToGesture = (
|
|
||||||
_: GestureResponderEvent,
|
|
||||||
gestureState: PanResponderGestureState,
|
|
||||||
) => {
|
|
||||||
const diffX = I18nManager.isRTL ? -gestureState.dx : gestureState.dx
|
|
||||||
|
|
||||||
if (
|
|
||||||
// swiping left
|
|
||||||
(diffX > 0 && !canSwipeLeft) ||
|
|
||||||
// swiping right
|
|
||||||
(diffX < 0 && !canSwipeRight)
|
|
||||||
) {
|
|
||||||
panX.setValue(0)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
panX.setValue(clamp(diffX / swipeDistanceThreshold, -1, 1) * -1)
|
|
||||||
|
|
||||||
const newDir = diffX > 0 ? -1 : diffX < 0 ? 1 : 0
|
|
||||||
if (newDir !== dir) {
|
|
||||||
setDir(newDir)
|
|
||||||
onSwipeStartDirection?.(newDir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const finishGesture = (
|
|
||||||
_: GestureResponderEvent,
|
|
||||||
gestureState: PanResponderGestureState,
|
|
||||||
) => {
|
|
||||||
if (
|
|
||||||
Math.abs(gestureState.dx) > Math.abs(gestureState.dy) &&
|
|
||||||
Math.abs(gestureState.vx) > Math.abs(gestureState.vy) &&
|
|
||||||
(Math.abs(gestureState.dx) > swipeDistanceThreshold / 4 ||
|
|
||||||
Math.abs(gestureState.vx) > swipeVelocityThreshold)
|
|
||||||
) {
|
|
||||||
const final = Math.floor(
|
|
||||||
(gestureState.dx / Math.abs(gestureState.dx)) * -1,
|
|
||||||
)
|
|
||||||
Animated.timing(panX, {
|
|
||||||
toValue: final,
|
|
||||||
duration: 100,
|
|
||||||
useNativeDriver,
|
|
||||||
isInteraction: false,
|
|
||||||
}).start(() => {
|
|
||||||
onSwipeEnd?.(final)
|
|
||||||
panX.flattenOffset()
|
|
||||||
panX.setValue(0)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
onSwipeEnd?.(0)
|
|
||||||
Animated.timing(panX, {
|
|
||||||
toValue: 0,
|
|
||||||
duration: 100,
|
|
||||||
useNativeDriver,
|
|
||||||
isInteraction: false,
|
|
||||||
}).start(() => {
|
|
||||||
panX.flattenOffset()
|
|
||||||
panX.setValue(0)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const panResponder = PanResponder.create({
|
|
||||||
onMoveShouldSetPanResponder: canMoveScreen,
|
|
||||||
onPanResponderGrant: startGesture,
|
|
||||||
onPanResponderMove: respondToGesture,
|
|
||||||
onPanResponderTerminate: finishGesture,
|
|
||||||
onPanResponderRelease: finishGesture,
|
|
||||||
onPanResponderTerminationRequest: () => !hasPriority,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View {...panResponder.panHandlers} style={s.h100pct}>
|
|
||||||
{children}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,302 +0,0 @@
|
||||||
import React, {useState} from 'react'
|
|
||||||
import {
|
|
||||||
Animated,
|
|
||||||
GestureResponderEvent,
|
|
||||||
I18nManager,
|
|
||||||
PanResponder,
|
|
||||||
PanResponderGestureState,
|
|
||||||
useWindowDimensions,
|
|
||||||
View,
|
|
||||||
} from 'react-native'
|
|
||||||
import {clamp} from 'lodash'
|
|
||||||
import {s} from 'lib/styles'
|
|
||||||
|
|
||||||
export enum Dir {
|
|
||||||
None,
|
|
||||||
Up,
|
|
||||||
Down,
|
|
||||||
Left,
|
|
||||||
Right,
|
|
||||||
Zoom,
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
panX: Animated.Value
|
|
||||||
panY: Animated.Value
|
|
||||||
zoom: Animated.Value
|
|
||||||
canSwipeLeft?: boolean
|
|
||||||
canSwipeRight?: boolean
|
|
||||||
canSwipeUp?: boolean
|
|
||||||
canSwipeDown?: boolean
|
|
||||||
swipeEnabled?: boolean
|
|
||||||
zoomEnabled?: boolean
|
|
||||||
hasPriority?: boolean // if has priority, will not release control of the gesture to another gesture
|
|
||||||
horzDistThresholdDivisor?: number
|
|
||||||
vertDistThresholdDivisor?: number
|
|
||||||
useNativeDriver?: boolean
|
|
||||||
onSwipeStart?: () => void
|
|
||||||
onSwipeStartDirection?: (dir: Dir) => void
|
|
||||||
onSwipeEnd?: (dir: Dir) => void
|
|
||||||
children: React.ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SwipeAndZoom({
|
|
||||||
panX,
|
|
||||||
panY,
|
|
||||||
zoom,
|
|
||||||
canSwipeLeft = false,
|
|
||||||
canSwipeRight = false,
|
|
||||||
canSwipeUp = false,
|
|
||||||
canSwipeDown = false,
|
|
||||||
swipeEnabled = false,
|
|
||||||
zoomEnabled = false,
|
|
||||||
hasPriority = false,
|
|
||||||
horzDistThresholdDivisor = 1.75,
|
|
||||||
vertDistThresholdDivisor = 1.75,
|
|
||||||
useNativeDriver = false,
|
|
||||||
onSwipeStart,
|
|
||||||
onSwipeStartDirection,
|
|
||||||
onSwipeEnd,
|
|
||||||
children,
|
|
||||||
}: Props) {
|
|
||||||
const winDim = useWindowDimensions()
|
|
||||||
const [dir, setDir] = useState<Dir>(Dir.None)
|
|
||||||
const [initialDistance, setInitialDistance] = useState<number | undefined>(
|
|
||||||
undefined,
|
|
||||||
)
|
|
||||||
|
|
||||||
const swipeVelocityThreshold = 35
|
|
||||||
const swipeHorzDistanceThreshold = winDim.width / horzDistThresholdDivisor
|
|
||||||
const swipeVertDistanceThreshold = winDim.height / vertDistThresholdDivisor
|
|
||||||
|
|
||||||
const isMovingHorizontally = (
|
|
||||||
_: GestureResponderEvent,
|
|
||||||
gestureState: PanResponderGestureState,
|
|
||||||
) => {
|
|
||||||
return (
|
|
||||||
Math.abs(gestureState.dx) > Math.abs(gestureState.dy * 1.25) &&
|
|
||||||
Math.abs(gestureState.vx) > Math.abs(gestureState.vy * 1.25)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const isMovingVertically = (
|
|
||||||
_: GestureResponderEvent,
|
|
||||||
gestureState: PanResponderGestureState,
|
|
||||||
) => {
|
|
||||||
return (
|
|
||||||
Math.abs(gestureState.dy) > Math.abs(gestureState.dx * 1.25) &&
|
|
||||||
Math.abs(gestureState.vy) > Math.abs(gestureState.vx * 1.25)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const canDir = (d: Dir) => {
|
|
||||||
if (d === Dir.Left) {
|
|
||||||
return canSwipeLeft
|
|
||||||
}
|
|
||||||
if (d === Dir.Right) {
|
|
||||||
return canSwipeRight
|
|
||||||
}
|
|
||||||
if (d === Dir.Up) {
|
|
||||||
return canSwipeUp
|
|
||||||
}
|
|
||||||
if (d === Dir.Down) {
|
|
||||||
return canSwipeDown
|
|
||||||
}
|
|
||||||
if (d === Dir.Zoom) {
|
|
||||||
return zoomEnabled
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const isHorz = (d: Dir) => d === Dir.Left || d === Dir.Right
|
|
||||||
const isVert = (d: Dir) => d === Dir.Up || d === Dir.Down
|
|
||||||
|
|
||||||
const canMoveScreen = (
|
|
||||||
event: GestureResponderEvent,
|
|
||||||
gestureState: PanResponderGestureState,
|
|
||||||
) => {
|
|
||||||
if (zoomEnabled && gestureState.numberActiveTouches === 2) {
|
|
||||||
return true
|
|
||||||
} else if (swipeEnabled && gestureState.numberActiveTouches === 1) {
|
|
||||||
const dx = I18nManager.isRTL ? -gestureState.dx : gestureState.dx
|
|
||||||
const dy = gestureState.dy
|
|
||||||
const willHandle =
|
|
||||||
(isMovingHorizontally(event, gestureState) &&
|
|
||||||
((dx > 0 && canSwipeLeft) || (dx < 0 && canSwipeRight))) ||
|
|
||||||
(isMovingVertically(event, gestureState) &&
|
|
||||||
((dy > 0 && canSwipeUp) || (dy < 0 && canSwipeDown)))
|
|
||||||
return willHandle
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const startGesture = () => {
|
|
||||||
setDir(Dir.None)
|
|
||||||
onSwipeStart?.()
|
|
||||||
|
|
||||||
// reset all state
|
|
||||||
panX.stopAnimation()
|
|
||||||
// @ts-expect-error: _value is private, but docs use it as well
|
|
||||||
panX.setOffset(panX._value)
|
|
||||||
panY.stopAnimation()
|
|
||||||
// @ts-expect-error: _value is private, but docs use it as well
|
|
||||||
panY.setOffset(panY._value)
|
|
||||||
zoom.stopAnimation()
|
|
||||||
// @ts-expect-error: _value is private, but docs use it as well
|
|
||||||
zoom.setOffset(zoom._value)
|
|
||||||
setInitialDistance(undefined)
|
|
||||||
}
|
|
||||||
|
|
||||||
const respondToGesture = (
|
|
||||||
e: GestureResponderEvent,
|
|
||||||
gestureState: PanResponderGestureState,
|
|
||||||
) => {
|
|
||||||
const dx = I18nManager.isRTL ? -gestureState.dx : gestureState.dx
|
|
||||||
const dy = gestureState.dy
|
|
||||||
|
|
||||||
let newDir = Dir.None
|
|
||||||
if (dir === Dir.None) {
|
|
||||||
// establish if the user is swiping horz or vert, or zooming
|
|
||||||
if (gestureState.numberActiveTouches === 2) {
|
|
||||||
newDir = Dir.Zoom
|
|
||||||
} else if (Math.abs(dx) > Math.abs(dy)) {
|
|
||||||
newDir = dx > 0 ? Dir.Left : Dir.Right
|
|
||||||
} else {
|
|
||||||
newDir = dy > 0 ? Dir.Up : Dir.Down
|
|
||||||
}
|
|
||||||
} else if (isHorz(dir)) {
|
|
||||||
// direction update
|
|
||||||
newDir = dx > 0 ? Dir.Left : Dir.Right
|
|
||||||
} else if (isVert(dir)) {
|
|
||||||
// direction update
|
|
||||||
newDir = dy > 0 ? Dir.Up : Dir.Down
|
|
||||||
} else {
|
|
||||||
newDir = dir
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newDir === Dir.Zoom) {
|
|
||||||
if (zoomEnabled) {
|
|
||||||
if (gestureState.numberActiveTouches === 2) {
|
|
||||||
// zoom in/out
|
|
||||||
const x0 = e.nativeEvent.touches[0].pageX
|
|
||||||
const x1 = e.nativeEvent.touches[1].pageX
|
|
||||||
const y0 = e.nativeEvent.touches[0].pageY
|
|
||||||
const y1 = e.nativeEvent.touches[1].pageY
|
|
||||||
const zoomDx = Math.abs(x0 - x1)
|
|
||||||
const zoomDy = Math.abs(y0 - y1)
|
|
||||||
const dist = Math.sqrt(zoomDx * zoomDx + zoomDy * zoomDy) / 100
|
|
||||||
if (
|
|
||||||
typeof initialDistance === 'undefined' ||
|
|
||||||
dist - initialDistance < 0
|
|
||||||
) {
|
|
||||||
setInitialDistance(dist)
|
|
||||||
} else {
|
|
||||||
zoom.setValue(dist - initialDistance)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// pan around after zooming
|
|
||||||
panX.setValue(clamp(dx / winDim.width, -1, 1) * -1)
|
|
||||||
panY.setValue(clamp(dy / winDim.height, -1, 1) * -1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (isHorz(newDir)) {
|
|
||||||
// swipe left/right
|
|
||||||
panX.setValue(
|
|
||||||
clamp(
|
|
||||||
dx / swipeHorzDistanceThreshold,
|
|
||||||
canSwipeRight ? -1 : 0,
|
|
||||||
canSwipeLeft ? 1 : 0,
|
|
||||||
) * -1,
|
|
||||||
)
|
|
||||||
panY.setValue(0)
|
|
||||||
} else if (isVert(newDir)) {
|
|
||||||
// swipe up/down
|
|
||||||
panY.setValue(
|
|
||||||
clamp(
|
|
||||||
dy / swipeVertDistanceThreshold,
|
|
||||||
canSwipeDown ? -1 : 0,
|
|
||||||
canSwipeUp ? 1 : 0,
|
|
||||||
) * -1,
|
|
||||||
)
|
|
||||||
panX.setValue(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!canDir(newDir)) {
|
|
||||||
newDir = Dir.None
|
|
||||||
}
|
|
||||||
if (newDir !== dir) {
|
|
||||||
setDir(newDir)
|
|
||||||
onSwipeStartDirection?.(newDir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const finishGesture = (
|
|
||||||
_: GestureResponderEvent,
|
|
||||||
gestureState: PanResponderGestureState,
|
|
||||||
) => {
|
|
||||||
const finish = (finalDir: Dir) => () => {
|
|
||||||
if (finalDir !== Dir.None) {
|
|
||||||
onSwipeEnd?.(finalDir)
|
|
||||||
}
|
|
||||||
setDir(Dir.None)
|
|
||||||
panX.flattenOffset()
|
|
||||||
panX.setValue(0)
|
|
||||||
panY.flattenOffset()
|
|
||||||
panY.setValue(0)
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
isHorz(dir) &&
|
|
||||||
(Math.abs(gestureState.dx) > swipeHorzDistanceThreshold / 4 ||
|
|
||||||
Math.abs(gestureState.vx) > swipeVelocityThreshold)
|
|
||||||
) {
|
|
||||||
// horizontal swipe reset
|
|
||||||
Animated.timing(panX, {
|
|
||||||
toValue: dir === Dir.Left ? -1 : 1,
|
|
||||||
duration: 100,
|
|
||||||
useNativeDriver,
|
|
||||||
}).start(finish(dir))
|
|
||||||
} else if (
|
|
||||||
isVert(dir) &&
|
|
||||||
(Math.abs(gestureState.dy) > swipeVertDistanceThreshold / 8 ||
|
|
||||||
Math.abs(gestureState.vy) > swipeVelocityThreshold)
|
|
||||||
) {
|
|
||||||
// vertical swipe reset
|
|
||||||
Animated.timing(panY, {
|
|
||||||
toValue: dir === Dir.Up ? -1 : 1,
|
|
||||||
duration: 100,
|
|
||||||
useNativeDriver,
|
|
||||||
}).start(finish(dir))
|
|
||||||
} else {
|
|
||||||
// zoom (or no direction) reset
|
|
||||||
onSwipeEnd?.(Dir.None)
|
|
||||||
Animated.timing(panX, {
|
|
||||||
toValue: 0,
|
|
||||||
duration: 100,
|
|
||||||
useNativeDriver,
|
|
||||||
}).start()
|
|
||||||
Animated.timing(panY, {
|
|
||||||
toValue: 0,
|
|
||||||
duration: 100,
|
|
||||||
useNativeDriver,
|
|
||||||
}).start()
|
|
||||||
Animated.timing(zoom, {
|
|
||||||
toValue: 0,
|
|
||||||
duration: 100,
|
|
||||||
useNativeDriver,
|
|
||||||
}).start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const panResponder = PanResponder.create({
|
|
||||||
onMoveShouldSetPanResponder: canMoveScreen,
|
|
||||||
onPanResponderGrant: startGesture,
|
|
||||||
onPanResponderMove: respondToGesture,
|
|
||||||
onPanResponderTerminate: finishGesture,
|
|
||||||
onPanResponderRelease: finishGesture,
|
|
||||||
onPanResponderTerminationRequest: () => !hasPriority,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View {...panResponder.panHandlers} style={s.h100pct}>
|
|
||||||
{children}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -18,7 +18,6 @@ import {EmptyState} from '../com/util/EmptyState'
|
||||||
import {Text} from '../com/util/text/Text'
|
import {Text} from '../com/util/text/Text'
|
||||||
import {FAB} from '../com/util/fab/FAB'
|
import {FAB} from '../com/util/fab/FAB'
|
||||||
import {s, colors} from 'lib/styles'
|
import {s, colors} from 'lib/styles'
|
||||||
import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
|
|
||||||
import {useAnalytics} from 'lib/analytics'
|
import {useAnalytics} from 'lib/analytics'
|
||||||
import {ComposeIcon2} from 'lib/icons'
|
import {ComposeIcon2} from 'lib/icons'
|
||||||
|
|
||||||
|
@ -32,7 +31,6 @@ export const ProfileScreen = withAuthRequired(
|
||||||
screen('Profile')
|
screen('Profile')
|
||||||
}, [screen])
|
}, [screen])
|
||||||
|
|
||||||
const onMainScroll = useOnMainScroll(store)
|
|
||||||
const [hasSetup, setHasSetup] = useState<boolean>(false)
|
const [hasSetup, setHasSetup] = useState<boolean>(false)
|
||||||
const uiState = React.useMemo(
|
const uiState = React.useMemo(
|
||||||
() => new ProfileUiModel(store, {user: route.params.name}),
|
() => new ProfileUiModel(store, {user: route.params.name}),
|
||||||
|
@ -68,9 +66,12 @@ export const ProfileScreen = withAuthRequired(
|
||||||
track('ProfileScreen:PressCompose')
|
track('ProfileScreen:PressCompose')
|
||||||
store.shell.openComposer({})
|
store.shell.openComposer({})
|
||||||
}, [store, track])
|
}, [store, track])
|
||||||
const onSelectView = (index: number) => {
|
const onSelectView = React.useCallback(
|
||||||
|
(index: number) => {
|
||||||
uiState.setSelectedViewIndex(index)
|
uiState.setSelectedViewIndex(index)
|
||||||
}
|
},
|
||||||
|
[uiState],
|
||||||
|
)
|
||||||
const onRefresh = React.useCallback(() => {
|
const onRefresh = React.useCallback(() => {
|
||||||
uiState
|
uiState
|
||||||
.refresh()
|
.refresh()
|
||||||
|
@ -158,7 +159,6 @@ export const ProfileScreen = withAuthRequired(
|
||||||
ListFooterComponent={Footer}
|
ListFooterComponent={Footer}
|
||||||
refreshing={uiState.isRefreshing || false}
|
refreshing={uiState.isRefreshing || false}
|
||||||
onSelectView={onSelectView}
|
onSelectView={onSelectView}
|
||||||
onScroll={onMainScroll}
|
|
||||||
onRefresh={onRefresh}
|
onRefresh={onRefresh}
|
||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
/>
|
/>
|
||||||
|
|
Loading…
Reference in New Issue