From a84b2f9f2f64b1d434c5adbb12af6f7d76ba42ea Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 16 Nov 2023 08:18:59 -0800 Subject: [PATCH] Close active elems (react-query refactor) (#1926) * Refactor closeAny and closeAllActiveElements * Add close lightbox * Switch to hooks * Fixes --- src/lib/hooks/useAccountSwitcher.ts | 22 +++----------- src/state/lightbox.tsx | 8 +++-- src/state/modals/index.tsx | 30 +++++++++++++++---- src/state/models/ui/shell.ts | 13 --------- src/state/shell/composer.tsx | 11 +++++-- src/state/shell/drawer-open.tsx | 1 + src/state/util.ts | 45 +++++++++++++++++++++++++++++ src/view/shell/index.tsx | 21 +++++--------- src/view/shell/index.web.tsx | 16 ++++------ 9 files changed, 101 insertions(+), 66 deletions(-) create mode 100644 src/state/util.ts diff --git a/src/lib/hooks/useAccountSwitcher.ts b/src/lib/hooks/useAccountSwitcher.ts index 83853673..a4c80066 100644 --- a/src/lib/hooks/useAccountSwitcher.ts +++ b/src/lib/hooks/useAccountSwitcher.ts @@ -1,18 +1,13 @@ import {useCallback} from 'react' - 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 * as Toast from '#/view/com/util/Toast' +import {useCloseAllActiveElements} from '#/state/util' export function useAccountSwitcher() { const {track} = useAnalytics() - const store = useStores() - const setDrawerOpen = useSetDrawerOpen() - const {closeModal} = useModalControls() const {selectAccount, clearCurrentAccount} = useSessionApi() + const closeAllActiveElements = useCloseAllActiveElements() const onPressSwitchAccount = useCallback( async (acct: SessionAccount) => { @@ -20,23 +15,14 @@ export function useAccountSwitcher() { try { await selectAccount(acct) - setDrawerOpen(false) - closeModal() - store.shell.closeAllActiveElements() + closeAllActiveElements() Toast.show(`Signed in as ${acct.handle}`) } catch (e) { Toast.show('Sorry! We need you to enter your password.') clearCurrentAccount() // back user out to login } }, - [ - track, - store, - setDrawerOpen, - closeModal, - clearCurrentAccount, - selectAccount, - ], + [track, clearCurrentAccount, selectAccount, closeAllActiveElements], ) return {onPressSwitchAccount} diff --git a/src/state/lightbox.tsx b/src/state/lightbox.tsx index 613cd638..d5528ac2 100644 --- a/src/state/lightbox.tsx +++ b/src/state/lightbox.tsx @@ -31,10 +31,10 @@ const LightboxContext = React.createContext<{ const LightboxControlContext = React.createContext<{ openLightbox: (lightbox: Lightbox) => void - closeLightbox: () => void + closeLightbox: () => boolean }>({ openLightbox: () => {}, - closeLightbox: () => {}, + closeLightbox: () => false, }) export function Provider({children}: React.PropsWithChildren<{}>) { @@ -50,8 +50,10 @@ export function Provider({children}: React.PropsWithChildren<{}>) { ) const closeLightbox = React.useCallback(() => { + let wasActive = !!activeLightbox setActiveLightbox(null) - }, [setActiveLightbox]) + return wasActive + }, [setActiveLightbox, activeLightbox]) const state = React.useMemo( () => ({ diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx index 9dd3e419..fa86aaa3 100644 --- a/src/state/modals/index.tsx +++ b/src/state/modals/index.tsx @@ -213,10 +213,12 @@ const ModalContext = React.createContext<{ const ModalControlContext = React.createContext<{ openModal: (modal: Modal) => void - closeModal: () => void + closeModal: () => boolean + closeAllModals: () => void }>({ openModal: () => {}, - closeModal: () => {}, + closeModal: () => false, + closeAllModals: () => {}, }) /** @@ -226,6 +228,13 @@ export let unstable__openModal: (modal: Modal) => void = () => { 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<{}>) { const [isModalActive, setIsModalActive] = React.useState(false) const [activeModals, setActiveModals] = React.useState([]) @@ -238,17 +247,25 @@ export function Provider({children}: React.PropsWithChildren<{}>) { [setIsModalActive, setActiveModals], ) - unstable__openModal = openModal - const closeModal = React.useCallback(() => { let totalActiveModals = 0 + let wasActive = isModalActive setActiveModals(activeModals => { activeModals = activeModals.slice(0, -1) totalActiveModals = activeModals.length return activeModals }) 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( () => ({ @@ -262,8 +279,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) { () => ({ openModal, closeModal, + closeAllModals, }), - [openModal, closeModal], + [openModal, closeModal, closeAllModals], ) return ( diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts index 1631b8f9..18287c95 100644 --- a/src/state/models/ui/shell.ts +++ b/src/state/models/ui/shell.ts @@ -21,19 +21,6 @@ export class ShellUiModel { 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() { this.rootStore.onSessionReady(() => { if (shouldRequestEmailConfirmation(this.rootStore.session)) { diff --git a/src/state/shell/composer.tsx b/src/state/shell/composer.tsx index a350bd7f..70d77a7e 100644 --- a/src/state/shell/composer.tsx +++ b/src/state/shell/composer.tsx @@ -34,13 +34,15 @@ export interface ComposerOpts { type StateContext = ComposerOpts | undefined type ControlsContext = { openComposer: (opts: ComposerOpts) => void - closeComposer: () => void + closeComposer: () => boolean } const stateContext = React.createContext(undefined) const controlsContext = React.createContext({ openComposer(_opts: ComposerOpts) {}, - closeComposer() {}, + closeComposer() { + return false + }, }) export function Provider({children}: React.PropsWithChildren<{}>) { @@ -51,11 +53,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) { setState(opts) }, closeComposer() { + let wasOpen = !!state setState(undefined) + return wasOpen }, }), - [setState], + [setState, state], ) + return ( diff --git a/src/state/shell/drawer-open.tsx b/src/state/shell/drawer-open.tsx index a2322f68..061ff53d 100644 --- a/src/state/shell/drawer-open.tsx +++ b/src/state/shell/drawer-open.tsx @@ -8,6 +8,7 @@ const setContext = React.createContext((_: boolean) => {}) export function Provider({children}: React.PropsWithChildren<{}>) { const [state, setState] = React.useState(false) + return ( {children} diff --git a/src/state/util.ts b/src/state/util.ts new file mode 100644 index 00000000..57f4331b --- /dev/null +++ b/src/state/util.ts @@ -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]) +} diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx index ff7a7dcd..71d65a75 100644 --- a/src/view/shell/index.tsx +++ b/src/view/shell/index.tsx @@ -1,5 +1,4 @@ import React from 'react' -import {observer} from 'mobx-react-lite' import {StatusBar} from 'expo-status-bar' import { DimensionValue, @@ -11,7 +10,6 @@ import { import {useSafeAreaInsets} from 'react-native-safe-area-context' import {Drawer} from 'react-native-drawer-layout' import {useNavigationState} from '@react-navigation/native' -import {useStores} from 'state/index' import {ModalsContainer} from 'view/com/modals/Modal' import {Lightbox} from 'view/com/lightbox/Lightbox' import {ErrorBoundary} from 'view/com/util/ErrorBoundary' @@ -32,15 +30,13 @@ import { useIsDrawerSwipeDisabled, } from '#/state/shell' import {isAndroid} from 'platform/detection' -import {useModalControls} from '#/state/modals' import {useSession} from '#/state/session' +import {useCloseAnyActiveElement} from '#/state/util' -const ShellInner = observer(function ShellInnerImpl() { - const store = useStores() +function ShellInner() { const isDrawerOpen = useIsDrawerOpen() const isDrawerSwipeDisabled = useIsDrawerSwipeDisabled() const setIsDrawerOpen = useSetDrawerOpen() - const {closeModal} = useModalControls() useOTAUpdate() // this hook polls for OTA updates every few seconds const winDim = useWindowDimensions() const safeAreaInsets = useSafeAreaInsets() @@ -59,20 +55,19 @@ const ShellInner = observer(function ShellInnerImpl() { ) const canGoBack = useNavigationState(state => !isStateAtTabRoot(state)) const {hasSession} = useSession() + const closeAnyActiveElement = useCloseAnyActiveElement() React.useEffect(() => { let listener = {remove() {}} if (isAndroid) { listener = BackHandler.addEventListener('hardwareBackPress', () => { - setIsDrawerOpen(false) - closeModal() - return store.shell.closeAnyActiveElement() + return closeAnyActiveElement() }) } return () => { listener.remove() } - }, [store, setIsDrawerOpen, closeModal]) + }, [closeAnyActiveElement]) return ( <> @@ -94,9 +89,9 @@ const ShellInner = observer(function ShellInnerImpl() { ) -}) +} -export const Shell: React.FC = observer(function ShellImpl() { +export const Shell: React.FC = function ShellImpl() { const pal = usePalette('default') const theme = useTheme() return ( @@ -109,7 +104,7 @@ export const Shell: React.FC = observer(function ShellImpl() { ) -}) +} const styles = StyleSheet.create({ outerContainer: { diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx index e134358d..c0eed078 100644 --- a/src/view/shell/index.web.tsx +++ b/src/view/shell/index.web.tsx @@ -1,7 +1,6 @@ import React, {useEffect} from 'react' import {observer} from 'mobx-react-lite' import {View, StyleSheet, TouchableOpacity} from 'react-native' -import {useStores} from 'state/index' import {DesktopLeftNav} from './desktop/LeftNav' import {DesktopRightNav} from './desktop/RightNav' import {ErrorBoundary} from '../com/util/ErrorBoundary' @@ -23,28 +22,25 @@ import { useSetDrawerOpen, useOnboardingState, } from '#/state/shell' -import {useModalControls} from '#/state/modals' import {useSession} from '#/state/session' +import {useCloseAllActiveElements} from '#/state/util' -const ShellInner = observer(function ShellInnerImpl() { - const store = useStores() +function ShellInner() { const isDrawerOpen = useIsDrawerOpen() const setDrawerOpen = useSetDrawerOpen() - const {closeModal} = useModalControls() const onboardingState = useOnboardingState() const {isDesktop, isMobile} = useWebMediaQueries() const navigator = useNavigation() const {hasSession} = useSession() + const closeAllActiveElements = useCloseAllActiveElements() useAuxClick() useEffect(() => { navigator.addListener('state', () => { - setDrawerOpen(false) - closeModal() - store.shell.closeAnyActiveElement() + closeAllActiveElements() }) - }, [navigator, store.shell, setDrawerOpen, closeModal]) + }, [navigator, closeAllActiveElements]) const showBottomBar = isMobile && !onboardingState.isActive const showSideNavs = !isMobile && hasSession && !onboardingState.isActive @@ -78,7 +74,7 @@ const ShellInner = observer(function ShellInnerImpl() { )} ) -}) +} export const Shell: React.FC = observer(function ShellImpl() { const pageBg = useColorSchemeStyle(styles.bgLight, styles.bgDark)