about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.native.tsx5
-rw-r--r--src/App.web.tsx5
-rw-r--r--src/state/lightbox.tsx86
-rw-r--r--src/state/modals/index.tsx3
-rw-r--r--src/state/models/ui/shell.ts49
-rw-r--r--src/view/com/lightbox/Lightbox.tsx46
-rw-r--r--src/view/com/lightbox/Lightbox.web.tsx33
-rw-r--r--src/view/com/profile/ProfileHeader.tsx9
-rw-r--r--src/view/com/profile/ProfileSubpageHeader.tsx7
-rw-r--r--src/view/com/util/post-embeds/index.tsx13
10 files changed, 152 insertions, 104 deletions
diff --git a/src/App.native.tsx b/src/App.native.tsx
index ffa8b338e..3f49eb11f 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -25,6 +25,7 @@ import {queryClient} from 'lib/react-query'
 import {TestCtrls} from 'view/com/testing/TestCtrls'
 import {Provider as ShellStateProvider} from 'state/shell'
 import {Provider as ModalStateProvider} from 'state/modals'
+import {Provider as LightboxStateProvider} from 'state/lightbox'
 import {Provider as MutedThreadsProvider} from 'state/muted-threads'
 import {Provider as InvitesStateProvider} from 'state/invites'
 import {Provider as PrefsStateProvider} from 'state/preferences'
