diff --git a/src/App.native.tsx b/src/App.native.tsx
index ffa8b338..3f49eb11 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() {
-
+
+
+
diff --git a/src/App.web.tsx b/src/App.web.tsx
index 8e22f648..e1f4e803 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() {
-
+
+
+
diff --git a/src/state/lightbox.tsx b/src/state/lightbox.tsx
new file mode 100644
index 00000000..613cd638
--- /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(
+ 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 (
+
+
+ {children}
+
+
+ )
+}
+
+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 57f48663..9dd3e419 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 223c2062..1631b8f9 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 1cadbf9a..8a18df33 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 (
)
- } 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 (
({...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({
)
-})
+}
const styles = StyleSheet.create({
footer: {
diff --git a/src/view/com/lightbox/Lightbox.web.tsx b/src/view/com/lightbox/Lightbox.web.tsx
index 4b6ad59f..45e1fa5a 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()
+export function Lightbox() {
+ const {activeLightbox} = useLightbox()
+ const {closeLightbox} = useLightboxControls()
- const onClose = useCallback(() => store.shell.closeLightbox(), [store.shell])
-
- 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 (
-
+
)
-})
+}
function LightboxInner({
imgs,
diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx
index 92d51ac7..a6c978fd 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()
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 e1b587be..ef128e87 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()
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 (
diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx
index b4c7c45a..ca3bf110 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
}) {
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({