From a84b2f9f2f64b1d434c5adbb12af6f7d76ba42ea Mon Sep 17 00:00:00 2001
From: Paul Frazee <pfrazee@gmail.com>
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<Modal[]>([])
@@ -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<StateContext>(undefined)
 const controlsContext = React.createContext<ControlsContext>({
   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 (
     <stateContext.Provider value={state}>
       <controlsContext.Provider value={api}>
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<SetContext>((_: boolean) => {})
 
 export function Provider({children}: React.PropsWithChildren<{}>) {
   const [state, setState] = React.useState(false)
+
   return (
     <stateContext.Provider value={state}>
       <setContext.Provider value={setState}>{children}</setContext.Provider>
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() {
       <Lightbox />
     </>
   )
-})
+}
 
-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() {
       </View>
     </SafeAreaProvider>
   )
-})
+}
 
 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<NavigationProp>()
   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() {
       )}
     </View>
   )
-})
+}
 
 export const Shell: React.FC = observer(function ShellImpl() {
   const pageBg = useColorSchemeStyle(styles.bgLight, styles.bgDark)