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