about summary refs log tree commit diff
path: root/src/state/modals
diff options
context:
space:
mode:
authorEric Bailey <git@esb.lol>2023-11-08 12:34:10 -0600
committerGitHub <noreply@github.com>2023-11-08 10:34:10 -0800
commitf18b15241ab708f8c25a11937a875e361e9f1221 (patch)
tree07829ce8617cb858b4519d6f16c89c7e43f84d9c /src/state/modals
parent5eadadffbf5475b233da7b1463e2345ff3e3cfce (diff)
downloadvoidsky-f18b15241ab708f8c25a11937a875e361e9f1221.tar.zst
Add modal state provider, replace usage except methods (#1833)
* Add modal state provider, replace usage except methods

* Replace easy spots

* Fix sticky spots

* Replace final usages

* Memorize context objects

* Add more warnings
Diffstat (limited to 'src/state/modals')
-rw-r--r--src/state/modals/index.tsx284
1 files changed, 284 insertions, 0 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)
+}