Add scroll-to-top for all screens (#177)
This commit is contained in:
@@ -658,6 +658,7 @@ export const mockedRootStore = {
|
|||||||
profiles: mockedProfilesStore,
|
profiles: mockedProfilesStore,
|
||||||
linkMetas: mockedLinkMetasStore,
|
linkMetas: mockedLinkMetasStore,
|
||||||
log: mockedLogStore,
|
log: mockedLogStore,
|
||||||
|
onScreenSoftReset: jest.fn().mockReturnValue({remove: jest.fn()}),
|
||||||
} as RootStoreModel
|
} as RootStoreModel
|
||||||
|
|
||||||
export const mockedProfileUiStore = {
|
export const mockedProfileUiStore = {
|
||||||
|
|||||||
@@ -166,6 +166,16 @@ export class RootStoreModel {
|
|||||||
DeviceEventEmitter.emit('navigation')
|
DeviceEventEmitter.emit('navigation')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// a "soft reset" typically means scrolling to top and loading latest
|
||||||
|
// but it can depend on the screen
|
||||||
|
onScreenSoftReset(handler: () => void): EmitterSubscription {
|
||||||
|
return DeviceEventEmitter.addListener('screen-soft-reset', handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
emitScreenSoftReset() {
|
||||||
|
DeviceEventEmitter.emit('screen-soft-reset')
|
||||||
|
}
|
||||||
|
|
||||||
// background fetch
|
// background fetch
|
||||||
// =
|
// =
|
||||||
// - we use this to poll for unread notifications, which is not "ideal" behavior but
|
// - we use this to poll for unread notifications, which is not "ideal" behavior but
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import React, {MutableRefObject} from 'react'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {FlatList, StyleSheet, View} from 'react-native'
|
import {FlatList, StyleSheet, View} from 'react-native'
|
||||||
import {NotificationsViewModel} from '../../../state/models/notifications-view'
|
import {NotificationsViewModel} from '../../../state/models/notifications-view'
|
||||||
@@ -13,10 +13,12 @@ const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
|
|||||||
|
|
||||||
export const Feed = observer(function Feed({
|
export const Feed = observer(function Feed({
|
||||||
view,
|
view,
|
||||||
|
scrollElRef,
|
||||||
onPressTryAgain,
|
onPressTryAgain,
|
||||||
onScroll,
|
onScroll,
|
||||||
}: {
|
}: {
|
||||||
view: NotificationsViewModel
|
view: NotificationsViewModel
|
||||||
|
scrollElRef?: MutableRefObject<FlatList<any> | null>
|
||||||
onPressTryAgain?: () => void
|
onPressTryAgain?: () => void
|
||||||
onScroll?: OnScrollCb
|
onScroll?: OnScrollCb
|
||||||
}) {
|
}) {
|
||||||
@@ -66,6 +68,7 @@ export const Feed = observer(function Feed({
|
|||||||
)}
|
)}
|
||||||
{data && (
|
{data && (
|
||||||
<FlatList
|
<FlatList
|
||||||
|
ref={scrollElRef}
|
||||||
data={data}
|
data={data}
|
||||||
keyExtractor={item => item._reactKey}
|
keyExtractor={item => item._reactKey}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
|
|||||||
+1
-3
@@ -1,5 +1,4 @@
|
|||||||
import React, {MutableRefObject} from 'react'
|
import React from 'react'
|
||||||
import {FlatList} from 'react-native'
|
|
||||||
import {IconProp} from '@fortawesome/fontawesome-svg-core'
|
import {IconProp} from '@fortawesome/fontawesome-svg-core'
|
||||||
import {Home} from './screens/Home'
|
import {Home} from './screens/Home'
|
||||||
import {Contacts} from './screens/Contacts'
|
import {Contacts} from './screens/Contacts'
|
||||||
@@ -21,7 +20,6 @@ export type ScreenParams = {
|
|||||||
navIdx: string
|
navIdx: string
|
||||||
params: Record<string, any>
|
params: Record<string, any>
|
||||||
visible: boolean
|
visible: boolean
|
||||||
scrollElRef?: MutableRefObject<FlatList<any> | undefined>
|
|
||||||
}
|
}
|
||||||
export type Route = [React.FC<ScreenParams>, string, IconProp, RegExp]
|
export type Route = [React.FC<ScreenParams>, string, IconProp, RegExp]
|
||||||
export type MatchResult = {
|
export type MatchResult = {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, {useEffect} from 'react'
|
import React, {useEffect} from 'react'
|
||||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
import {FlatList, StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import useAppState from 'react-native-appstate-hook'
|
import useAppState from 'react-native-appstate-hook'
|
||||||
import LinearGradient from 'react-native-linear-gradient'
|
import LinearGradient from 'react-native-linear-gradient'
|
||||||
@@ -17,15 +17,12 @@ import {useAnalytics} from '@segment/analytics-react-native'
|
|||||||
|
|
||||||
const HITSLOP = {left: 20, top: 20, right: 20, bottom: 20}
|
const HITSLOP = {left: 20, top: 20, right: 20, bottom: 20}
|
||||||
|
|
||||||
export const Home = observer(function Home({
|
export const Home = observer(function Home({navIdx, visible}: ScreenParams) {
|
||||||
navIdx,
|
|
||||||
visible,
|
|
||||||
scrollElRef,
|
|
||||||
}: ScreenParams) {
|
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const onMainScroll = useOnMainScroll(store)
|
const onMainScroll = useOnMainScroll(store)
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
const safeAreaInsets = useSafeAreaInsets()
|
const safeAreaInsets = useSafeAreaInsets()
|
||||||
|
const scrollElRef = React.useRef<FlatList>(null)
|
||||||
const [wasVisible, setWasVisible] = React.useState<boolean>(false)
|
const [wasVisible, setWasVisible] = React.useState<boolean>(false)
|
||||||
const {appState} = useAppState({
|
const {appState} = useAppState({
|
||||||
onForeground: () => doPoll(true),
|
onForeground: () => doPoll(true),
|
||||||
@@ -45,11 +42,17 @@ export const Home = observer(function Home({
|
|||||||
[appState, visible, store],
|
[appState, visible, store],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const onSoftReset = () => {
|
||||||
|
scrollElRef.current?.scrollToOffset({offset: 0})
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const softResetSub = store.onScreenSoftReset(onSoftReset)
|
||||||
const feedCleanup = store.me.mainFeed.registerListeners()
|
const feedCleanup = store.me.mainFeed.registerListeners()
|
||||||
const pollInterval = setInterval(() => doPoll(), 15e3)
|
const pollInterval = setInterval(() => doPoll(), 15e3)
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
clearInterval(pollInterval)
|
clearInterval(pollInterval)
|
||||||
|
softResetSub.remove()
|
||||||
feedCleanup()
|
feedCleanup()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, {useEffect} from 'react'
|
import React, {useEffect} from 'react'
|
||||||
import {View} from 'react-native'
|
import {FlatList, View} from 'react-native'
|
||||||
import {ViewHeader} from '../com/util/ViewHeader'
|
import {ViewHeader} from '../com/util/ViewHeader'
|
||||||
import {Feed} from '../com/notifications/Feed'
|
import {Feed} from '../com/notifications/Feed'
|
||||||
import {useStores} from '../../state'
|
import {useStores} from '../../state'
|
||||||
@@ -11,21 +11,31 @@ import {useAnalytics} from '@segment/analytics-react-native'
|
|||||||
export const Notifications = ({navIdx, visible}: ScreenParams) => {
|
export const Notifications = ({navIdx, visible}: ScreenParams) => {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const onMainScroll = useOnMainScroll(store)
|
const onMainScroll = useOnMainScroll(store)
|
||||||
|
const scrollElRef = React.useRef<FlatList>(null)
|
||||||
const {screen} = useAnalytics()
|
const {screen} = useAnalytics()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
screen('Notifications')
|
screen('Notifications')
|
||||||
}, [screen])
|
}, [screen])
|
||||||
|
|
||||||
|
const onSoftReset = () => {
|
||||||
|
scrollElRef.current?.scrollToOffset({offset: 0})
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const softResetSub = store.onScreenSoftReset(onSoftReset)
|
||||||
|
const cleanup = () => {
|
||||||
|
softResetSub.remove()
|
||||||
|
}
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
return
|
return cleanup
|
||||||
}
|
}
|
||||||
store.log.debug('Updating notifications feed')
|
store.log.debug('Updating notifications feed')
|
||||||
store.me.notifications.update().then(() => {
|
store.me.notifications.update().then(() => {
|
||||||
store.me.notifications.updateReadState()
|
store.me.notifications.updateReadState()
|
||||||
})
|
})
|
||||||
store.nav.setTitle(navIdx, 'Notifications')
|
store.nav.setTitle(navIdx, 'Notifications')
|
||||||
|
return cleanup
|
||||||
}, [visible, store, navIdx])
|
}, [visible, store, navIdx])
|
||||||
|
|
||||||
const onPressTryAgain = () => {
|
const onPressTryAgain = () => {
|
||||||
@@ -39,6 +49,7 @@ export const Notifications = ({navIdx, visible}: ScreenParams) => {
|
|||||||
view={store.me.notifications}
|
view={store.me.notifications}
|
||||||
onPressTryAgain={onPressTryAgain}
|
onPressTryAgain={onPressTryAgain}
|
||||||
onScroll={onMainScroll}
|
onScroll={onMainScroll}
|
||||||
|
scrollElRef={scrollElRef}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export const Search = observer(({navIdx, visible, params}: ScreenParams) => {
|
|||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
|
const scrollElRef = React.useRef<ScrollView>(null)
|
||||||
const textInput = React.useRef<TextInput>(null)
|
const textInput = React.useRef<TextInput>(null)
|
||||||
const [lastRenderTime, setRenderTime] = React.useState<number>(0) // used to trigger reloads
|
const [lastRenderTime, setRenderTime] = React.useState<number>(0) // used to trigger reloads
|
||||||
const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false)
|
const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false)
|
||||||
@@ -39,7 +40,16 @@ export const Search = observer(({navIdx, visible, params}: ScreenParams) => {
|
|||||||
)
|
)
|
||||||
const {name} = params
|
const {name} = params
|
||||||
|
|
||||||
|
const onSoftReset = () => {
|
||||||
|
scrollElRef.current?.scrollTo({x: 0, y: 0})
|
||||||
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
const softResetSub = store.onScreenSoftReset(onSoftReset)
|
||||||
|
const cleanup = () => {
|
||||||
|
softResetSub.remove()
|
||||||
|
}
|
||||||
|
|
||||||
if (visible) {
|
if (visible) {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
if (lastRenderTime - now > FIVE_MIN) {
|
if (lastRenderTime - now > FIVE_MIN) {
|
||||||
@@ -49,6 +59,7 @@ export const Search = observer(({navIdx, visible, params}: ScreenParams) => {
|
|||||||
autocompleteView.setup()
|
autocompleteView.setup()
|
||||||
store.nav.setTitle(navIdx, 'Search')
|
store.nav.setTitle(navIdx, 'Search')
|
||||||
}
|
}
|
||||||
|
return cleanup
|
||||||
}, [store, visible, name, navIdx, autocompleteView, lastRenderTime])
|
}, [store, visible, name, navIdx, autocompleteView, lastRenderTime])
|
||||||
|
|
||||||
const onPressMenu = () => {
|
const onPressMenu = () => {
|
||||||
@@ -143,7 +154,7 @@ export const Search = observer(({navIdx, visible, params}: ScreenParams) => {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<ScrollView onScroll={Keyboard.dismiss}>
|
<ScrollView onScroll={Keyboard.dismiss} ref={scrollElRef}>
|
||||||
<WhoToFollow key={`wtf-${lastRenderTime}`} />
|
<WhoToFollow key={`wtf-${lastRenderTime}`} />
|
||||||
<SuggestedPosts key={`sp-${lastRenderTime}`} />
|
<SuggestedPosts key={`sp-${lastRenderTime}`} />
|
||||||
<View style={s.footerSpacer} />
|
<View style={s.footerSpacer} />
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import React, {useState, useEffect, useRef} from 'react'
|
import React, {useState, useEffect} from 'react'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
import {
|
import {
|
||||||
Animated,
|
Animated,
|
||||||
Easing,
|
Easing,
|
||||||
FlatList,
|
|
||||||
GestureResponderEvent,
|
GestureResponderEvent,
|
||||||
StatusBar,
|
StatusBar,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
@@ -148,7 +147,6 @@ export const MobileShell: React.FC = observer(() => {
|
|||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const [isTabsSelectorActive, setTabsSelectorActive] = useState(false)
|
const [isTabsSelectorActive, setTabsSelectorActive] = useState(false)
|
||||||
const scrollElRef = useRef<FlatList | undefined>()
|
|
||||||
const winDim = useWindowDimensions()
|
const winDim = useWindowDimensions()
|
||||||
const [menuSwipingDirection, setMenuSwipingDirection] = useState(0)
|
const [menuSwipingDirection, setMenuSwipingDirection] = useState(0)
|
||||||
const swipeGestureInterp = useAnimatedValue(0)
|
const swipeGestureInterp = useAnimatedValue(0)
|
||||||
@@ -164,8 +162,8 @@ export const MobileShell: React.FC = observer(() => {
|
|||||||
const onPressHome = () => {
|
const onPressHome = () => {
|
||||||
track('MobileShell:HomeButtonPressed')
|
track('MobileShell:HomeButtonPressed')
|
||||||
if (store.nav.tab.fixedTabPurpose === TabPurpose.Default) {
|
if (store.nav.tab.fixedTabPurpose === TabPurpose.Default) {
|
||||||
if (store.nav.tab.current.url === '/') {
|
if (!store.nav.tab.canGoBack) {
|
||||||
scrollElRef.current?.scrollToOffset({offset: 0})
|
store.emitScreenSoftReset()
|
||||||
} else {
|
} else {
|
||||||
store.nav.tab.fixedTabReset()
|
store.nav.tab.fixedTabReset()
|
||||||
}
|
}
|
||||||
@@ -179,8 +177,8 @@ export const MobileShell: React.FC = observer(() => {
|
|||||||
const onPressSearch = () => {
|
const onPressSearch = () => {
|
||||||
track('MobileShell:SearchButtonPressed')
|
track('MobileShell:SearchButtonPressed')
|
||||||
if (store.nav.tab.fixedTabPurpose === TabPurpose.Search) {
|
if (store.nav.tab.fixedTabPurpose === TabPurpose.Search) {
|
||||||
if (store.nav.tab.current.url === '/') {
|
if (!store.nav.tab.canGoBack) {
|
||||||
scrollElRef.current?.scrollToOffset({offset: 0})
|
store.emitScreenSoftReset()
|
||||||
} else {
|
} else {
|
||||||
store.nav.tab.fixedTabReset()
|
store.nav.tab.fixedTabReset()
|
||||||
}
|
}
|
||||||
@@ -194,7 +192,11 @@ export const MobileShell: React.FC = observer(() => {
|
|||||||
const onPressNotifications = () => {
|
const onPressNotifications = () => {
|
||||||
track('MobileShell:NotificationsButtonPressed')
|
track('MobileShell:NotificationsButtonPressed')
|
||||||
if (store.nav.tab.fixedTabPurpose === TabPurpose.Notifs) {
|
if (store.nav.tab.fixedTabPurpose === TabPurpose.Notifs) {
|
||||||
store.nav.tab.fixedTabReset()
|
if (!store.nav.tab.canGoBack) {
|
||||||
|
store.emitScreenSoftReset()
|
||||||
|
} else {
|
||||||
|
store.nav.tab.fixedTabReset()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
store.nav.switchTo(TabPurpose.Notifs, false)
|
store.nav.switchTo(TabPurpose.Notifs, false)
|
||||||
if (store.nav.tab.index === 0) {
|
if (store.nav.tab.index === 0) {
|
||||||
@@ -444,7 +446,6 @@ export const MobileShell: React.FC = observer(() => {
|
|||||||
params={params}
|
params={params}
|
||||||
navIdx={navIdx}
|
navIdx={navIdx}
|
||||||
visible={current}
|
visible={current}
|
||||||
scrollElRef={current ? scrollElRef : undefined}
|
|
||||||
/>
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|||||||
Reference in New Issue
Block a user