Close active elems (react-query refactor) (#1926)

* Refactor closeAny and closeAllActiveElements

* Add close lightbox

* Switch to hooks

* Fixes
zio/stable
Paul Frazee 2023-11-16 08:18:59 -08:00 committed by GitHub
parent 0de8d40981
commit a84b2f9f2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 101 additions and 66 deletions

View File

@ -1,18 +1,13 @@
import {useCallback} from 'react' import {useCallback} from 'react'
import {useAnalytics} from '#/lib/analytics/analytics' import {useAnalytics} from '#/lib/analytics/analytics'
import {useStores} from '#/state/index'
import {useSetDrawerOpen} from '#/state/shell/drawer-open'
import {useModalControls} from '#/state/modals'
import {useSessionApi, SessionAccount} from '#/state/session' import {useSessionApi, SessionAccount} from '#/state/session'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
import {useCloseAllActiveElements} from '#/state/util'
export function useAccountSwitcher() { export function useAccountSwitcher() {
const {track} = useAnalytics() const {track} = useAnalytics()
const store = useStores()
const setDrawerOpen = useSetDrawerOpen()
const {closeModal} = useModalControls()
const {selectAccount, clearCurrentAccount} = useSessionApi() const {selectAccount, clearCurrentAccount} = useSessionApi()
const closeAllActiveElements = useCloseAllActiveElements()
const onPressSwitchAccount = useCallback( const onPressSwitchAccount = useCallback(
async (acct: SessionAccount) => { async (acct: SessionAccount) => {
@ -20,23 +15,14 @@ export function useAccountSwitcher() {
try { try {
await selectAccount(acct) await selectAccount(acct)
setDrawerOpen(false) closeAllActiveElements()
closeModal()
store.shell.closeAllActiveElements()
Toast.show(`Signed in as ${acct.handle}`) Toast.show(`Signed in as ${acct.handle}`)
} catch (e) { } catch (e) {
Toast.show('Sorry! We need you to enter your password.') Toast.show('Sorry! We need you to enter your password.')
clearCurrentAccount() // back user out to login clearCurrentAccount() // back user out to login
} }
}, },
[ [track, clearCurrentAccount, selectAccount, closeAllActiveElements],
track,
store,
setDrawerOpen,
closeModal,
clearCurrentAccount,
selectAccount,
],
) )
return {onPressSwitchAccount} return {onPressSwitchAccount}

View File

@ -31,10 +31,10 @@ const LightboxContext = React.createContext<{
const LightboxControlContext = React.createContext<{ const LightboxControlContext = React.createContext<{
openLightbox: (lightbox: Lightbox) => void openLightbox: (lightbox: Lightbox) => void
closeLightbox: () => void closeLightbox: () => boolean
}>({ }>({
openLightbox: () => {}, openLightbox: () => {},
closeLightbox: () => {}, closeLightbox: () => false,
}) })
export function Provider({children}: React.PropsWithChildren<{}>) { export function Provider({children}: React.PropsWithChildren<{}>) {
@ -50,8 +50,10 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
) )
const closeLightbox = React.useCallback(() => { const closeLightbox = React.useCallback(() => {
let wasActive = !!activeLightbox
setActiveLightbox(null) setActiveLightbox(null)
}, [setActiveLightbox]) return wasActive
}, [setActiveLightbox, activeLightbox])
const state = React.useMemo( const state = React.useMemo(
() => ({ () => ({

View File

@ -213,10 +213,12 @@ const ModalContext = React.createContext<{
const ModalControlContext = React.createContext<{ const ModalControlContext = React.createContext<{
openModal: (modal: Modal) => void openModal: (modal: Modal) => void
closeModal: () => void closeModal: () => boolean
closeAllModals: () => void
}>({ }>({
openModal: () => {}, openModal: () => {},
closeModal: () => {}, closeModal: () => false,
closeAllModals: () => {},
}) })
/** /**
@ -226,6 +228,13 @@ export let unstable__openModal: (modal: Modal) => void = () => {
throw new Error(`ModalContext is not initialized`) throw new Error(`ModalContext is not initialized`)
} }
/**
* @deprecated DO NOT USE THIS unless you have no other choice.
*/
export let unstable__closeModal: () => boolean = () => {
throw new Error(`ModalContext is not initialized`)
}
export function Provider({children}: React.PropsWithChildren<{}>) { export function Provider({children}: React.PropsWithChildren<{}>) {
const [isModalActive, setIsModalActive] = React.useState(false) const [isModalActive, setIsModalActive] = React.useState(false)
const [activeModals, setActiveModals] = React.useState<Modal[]>([]) const [activeModals, setActiveModals] = React.useState<Modal[]>([])
@ -238,17 +247,25 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
[setIsModalActive, setActiveModals], [setIsModalActive, setActiveModals],
) )
unstable__openModal = openModal
const closeModal = React.useCallback(() => { const closeModal = React.useCallback(() => {
let totalActiveModals = 0 let totalActiveModals = 0
let wasActive = isModalActive
setActiveModals(activeModals => { setActiveModals(activeModals => {
activeModals = activeModals.slice(0, -1) activeModals = activeModals.slice(0, -1)
totalActiveModals = activeModals.length totalActiveModals = activeModals.length
return activeModals return activeModals
}) })
setIsModalActive(totalActiveModals > 0) setIsModalActive(totalActiveModals > 0)
}, [setIsModalActive, setActiveModals]) return wasActive
}, [setIsModalActive, setActiveModals, isModalActive])
const closeAllModals = React.useCallback(() => {
setActiveModals([])
setIsModalActive(false)
}, [setActiveModals, setIsModalActive])
unstable__openModal = openModal
unstable__closeModal = closeModal
const state = React.useMemo( const state = React.useMemo(
() => ({ () => ({
@ -262,8 +279,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
() => ({ () => ({
openModal, openModal,
closeModal, closeModal,
closeAllModals,
}), }),
[openModal, closeModal], [openModal, closeModal, closeAllModals],
) )
return ( return (

View File

@ -21,19 +21,6 @@ export class ShellUiModel {
this.setupLoginModals() this.setupLoginModals()
} }
/**
* returns true if something was closed
* (used by the android hardware back btn)
*/
closeAnyActiveElement(): boolean {
return false
}
/**
* used to clear out any modals, eg for a navigation
*/
closeAllActiveElements() {}
setupLoginModals() { setupLoginModals() {
this.rootStore.onSessionReady(() => { this.rootStore.onSessionReady(() => {
if (shouldRequestEmailConfirmation(this.rootStore.session)) { if (shouldRequestEmailConfirmation(this.rootStore.session)) {

View File

@ -34,13 +34,15 @@ export interface ComposerOpts {
type StateContext = ComposerOpts | undefined type StateContext = ComposerOpts | undefined
type ControlsContext = { type ControlsContext = {
openComposer: (opts: ComposerOpts) => void openComposer: (opts: ComposerOpts) => void
closeComposer: () => void closeComposer: () => boolean
} }
const stateContext = React.createContext<StateContext>(undefined) const stateContext = React.createContext<StateContext>(undefined)
const controlsContext = React.createContext<ControlsContext>({ const controlsContext = React.createContext<ControlsContext>({
openComposer(_opts: ComposerOpts) {}, openComposer(_opts: ComposerOpts) {},
closeComposer() {}, closeComposer() {
return false
},
}) })
export function Provider({children}: React.PropsWithChildren<{}>) { export function Provider({children}: React.PropsWithChildren<{}>) {
@ -51,11 +53,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
setState(opts) setState(opts)
}, },
closeComposer() { closeComposer() {
let wasOpen = !!state
setState(undefined) setState(undefined)
return wasOpen
}, },
}), }),
[setState], [setState, state],
) )
return ( return (
<stateContext.Provider value={state}> <stateContext.Provider value={state}>
<controlsContext.Provider value={api}> <controlsContext.Provider value={api}>

View File

@ -8,6 +8,7 @@ const setContext = React.createContext<SetContext>((_: boolean) => {})
export function Provider({children}: React.PropsWithChildren<{}>) { export function Provider({children}: React.PropsWithChildren<{}>) {
const [state, setState] = React.useState(false) const [state, setState] = React.useState(false)
return ( return (
<stateContext.Provider value={state}> <stateContext.Provider value={state}>
<setContext.Provider value={setState}>{children}</setContext.Provider> <setContext.Provider value={setState}>{children}</setContext.Provider>

45
src/state/util.ts 100644
View File

@ -0,0 +1,45 @@
import {useCallback} from 'react'
import {useLightboxControls} from './lightbox'
import {useModalControls} from './modals'
import {useComposerControls} from './shell/composer'
import {useSetDrawerOpen} from './shell/drawer-open'
/**
* returns true if something was closed
* (used by the android hardware back btn)
*/
export function useCloseAnyActiveElement() {
const {closeLightbox} = useLightboxControls()
const {closeModal} = useModalControls()
const {closeComposer} = useComposerControls()
const setDrawerOpen = useSetDrawerOpen()
return useCallback(() => {
if (closeLightbox()) {
return true
}
if (closeModal()) {
return true
}
if (closeComposer()) {
return true
}
setDrawerOpen(false)
return false
}, [closeLightbox, closeModal, closeComposer, setDrawerOpen])
}
/**
* used to clear out any modals, eg for a navigation
*/
export function useCloseAllActiveElements() {
const {closeLightbox} = useLightboxControls()
const {closeAllModals} = useModalControls()
const {closeComposer} = useComposerControls()
const setDrawerOpen = useSetDrawerOpen()
return useCallback(() => {
closeLightbox()
closeAllModals()
closeComposer()
setDrawerOpen(false)
}, [closeLightbox, closeAllModals, closeComposer, setDrawerOpen])
}

View File

@ -1,5 +1,4 @@
import React from 'react' import React from 'react'
import {observer} from 'mobx-react-lite'
import {StatusBar} from 'expo-status-bar' import {StatusBar} from 'expo-status-bar'
import { import {
DimensionValue, DimensionValue,
@ -11,7 +10,6 @@ import {
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {Drawer} from 'react-native-drawer-layout' import {Drawer} from 'react-native-drawer-layout'
import {useNavigationState} from '@react-navigation/native' import {useNavigationState} from '@react-navigation/native'
import {useStores} from 'state/index'
import {ModalsContainer} from 'view/com/modals/Modal' import {ModalsContainer} from 'view/com/modals/Modal'
import {Lightbox} from 'view/com/lightbox/Lightbox' import {Lightbox} from 'view/com/lightbox/Lightbox'
import {ErrorBoundary} from 'view/com/util/ErrorBoundary' import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
@ -32,15 +30,13 @@ import {
useIsDrawerSwipeDisabled, useIsDrawerSwipeDisabled,
} from '#/state/shell' } from '#/state/shell'
import {isAndroid} from 'platform/detection' import {isAndroid} from 'platform/detection'
import {useModalControls} from '#/state/modals'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useCloseAnyActiveElement} from '#/state/util'
const ShellInner = observer(function ShellInnerImpl() { function ShellInner() {
const store = useStores()
const isDrawerOpen = useIsDrawerOpen() const isDrawerOpen = useIsDrawerOpen()
const isDrawerSwipeDisabled = useIsDrawerSwipeDisabled() const isDrawerSwipeDisabled = useIsDrawerSwipeDisabled()
const setIsDrawerOpen = useSetDrawerOpen() const setIsDrawerOpen = useSetDrawerOpen()
const {closeModal} = useModalControls()
useOTAUpdate() // this hook polls for OTA updates every few seconds useOTAUpdate() // this hook polls for OTA updates every few seconds
const winDim = useWindowDimensions() const winDim = useWindowDimensions()
const safeAreaInsets = useSafeAreaInsets() const safeAreaInsets = useSafeAreaInsets()
@ -59,20 +55,19 @@ const ShellInner = observer(function ShellInnerImpl() {
) )
const canGoBack = useNavigationState(state => !isStateAtTabRoot(state)) const canGoBack = useNavigationState(state => !isStateAtTabRoot(state))
const {hasSession} = useSession() const {hasSession} = useSession()
const closeAnyActiveElement = useCloseAnyActiveElement()
React.useEffect(() => { React.useEffect(() => {
let listener = {remove() {}} let listener = {remove() {}}
if (isAndroid) { if (isAndroid) {
listener = BackHandler.addEventListener('hardwareBackPress', () => { listener = BackHandler.addEventListener('hardwareBackPress', () => {
setIsDrawerOpen(false) return closeAnyActiveElement()
closeModal()
return store.shell.closeAnyActiveElement()
}) })
} }
return () => { return () => {
listener.remove() listener.remove()
} }
}, [store, setIsDrawerOpen, closeModal]) }, [closeAnyActiveElement])
return ( return (
<> <>
@ -94,9 +89,9 @@ const ShellInner = observer(function ShellInnerImpl() {
<Lightbox /> <Lightbox />
</> </>
) )
}) }
export const Shell: React.FC = observer(function ShellImpl() { export const Shell: React.FC = function ShellImpl() {
const pal = usePalette('default') const pal = usePalette('default')
const theme = useTheme() const theme = useTheme()
return ( return (
@ -109,7 +104,7 @@ export const Shell: React.FC = observer(function ShellImpl() {
</View> </View>
</SafeAreaProvider> </SafeAreaProvider>
) )
}) }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
outerContainer: { outerContainer: {

View File

@ -1,7 +1,6 @@
import React, {useEffect} from 'react' import React, {useEffect} from 'react'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {View, StyleSheet, TouchableOpacity} from 'react-native' import {View, StyleSheet, TouchableOpacity} from 'react-native'
import {useStores} from 'state/index'
import {DesktopLeftNav} from './desktop/LeftNav' import {DesktopLeftNav} from './desktop/LeftNav'
import {DesktopRightNav} from './desktop/RightNav' import {DesktopRightNav} from './desktop/RightNav'
import {ErrorBoundary} from '../com/util/ErrorBoundary' import {ErrorBoundary} from '../com/util/ErrorBoundary'
@ -23,28 +22,25 @@ import {
useSetDrawerOpen, useSetDrawerOpen,
useOnboardingState, useOnboardingState,
} from '#/state/shell' } from '#/state/shell'
import {useModalControls} from '#/state/modals'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {useCloseAllActiveElements} from '#/state/util'
const ShellInner = observer(function ShellInnerImpl() { function ShellInner() {
const store = useStores()
const isDrawerOpen = useIsDrawerOpen() const isDrawerOpen = useIsDrawerOpen()
const setDrawerOpen = useSetDrawerOpen() const setDrawerOpen = useSetDrawerOpen()
const {closeModal} = useModalControls()
const onboardingState = useOnboardingState() const onboardingState = useOnboardingState()
const {isDesktop, isMobile} = useWebMediaQueries() const {isDesktop, isMobile} = useWebMediaQueries()
const navigator = useNavigation<NavigationProp>() const navigator = useNavigation<NavigationProp>()
const {hasSession} = useSession() const {hasSession} = useSession()
const closeAllActiveElements = useCloseAllActiveElements()
useAuxClick() useAuxClick()
useEffect(() => { useEffect(() => {
navigator.addListener('state', () => { navigator.addListener('state', () => {
setDrawerOpen(false) closeAllActiveElements()
closeModal()
store.shell.closeAnyActiveElement()
}) })
}, [navigator, store.shell, setDrawerOpen, closeModal]) }, [navigator, closeAllActiveElements])
const showBottomBar = isMobile && !onboardingState.isActive const showBottomBar = isMobile && !onboardingState.isActive
const showSideNavs = !isMobile && hasSession && !onboardingState.isActive const showSideNavs = !isMobile && hasSession && !onboardingState.isActive
@ -78,7 +74,7 @@ const ShellInner = observer(function ShellInnerImpl() {
)} )}
</View> </View>
) )
}) }
export const Shell: React.FC = observer(function ShellImpl() { export const Shell: React.FC = observer(function ShellImpl() {
const pageBg = useColorSchemeStyle(styles.bgLight, styles.bgDark) const pageBg = useColorSchemeStyle(styles.bgLight, styles.bgDark)