about summary refs log tree commit diff
path: root/src/state
diff options
context:
space:
mode:
Diffstat (limited to 'src/state')
-rw-r--r--src/state/modals/index.tsx284
-rw-r--r--src/state/models/media/gallery.ts13
-rw-r--r--src/state/models/ui/shell.ts224
3 files changed, 287 insertions, 234 deletions
diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx
new file mode 100644
index 000000000..f9bd1e3c9
--- /dev/null
+++ b/src/state/modals/index.tsx
@@ -0,0 +1,284 @@
+import React from 'react'
+import {AppBskyActorDefs, ModerationUI} from '@atproto/api'
+import {StyleProp, ViewStyle, DeviceEventEmitter} from 'react-native'
+import {Image as RNImage} from 'react-native-image-crop-picker'
+
+import {ProfileModel} from '#/state/models/content/profile'
+import {ImageModel} from '#/state/models/media/image'
+import {ListModel} from '#/state/models/content/list'
+import {GalleryModel} from '#/state/models/media/gallery'
+
+export interface ConfirmModal {
+  name: 'confirm'
+  title: string
+  message: string | (() => JSX.Element)
+  onPressConfirm: () => void | Promise<void>
+  onPressCancel?: () => void | Promise<void>
+  confirmBtnText?: string
+  confirmBtnStyle?: StyleProp<ViewStyle>
+  cancelBtnText?: string
+}
+
+export interface EditProfileModal {
+  name: 'edit-profile'
+  profileView: ProfileModel
+  onUpdate?: () => void
+}
+
+export interface ProfilePreviewModal {
+  name: 'profile-preview'
+  did: string
+}
+
+export interface ServerInputModal {
+  name: 'server-input'
+  initialService: string
+  onSelect: (url: string) => void
+}
+
+export interface ModerationDetailsModal {
+  name: 'moderation-details'
+  context: 'account' | 'content'
+  moderation: ModerationUI
+}
+
+export type ReportModal = {
+  name: 'report'
+} & (
+  | {
+      uri: string
+      cid: string
+    }
+  | {did: string}
+)
+
+export interface CreateOrEditListModal {
+  name: 'create-or-edit-list'
+  purpose?: string
+  list?: ListModel
+  onSave?: (uri: string) => void
+}
+
+export interface UserAddRemoveListsModal {
+  name: 'user-add-remove-lists'
+  subject: string
+  displayName: string
+  onAdd?: (listUri: string) => void
+  onRemove?: (listUri: string) => void
+}
+
+export interface ListAddUserModal {
+  name: 'list-add-user'
+  list: ListModel
+  onAdd?: (profile: AppBskyActorDefs.ProfileViewBasic) => void
+}
+
+export interface EditImageModal {
+  name: 'edit-image'
+  image: ImageModel
+  gallery: GalleryModel
+}
+
+export interface CropImageModal {
+  name: 'crop-image'
+  uri: string
+  onSelect: (img?: RNImage) => void
+}
+
+export interface AltTextImageModal {
+  name: 'alt-text-image'
+  image: ImageModel
+}
+
+export interface DeleteAccountModal {
+  name: 'delete-account'
+}
+
+export interface RepostModal {
+  name: 'repost'
+  onRepost: () => void
+  onQuote: () => void
+  isReposted: boolean
+}
+
+export interface SelfLabelModal {
+  name: 'self-label'
+  labels: string[]
+  hasMedia: boolean
+  onChange: (labels: string[]) => void
+}
+
+export interface ChangeHandleModal {
+  name: 'change-handle'
+  onChanged: () => void
+}
+
+export interface WaitlistModal {
+  name: 'waitlist'
+}
+
+export interface InviteCodesModal {
+  name: 'invite-codes'
+}
+
+export interface AddAppPasswordModal {
+  name: 'add-app-password'
+}
+
+export interface ContentFilteringSettingsModal {
+  name: 'content-filtering-settings'
+}
+
+export interface ContentLanguagesSettingsModal {
+  name: 'content-languages-settings'
+}
+
+export interface PostLanguagesSettingsModal {
+  name: 'post-languages-settings'
+}
+
+export interface BirthDateSettingsModal {
+  name: 'birth-date-settings'
+}
+
+export interface VerifyEmailModal {
+  name: 'verify-email'
+  showReminder?: boolean
+}
+
+export interface ChangeEmailModal {
+  name: 'change-email'
+}
+
+export interface SwitchAccountModal {
+  name: 'switch-account'
+}
+
+export interface LinkWarningModal {
+  name: 'link-warning'
+  text: string
+  href: string
+}
+
+export type Modal =
+  // Account
+  | AddAppPasswordModal
+  | ChangeHandleModal
+  | DeleteAccountModal
+  | EditProfileModal
+  | ProfilePreviewModal
+  | BirthDateSettingsModal
+  | VerifyEmailModal
+  | ChangeEmailModal
+  | SwitchAccountModal
+
+  // Curation
+  | ContentFilteringSettingsModal
+  | ContentLanguagesSettingsModal
+  | PostLanguagesSettingsModal
+
+  // Moderation
+  | ModerationDetailsModal
+  | ReportModal
+
+  // Lists
+  | CreateOrEditListModal
+  | UserAddRemoveListsModal
+  | ListAddUserModal
+
+  // Posts
+  | AltTextImageModal
+  | CropImageModal
+  | EditImageModal
+  | ServerInputModal
+  | RepostModal
+  | SelfLabelModal
+
+  // Bluesky access
+  | WaitlistModal
+  | InviteCodesModal
+
+  // Generic
+  | ConfirmModal
+  | LinkWarningModal
+
+const ModalContext = React.createContext<{
+  isModalActive: boolean
+  activeModals: Modal[]
+}>({
+  isModalActive: false,
+  activeModals: [],
+})
+
+const ModalControlContext = React.createContext<{
+  openModal: (modal: Modal) => void
+  closeModal: () => void
+}>({
+  openModal: () => {},
+  closeModal: () => {},
+})
+
+/**
+ * @deprecated DO NOT USE THIS unless you have no other choice.
+ */
+export let unstable__openModal: (modal: Modal) => void = () => {
+  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[]>([])
+
+  const openModal = React.useCallback(
+    (modal: Modal) => {
+      DeviceEventEmitter.emit('navigation')
+      setActiveModals(activeModals => [...activeModals, modal])
+      setIsModalActive(true)
+    },
+    [setIsModalActive, setActiveModals],
+  )
+
+  unstable__openModal = openModal
+
+  const closeModal = React.useCallback(() => {
+    let totalActiveModals = 0
+    setActiveModals(activeModals => {
+      activeModals.pop()
+      totalActiveModals = activeModals.length
+      return activeModals
+    })
+    setIsModalActive(totalActiveModals > 0)
+  }, [setIsModalActive, setActiveModals])
+
+  const state = React.useMemo(
+    () => ({
+      isModalActive,
+      activeModals,
+    }),
+    [isModalActive, activeModals],
+  )
+
+  const methods = React.useMemo(
+    () => ({
+      openModal,
+      closeModal,
+    }),
+    [openModal, closeModal],
+  )
+
+  return (
+    <ModalContext.Provider value={state}>
+      <ModalControlContext.Provider value={methods}>
+        {children}
+      </ModalControlContext.Provider>
+    </ModalContext.Provider>
+  )
+}
+
+export function useModals() {
+  return React.useContext(ModalContext)
+}
+
+export function useModalControls() {
+  return React.useContext(ModalControlContext)
+}
diff --git a/src/state/models/media/gallery.ts b/src/state/models/media/gallery.ts
index 1b22fadbd..f9c3efcad 100644
--- a/src/state/models/media/gallery.ts
+++ b/src/state/models/media/gallery.ts
@@ -4,7 +4,6 @@ import {ImageModel} from './image'
 import {Image as RNImage} from 'react-native-image-crop-picker'
 import {openPicker} from 'lib/media/picker'
 import {getImageDim} from 'lib/media/manip'