@@ -124,7 +125,9 @@ function App() {
             <MutedThreadsProvider>
               <InvitesStateProvider>
                 <ModalStateProvider>
-                  <InnerApp />
+                  <LightboxStateProvider>
+                    <InnerApp />
+                  </LightboxStateProvider>
                 </ModalStateProvider>
               </InvitesStateProvider>
             </MutedThreadsProvider>
diff --git a/src/App.web.tsx b/src/App.web.tsx
index 8e22f6480..e1f4e8030 100644
--- a/src/App.web.tsx
+++ b/src/App.web.tsx
@@ -22,6 +22,7 @@ import {I18nProvider} from '@lingui/react'
 import {defaultLocale, dynamicActivate} from './locale/i18n'
 import {Provider as ShellStateProvider} from 'state/shell'
 import {Provider as ModalStateProvider} from 'state/modals'
+import {Provider as LightboxStateProvider} from 'state/lightbox'
 import {Provider as MutedThreadsProvider} from 'state/muted-threads'
 import {Provider as InvitesStateProvider} from 'state/invites'
 import {Provider as PrefsStateProvider} from 'state/preferences'
@@ -111,7 +112,9 @@ function App() {
             <MutedThreadsProvider>
               <InvitesStateProvider>
                 <ModalStateProvider>
-                  <InnerApp />
+                  <LightboxStateProvider>
+                    <InnerApp />
+                  </LightboxStateProvider>
                 </ModalStateProvider>
               </InvitesStateProvider>
             </MutedThreadsProvider>
diff --git a/src/state/lightbox.tsx b/src/state/lightbox.tsx
new file mode 100644
index 000000000..613cd638e
--- /dev/null
+++ b/src/state/lightbox.tsx
@@ -0,0 +1,86 @@
+import React from 'react'
+import {AppBskyActorDefs} from '@atproto/api'
+
+interface Lightbox {
+  name: string
+}
+
+export class ProfileImageLightbox implements Lightbox {
+  name = 'profile-image'
+  constructor(public profile: AppBskyActorDefs.ProfileViewDetailed) {}
+}
+
+interface ImagesLightboxItem {
+  uri: string
+  alt?: string
+}
+
+export class ImagesLightbox implements Lightbox {
+  name = 'images'
+  constructor(public images: ImagesLightboxItem[], public index: number) {}
+  setIndex(index: number) {
+    this.index = index
+  }
+}
+
+const LightboxContext = React.createContext<{
+  activeLightbox: Lightbox | null
+}>({
+  activeLightbox: null,
+})
+
+const LightboxControlContext = React.createContext<{
+  openLightbox: (lightbox: Lightbox) => void
+  closeLightbox: () => void
+}>({
+  openLightbox: () => {},
+  closeLightbox: () => {},
+})
+
+export function Provider({children}: React.PropsWithChildren<{}>) {
+  const [activeLightbox, setActiveLightbox] = React.useState<Lightbox | null>(
+    null,
+  )
+
+  const openLightbox = React.useCallback(
+    (lightbox: Lightbox) => {
+      setActiveLightbox(lightbox)
+    },
+    [setActiveLightbox],
+  )
+
+  const closeLightbox = React.useCallback(() => {
+    setActiveLightbox(null)
+  }, [setActiveLightbox])
+
+  const state = React.useMemo(
+    () => ({
+      activeLightbox,
+    }),
+    [activeLightbox],
+  )
+
+  const methods = React.useMemo(
+    () => ({
+      openLightbox,
+      closeLightbox,
+    }),
+    [openLightbox, closeLightbox],
+  )
+
+  return (
+    <LightboxContext.Provider value={state}>
+      <LightboxControlContext.Provider value={methods}>
+        {children}
+      </LightboxControlContext.Provider>
+    </LightboxContext.Provider>
+  )
+}
+
+export function useLightbox() {
+  return React.useContext(LightboxContext)
+}
+
+export function useLightboxControls() {
+  return React.useContext(LightboxControlContext)
+}
diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx
index 57f486630..9dd3e4195 100644
--- a/src/state/modals/index.tsx
+++ b/src/state/modals/index.tsx
@@ -1,6 +1,6 @@
 import React from 'react'
 import {AppBskyActorDefs, AppBskyGraphDefs, ModerationUI} from '@atproto/api'
-import {StyleProp, ViewStyle, DeviceEventEmitter} from 'react-native'
+import {StyleProp, ViewStyle} from 'react-native'
 import {Image as RNImage} from 'react-native-image-crop-picker'
 
 import {ImageModel} from '#/state/models/media/image'
@@ -232,7 +232,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
 
   const openModal = React.useCallback(
     (modal: Modal) => {
-      DeviceEventEmitter.emit('navigation')
       setActiveModals(activeModals => [...activeModals, modal])
       setIsModalActive(true)
     },
diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts
index 223c20625..1631b8f9c 100644
--- a/src/state/models/ui/shell.ts
+++ b/src/state/models/ui/shell.ts
@@ -1,4 +1,3 @@
-import {AppBskyActorDefs} from '@atproto/api'
 import {RootStoreModel} from '../root-store'
 import {makeAutoObservable} from 'mobx'
 import {
@@ -13,34 +12,7 @@ export function isColorMode(v: unknown): v is ColorMode {
   return v === 'system' || v === 'light' || v === 'dark'
 }
 
-interface LightboxModel {}
-
-export class ProfileImageLightbox implements LightboxModel {
-  name = 'profile-image'
-  constructor(public profile: AppBskyActorDefs.ProfileViewDetailed) {
-    makeAutoObservable(this)
-  }
-}
-
-interface ImagesLightboxItem {
-  uri: string
-  alt?: string
-}
-
-export class ImagesLightbox implements LightboxModel {
-  name = 'images'
-  constructor(public images: ImagesLightboxItem[], public index: number) {
-    makeAutoObservable(this)
-  }
-  setIndex(index: number) {
-    this.index = index
-  }
-}
-
 export class ShellUiModel {
-  isLightboxActive = false
-  activeLightbox: ProfileImageLightbox | ImagesLightbox | null = null
-
   constructor(public rootStore: RootStoreModel) {
     makeAutoObservable(this, {
       rootStore: false,
@@ -54,32 +26,13 @@ export class ShellUiModel {
    * (used by the android hardware back btn)
    */
   closeAnyActiveElement(): boolean {
-    if (this.isLightboxActive) {
-      this.closeLightbox()
-      return true
-    }
     return false
   }
 
   /**
    * used to clear out any modals, eg for a navigation
    */
-  closeAllActiveElements() {
-    if (this.isLightboxActive) {
-      this.closeLightbox()
-    }
-  }
-
-  openLightbox(lightbox: ProfileImageLightbox | ImagesLightbox) {
-    this.rootStore.emitNavigation()
-    this.isLightboxActive = true
-    this.activeLightbox = lightbox
-  }
-
-  closeLightbox() {
-    this.isLightboxActive = false
-    this.activeLightbox = null
-  }
+  closeAllActiveElements() {}
 
   setupLoginModals() {
     this.rootStore.onSessionReady(() => {
diff --git a/src/view/com/lightbox/Lightbox.tsx b/src/view/com/lightbox/Lightbox.tsx
index 1cadbf9a6..8a18df33f 100644
--- a/src/view/com/lightbox/Lightbox.tsx
+++ b/src/view/com/lightbox/Lightbox.tsx
@@ -1,10 +1,7 @@
 import React from 'react'
 import {Pressable, StyleSheet, View} from 'react-native'
-import {observer} from 'mobx-react-lite'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import ImageView from './ImageViewing'
-import {useStores} from 'state/index'
-import * as models from 'state/models/ui/shell'
 import {shareImageModal, saveImageToMediaLibrary} from 'lib/media/manip'
 import * as Toast from '../util/Toast'
 import {Text} from '../util/text/Text'
@@ -12,17 +9,24 @@ import {s, colors} from 'lib/styles'
 import {Button} from '../util/forms/Button'
 import {isIOS} from 'platform/detection'
 import * as MediaLibrary from 'expo-media-library'
+import {
+  useLightbox,
+  useLightboxControls,
+  ProfileImageLightbox,
+  ImagesLightbox,
+} from '#/state/lightbox'
 
-export const Lightbox = observer(function Lightbox() {
-  const store = useStores()
+export function Lightbox() {
+  const {activeLightbox} = useLightbox()
+  const {closeLightbox} = useLightboxControls()
   const onClose = React.useCallback(() => {
-    store.shell.closeLightbox()
-  }, [store])
+    closeLightbox()
+  }, [closeLightbox])
 
-  if (!store.shell.activeLightbox) {
+  if (!activeLightbox) {
     return null
-  } else if (store.shell.activeLightbox.name === 'profile-image') {
-    const opts = store.shell.activeLightbox as models.ProfileImageLightbox
+  } else if (activeLightbox.name === 'profile-image') {
+    const opts = activeLightbox as ProfileImageLightbox
     return (
       <ImageView
         images={[{uri: opts.profile.avatar || ''}]}
@@ -32,8 +36,8 @@ export const Lightbox = observer(function Lightbox() {
         FooterComponent={LightboxFooter}
       />
     )
-  } else if (store.shell.activeLightbox.name === 'images') {
-    const opts = store.shell.activeLightbox as models.ImagesLightbox
+  } else if (activeLightbox.name === 'images') {
+    const opts = activeLightbox as ImagesLightbox
     return (
       <ImageView
         images={opts.images.map(img => ({...img}))}
@@ -46,14 +50,10 @@ export const Lightbox = observer(function Lightbox() {
   } else {
     return null
   }
-})
+}
 
-const LightboxFooter = observer(function LightboxFooter({
-  imageIndex,
-}: {
-  imageIndex: number
-}) {
-  const store = useStores()
+function LightboxFooter({imageIndex}: {imageIndex: number}) {
+  const {activeLightbox} = useLightbox()
   const [isAltExpanded, setAltExpanded] = React.useState(false)
   const [permissionResponse, requestPermission] = MediaLibrary.usePermissions()
 
@@ -81,7 +81,7 @@ const LightboxFooter = observer(function LightboxFooter({
     [permissionResponse, requestPermission],
   )
 
-  const lightbox = store.shell.activeLightbox
+  const lightbox = activeLightbox
   if (!lightbox) {
     return null
   }
@@ -89,11 +89,11 @@ const LightboxFooter = observer(function LightboxFooter({
   let altText = ''
   let uri = ''
   if (lightbox.name === 'images') {
-    const opts = lightbox as models.ImagesLightbox
+    const opts = lightbox as ImagesLightbox
     uri = opts.images[imageIndex].uri
     altText = opts.images[imageIndex].alt || ''
   } else if (lightbox.name === 'profile-image') {
-    const opts = lightbox as models.ProfileImageLightbox
+    const opts = lightbox as ProfileImageLightbox
     uri = opts.profile.avatar || ''
   }
 
@@ -132,7 +132,7 @@ const LightboxFooter = observer(function LightboxFooter({
       </View>
     </View>
   )
-})
+}
 
 const styles = StyleSheet.create({
   footer: {
diff --git a/src/view/com/lightbox/Lightbox.web.tsx b/src/view/com/lightbox/Lightbox.web.tsx
index 4b6ad59f3..45e1fa5a3 100644
--- a/src/view/com/lightbox/Lightbox.web.tsx
+++ b/src/view/com/lightbox/Lightbox.web.tsx
@@ -7,41 +7,42 @@ import {
   View,
   Pressable,
 } from 'react-native'
-import {observer} from 'mobx-react-lite'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {useStores} from 'state/index'
-import * as models from 'state/models/ui/shell'
 import {colors, s} from 'lib/styles'
 import ImageDefaultHeader from './ImageViewing/components/ImageDefaultHeader'
 import {Text} from '../util/text/Text'
 import {useLingui} from '@lingui/react'
 import {msg} from '@lingui/macro'
+import {
+  useLightbox,
+  useLightboxControls,
+  ImagesLightbox,
+  ProfileImageLightbox,
+} from '#/state/lightbox'
 
 interface Img {
   uri: string
   alt?: string
 }
 
-export const Lightbox = observer(function Lightbox() {
-  const store = useStores()
-
-  const onClose = useCallback(() => store.shell.closeLightbox(), [store.shell])
+export function Lightbox() {
+  const {activeLightbox} = useLightbox()
+  const {closeLightbox} = useLightboxControls()
 
-  if (!store.shell.isLightboxActive) {
+  if (!activeLightbox) {
     return null
   }
 
-  const activeLightbox = store.shell.activeLightbox
   const initialIndex =
-    activeLightbox instanceof models.ImagesLightbox ? activeLightbox.index : 0
+    activeLightbox instanceof ImagesLightbox ? activeLightbox.index : 0
 
   let imgs: Img[] | undefined
-  if (activeLightbox instanceof models.ProfileImageLightbox) {
+  if (activeLightbox instanceof ProfileImageLightbox) {
     const opts = activeLightbox
     if (opts.profile.avatar) {
       imgs = [{uri: opts.profile.avatar}]
     }
-  } else if (activeLightbox instanceof models.ImagesLightbox) {
+  } else if (activeLightbox instanceof ImagesLightbox) {
     const opts = activeLightbox
     imgs = opts.images
   }
@@ -51,9 +52,13 @@ export const Lightbox = observer(function Lightbox() {
   }
 
   return (
-    <LightboxInner imgs={imgs} initialIndex={initialIndex} onClose={onClose} />
+    <LightboxInner
+      imgs={imgs}
+      initialIndex={initialIndex}
+      onClose={closeLightbox}
+    />
   )
-})
+}
 
 function LightboxInner({
   imgs,
diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx
index 92d51ac70..a6c978fd4 100644
--- a/src/view/com/profile/ProfileHeader.tsx
+++ b/src/view/com/profile/ProfileHeader.tsx
@@ -17,7 +17,6 @@ import {useLingui} from '@lingui/react'
 import {NavigationProp} from 'lib/routes/types'
 import {isNative} from 'platform/detection'
 import {BlurView} from '../util/BlurView'
-import {ProfileImageLightbox} from 'state/models/ui/shell'
 import * as Toast from '../util/Toast'
 import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
 import {Text} from '../util/text/Text'
@@ -30,8 +29,8 @@ import {formatCount} from '../util/numeric/format'
 import {NativeDropdown, DropdownItem} from '../util/forms/NativeDropdown'
 import {Link} from '../util/Link'
 import {ProfileHeaderSuggestedFollows} from './ProfileHeaderSuggestedFollows'
-import {useStores} from 'state/index'
 import {useModalControls} from '#/state/modals'
+import {useLightboxControls, ProfileImageLightbox} from '#/state/lightbox'
 import {
   useProfileFollowMutation,
   useProfileUnfollowMutation,
@@ -115,10 +114,10 @@ function ProfileHeaderLoaded({
 }: Props) {
   const pal = usePalette('default')
   const palInverted = usePalette('inverted')
-  const store = useStores()
   const {currentAccount} = useSession()
   const {_} = useLingui()
   const {openModal} = useModalControls()
+  const {openLightbox} = useLightboxControls()
   const navigation = useNavigation<NavigationProp>()
   const {track} = useAnalytics()
   const invalidHandle = isInvalidHandle(profile.handle)
@@ -151,9 +150,9 @@ function ProfileHeaderLoaded({
       profile.avatar &&
       !(moderation.avatar.blur && moderation.avatar.noOverride)
     ) {
-      store.shell.openLightbox(new ProfileImageLightbox(profile))
+      openLightbox(new ProfileImageLightbox(profile))
     }
-  }, [store, profile, moderation])
+  }, [openLightbox, profile, moderation])
 
   const onPressFollow = React.useCallback(async () => {
     if (profile.viewer?.following) {
diff --git a/src/view/com/profile/ProfileSubpageHeader.tsx b/src/view/com/profile/ProfileSubpageHeader.tsx
index e1b587beb..ef128e877 100644
--- a/src/view/com/profile/ProfileSubpageHeader.tsx
+++ b/src/view/com/profile/ProfileSubpageHeader.tsx
@@ -16,7 +16,7 @@ import {useStores} from 'state/index'
 import {NavigationProp} from 'lib/routes/types'
 import {BACK_HITSLOP} from 'lib/constants'
 import {isNative} from 'platform/detection'
-import {ImagesLightbox} from 'state/models/ui/shell'
+import {useLightboxControls, ImagesLightbox} from '#/state/lightbox'
 import {useLingui} from '@lingui/react'
 import {msg} from '@lingui/macro'
 import {useSetDrawerOpen} from '#/state/shell'
@@ -50,6 +50,7 @@ export const ProfileSubpageHeader = observer(function HeaderImpl({
   const navigation = useNavigation<NavigationProp>()
   const {_} = useLingui()
   const {isMobile} = useWebMediaQueries()
+  const {openLightbox} = useLightboxControls()
   const pal = usePalette('default')
   const canGoBack = navigation.canGoBack()
 
@@ -69,9 +70,9 @@ export const ProfileSubpageHeader = observer(function HeaderImpl({
     if (
       avatar // TODO && !(view.moderation.avatar.blur && view.moderation.avatar.noOverride)
     ) {
-      store.shell.openLightbox(new ImagesLightbox([{uri: avatar}], 0))
+      openLightbox(new ImagesLightbox([{uri: avatar}], 0))
     }
-  }, [store, avatar])
+  }, [openLightbox, avatar])
 
   return (
     <CenteredView style={pal.view}>
diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx
index b4c7c45ae..ca3bf1104 100644
--- a/src/view/com/util/post-embeds/index.tsx
+++ b/src/view/com/util/post-embeds/index.tsx
@@ -19,8 +19,7 @@ import {
 } from '@atproto/api'
 import {Link} from '../Link'
 import {ImageLayoutGrid} from '../images/ImageLayoutGrid'
-import {ImagesLightbox} from 'state/models/ui/shell'
-import {useStores} from 'state/index'
+import {useLightboxControls, ImagesLightbox} from '#/state/lightbox'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {YoutubeEmbed} from './YoutubeEmbed'
@@ -49,7 +48,7 @@ export function PostEmbeds({
   style?: StyleProp<ViewStyle>
 }) {
   const pal = usePalette('default')
-  const store = useStores()
+  const {openLightbox} = useLightboxControls()
   const {isMobile} = useWebMediaQueries()
 
   // quote post with media
@@ -104,8 +103,8 @@ export function PostEmbeds({
         alt: img.alt,
         aspectRatio: img.aspectRatio,
       }))
-      const openLightbox = (index: number) => {
-        store.shell.openLightbox(new ImagesLightbox(items, index))
+      const _openLightbox = (index: number) => {
+        openLightbox(new ImagesLightbox(items, index))
       }
       const onPressIn = (_: number) => {
         InteractionManager.runAfterInteractions(() => {
@@ -121,7 +120,7 @@ export function PostEmbeds({
               alt={alt}
               uri={thumb}
               dimensionsHint={aspectRatio}
-              onPress={() => openLightbox(0)}
+              onPress={() => _openLightbox(0)}
               onPressIn={() => onPressIn(0)}
               style={[
                 styles.singleImage,
@@ -143,7 +142,7 @@ export function PostEmbeds({
         <View style={[styles.imagesContainer, style]}>
           <ImageLayoutGrid
             images={embed.images}
-            onPress={openLightbox}
+            onPress={_openLightbox}
             onPressIn={onPressIn}
             style={
               embed.images.length === 1