-import {isNative} from 'platform/detection'
 
 export class GalleryModel {
   images: ImageModel[] = []
@@ -42,18 +41,6 @@ export class GalleryModel {
     }
   }
 
-  async edit(image: ImageModel) {
-    if (isNative) {
-      this.crop(image)
-    } else {
-      this.rootStore.shell.openModal({
-        name: 'edit-image',
-        image,
-        gallery: this,
-      })
-    }
-  }
-
   async paste(uri: string) {
     if (this.size >= 4) {
       return
diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts
index b5fa4e591..8ef322db5 100644
--- a/src/state/models/ui/shell.ts
+++ b/src/state/models/ui/shell.ts
@@ -1,16 +1,12 @@
-import {AppBskyEmbedRecord, AppBskyActorDefs, ModerationUI} from '@atproto/api'
+import {AppBskyEmbedRecord} from '@atproto/api'
 import {RootStoreModel} from '../root-store'
 import {makeAutoObservable, runInAction} from 'mobx'
 import {ProfileModel} from '../content/profile'
-import {Image as RNImage} from 'react-native-image-crop-picker'
-import {ImageModel} from '../media/image'
-import {ListModel} from '../content/list'
-import {GalleryModel} from '../media/gallery'
-import {StyleProp, ViewStyle} from 'react-native'
 import {
   shouldRequestEmailConfirmation,
   setEmailConfirmationRequested,
 } from '#/state/shell/reminders'
+import {unstable__openModal} from '#/state/modals'
 
 export type ColorMode = 'system' | 'light' | 'dark'
 
@@ -18,200 +14,6 @@ export function isColorMode(v: unknown): v is ColorMode {
   return v === 'system' || v === 'light' || v === 'dark'
 }
 
-export interface ConfirmModal {
-  name: 'confirm'
-  title: string
-  message: string | (() => JSX.Element)
-  onPressConfirm: () => void | Promise<void>
-  onPressCancel?: () => void | Promise<void>
-  confirmBtnText?: string
-  confirmBtnStyle?: StyleProp<ViewStyle>
-  cancelBtnText?: string
-}
-
-export interface EditProfileModal {
-  name: 'edit-profile'
-  profileView: ProfileModel
-  onUpdate?: () => void
-}
-
-export interface ProfilePreviewModal {
-  name: 'profile-preview'
-  did: string
-}
-
-export interface ServerInputModal {
-  name: 'server-input'
-  initialService: string
-  onSelect: (url: string) => void
-}
-
-export interface ModerationDetailsModal {
-  name: 'moderation-details'
-  context: 'account' | 'content'
-  moderation: ModerationUI
-}
-
-export type ReportModal = {
-  name: 'report'
-} & (
-  | {
-      uri: string
-      cid: string
-    }
-  | {did: string}
-)
-
-export interface CreateOrEditListModal {
-  name: 'create-or-edit-list'
-  purpose?: string
-  list?: ListModel
-  onSave?: (uri: string) => void
-}
-
-export interface UserAddRemoveListsModal {
-  name: 'user-add-remove-lists'
-  subject: string
-  displayName: string
-  onAdd?: (listUri: string) => void
-  onRemove?: (listUri: string) => void
-}
-
-export interface ListAddUserModal {
-  name: 'list-add-user'
-  list: ListModel
-  onAdd?: (profile: AppBskyActorDefs.ProfileViewBasic) => void
-}
-
-export interface EditImageModal {
-  name: 'edit-image'
-  image: ImageModel
-  gallery: GalleryModel
-}
-
-export interface CropImageModal {
-  name: 'crop-image'
-  uri: string
-  onSelect: (img?: RNImage) => void
-}
-
-export interface AltTextImageModal {
-  name: 'alt-text-image'
-  image: ImageModel
-}
-
-export interface DeleteAccountModal {
-  name: 'delete-account'
-}
-
-export interface RepostModal {
-  name: 'repost'
-  onRepost: () => void
-  onQuote: () => void
-  isReposted: boolean
-}
-
-export interface SelfLabelModal {
-  name: 'self-label'
-  labels: string[]
-  hasMedia: boolean
-  onChange: (labels: string[]) => void
-}
-
-export interface ChangeHandleModal {
-  name: 'change-handle'
-  onChanged: () => void
-}
-
-export interface WaitlistModal {
-  name: 'waitlist'
-}
-
-export interface InviteCodesModal {
-  name: 'invite-codes'
-}
-
-export interface AddAppPasswordModal {
-  name: 'add-app-password'
-}
-
-export interface ContentFilteringSettingsModal {
-  name: 'content-filtering-settings'
-}
-
-export interface ContentLanguagesSettingsModal {
-  name: 'content-languages-settings'
-}
-
-export interface PostLanguagesSettingsModal {
-  name: 'post-languages-settings'
-}
-
-export interface BirthDateSettingsModal {
-  name: 'birth-date-settings'
-}
-
-export interface VerifyEmailModal {
-  name: 'verify-email'
-  showReminder?: boolean
-}
-
-export interface ChangeEmailModal {
-  name: 'change-email'
-}
-
-export interface SwitchAccountModal {
-  name: 'switch-account'
-}
-
-export interface LinkWarningModal {
-  name: 'link-warning'
-  text: string
-  href: string
-}
-
-export type Modal =
-  // Account
-  | AddAppPasswordModal
-  | ChangeHandleModal
-  | DeleteAccountModal
-  | EditProfileModal
-  | ProfilePreviewModal
-  | BirthDateSettingsModal
-  | VerifyEmailModal
-  | ChangeEmailModal
-  | SwitchAccountModal
-
-  // Curation
-  | ContentFilteringSettingsModal
-  | ContentLanguagesSettingsModal
-  | PostLanguagesSettingsModal
-
-  // Moderation
-  | ModerationDetailsModal
-  | ReportModal
-
-  // Lists
-  | CreateOrEditListModal
-  | UserAddRemoveListsModal
-  | ListAddUserModal
-
-  // Posts
-  | AltTextImageModal
-  | CropImageModal
-  | EditImageModal
-  | ServerInputModal
-  | RepostModal
-  | SelfLabelModal
-
-  // Bluesky access
-  | WaitlistModal
-  | InviteCodesModal
-
-  // Generic
-  | ConfirmModal
-  | LinkWarningModal
-
 interface LightboxModel {}
 
 export class ProfileImageLightbox implements LightboxModel {
@@ -267,8 +69,6 @@ export interface ComposerOpts {
 }
 
 export class ShellUiModel {
-  isModalActive = false
-  activeModals: Modal[] = []
   isLightboxActive = false
   activeLightbox: ProfileImageLightbox | ImagesLightbox | null = null
   isComposerActive = false
@@ -293,10 +93,6 @@ export class ShellUiModel {
       this.closeLightbox()
       return true
     }
-    if (this.isModalActive) {
-      this.closeModal()
-      return true
-    }
     if (this.isComposerActive) {
       this.closeComposer()
       return true
@@ -311,25 +107,11 @@ export class ShellUiModel {
     if (this.isLightboxActive) {
       this.closeLightbox()
     }
-    while (this.isModalActive) {
-      this.closeModal()
-    }
     if (this.isComposerActive) {
       this.closeComposer()
     }
   }
 
-  openModal(modal: Modal) {
-    this.rootStore.emitNavigation()
-    this.isModalActive = true
-    this.activeModals.push(modal)
-  }
-
-  closeModal() {
-    this.activeModals.pop()
-    this.isModalActive = this.activeModals.length > 0
-  }
-
   openLightbox(lightbox: ProfileImageLightbox | ImagesLightbox) {
     this.rootStore.emitNavigation()
     this.isLightboxActive = true
@@ -363,7 +145,7 @@ export class ShellUiModel {
   setupLoginModals() {
     this.rootStore.onSessionReady(() => {
       if (shouldRequestEmailConfirmation(this.rootStore.session)) {
-        this.openModal({name: 'verify-email', showReminder: true})
+        unstable__openModal({name: 'verify-email', showReminder: true})
         setEmailConfirmationRequested()
       }
     })