From f18b15241ab708f8c25a11937a875e361e9f1221 Mon Sep 17 00:00:00 2001 From: Eric Bailey <git@esb.lol> Date: Wed, 8 Nov 2023 12:34:10 -0600 Subject: [PATCH] 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 --- src/App.native.tsx | 5 +- src/App.web.tsx | 5 +- src/lib/hooks/useAccountSwitcher.ts | 5 +- src/lib/hooks/useOTAUpdate.ts | 8 +- src/lib/media/alt-text.ts | 12 - src/lib/media/picker.web.tsx | 5 +- src/state/modals/index.tsx | 284 ++++++++++++++++++ src/state/models/media/gallery.ts | 13 - src/state/models/ui/shell.ts | 224 +------------- src/view/com/auth/create/Step2.tsx | 8 +- src/view/com/auth/login/Login.tsx | 8 +- src/view/com/composer/Composer.tsx | 13 +- src/view/com/composer/labels/LabelsBtn.tsx | 6 +- src/view/com/composer/photos/Gallery.tsx | 28 +- .../select-language/SelectLangBtn.tsx | 8 +- src/view/com/feeds/FeedSourceCard.tsx | 8 +- src/view/com/lists/ListItems.tsx | 8 +- src/view/com/modals/AddAppPasswords.tsx | 6 +- src/view/com/modals/AltImage.tsx | 10 +- src/view/com/modals/BirthDateSettings.tsx | 4 +- src/view/com/modals/ChangeEmail.tsx | 8 +- src/view/com/modals/ChangeHandle.tsx | 9 +- src/view/com/modals/Confirm.tsx | 8 +- .../com/modals/ContentFilteringSettings.tsx | 9 +- src/view/com/modals/CreateOrEditList.tsx | 9 +- src/view/com/modals/DeleteAccount.tsx | 6 +- src/view/com/modals/EditImage.tsx | 8 +- src/view/com/modals/EditProfile.tsx | 10 +- src/view/com/modals/InviteCodes.tsx | 6 +- src/view/com/modals/LinkWarning.tsx | 8 +- src/view/com/modals/ListAddUser.tsx | 4 +- src/view/com/modals/Modal.tsx | 24 +- src/view/com/modals/Modal.web.tsx | 17 +- src/view/com/modals/ModerationDetails.tsx | 9 +- src/view/com/modals/Repost.tsx | 6 +- src/view/com/modals/SelfLabel.tsx | 6 +- src/view/com/modals/ServerInput.tsx | 6 +- src/view/com/modals/UserAddRemoveLists.tsx | 10 +- src/view/com/modals/VerifyEmail.tsx | 10 +- src/view/com/modals/Waitlist.tsx | 6 +- .../com/modals/crop-image/CropImage.web.tsx | 8 +- .../ContentLanguagesSettings.tsx | 8 +- .../lang-settings/PostLanguagesSettings.tsx | 8 +- src/view/com/modals/report/Modal.tsx | 6 +- src/view/com/posts/FeedErrorMessage.tsx | 8 +- src/view/com/profile/ProfileHeader.tsx | 22 +- src/view/com/testing/TestCtrls.e2e.tsx | 4 +- src/view/com/util/Link.tsx | 28 +- src/view/com/util/UserPreviewLink.tsx | 6 +- src/view/com/util/forms/PostDropdownBtn.tsx | 8 +- src/view/com/util/moderation/ContentHider.tsx | 8 +- src/view/com/util/moderation/PostAlerts.tsx | 6 +- src/view/com/util/moderation/PostHider.tsx | 6 +- .../util/moderation/ProfileHeaderAlerts.tsx | 6 +- src/view/com/util/moderation/ScreenHider.tsx | 6 +- src/view/com/util/post-ctrls/PostCtrls.tsx | 9 +- src/view/com/util/post-ctrls/RepostButton.tsx | 8 +- src/view/screens/AppPasswords.tsx | 11 +- src/view/screens/LanguageSettings.tsx | 8 +- src/view/screens/Lists.tsx | 6 +- src/view/screens/Moderation.tsx | 8 +- src/view/screens/ModerationModlists.tsx | 6 +- src/view/screens/ProfileFeed.tsx | 6 +- src/view/screens/ProfileList.tsx | 32 +- src/view/screens/Settings.tsx | 25 +- src/view/shell/Drawer.tsx | 6 +- src/view/shell/bottom-bar/BottomBar.tsx | 6 +- src/view/shell/desktop/RightNav.tsx | 6 +- src/view/shell/index.tsx | 5 +- src/view/shell/index.web.tsx | 5 +- 70 files changed, 634 insertions(+), 498 deletions(-) delete mode 100644 src/lib/media/alt-text.ts create mode 100644 src/state/modals/index.tsx diff --git a/src/App.native.tsx b/src/App.native.tsx index ccc7de32..5955504e 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -22,6 +22,7 @@ import * as Toast from 'view/com/util/Toast' 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 MutedThreadsProvider} from 'state/muted-threads' import {Provider as InvitesStateProvider} from 'state/invites' import {Provider as PrefsStateProvider} from 'state/preferences' @@ -84,7 +85,9 @@ function App() { <PrefsStateProvider> <MutedThreadsProvider> <InvitesStateProvider> - <InnerApp /> + <ModalStateProvider> + <InnerApp /> + </ModalStateProvider> </InvitesStateProvider> </MutedThreadsProvider> </PrefsStateProvider> diff --git a/src/App.web.tsx b/src/App.web.tsx index 363161bf..9e5b99a9 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -17,6 +17,7 @@ import {ToastContainer} from 'view/com/util/Toast.web' import {ThemeProvider} from 'lib/ThemeContext' import {queryClient} from 'lib/react-query' import {Provider as ShellStateProvider} from 'state/shell' +import {Provider as ModalStateProvider} from 'state/modals' import {Provider as MutedThreadsProvider} from 'state/muted-threads' import {Provider as InvitesStateProvider} from 'state/invites' import {Provider as PrefsStateProvider} from 'state/preferences' @@ -74,7 +75,9 @@ function App() { <PrefsStateProvider> <MutedThreadsProvider> <InvitesStateProvider> - <InnerApp /> + <ModalStateProvider> + <InnerApp /> + </ModalStateProvider> </InvitesStateProvider> </MutedThreadsProvider> </PrefsStateProvider> diff --git a/src/lib/hooks/useAccountSwitcher.ts b/src/lib/hooks/useAccountSwitcher.ts index 1ddb181a..b165fddb 100644 --- a/src/lib/hooks/useAccountSwitcher.ts +++ b/src/lib/hooks/useAccountSwitcher.ts @@ -7,6 +7,7 @@ import {AccountData} from 'state/models/session' import {reset as resetNavigation} from '../../Navigation' import * as Toast from 'view/com/util/Toast' import {useSetDrawerOpen} from '#/state/shell/drawer-open' +import {useModalControls} from '#/state/modals' export function useAccountSwitcher(): [ boolean, @@ -16,6 +17,7 @@ export function useAccountSwitcher(): [ const {track} = useAnalytics() const store = useStores() const setDrawerOpen = useSetDrawerOpen() + const {closeModal} = useModalControls() const [isSwitching, setIsSwitching] = useState(false) const navigation = useNavigation<NavigationProp>() @@ -25,6 +27,7 @@ export function useAccountSwitcher(): [ setIsSwitching(true) const success = await store.session.resumeSession(acct) setDrawerOpen(false) + closeModal() store.shell.closeAllActiveElements() if (success) { resetNavigation() @@ -36,7 +39,7 @@ export function useAccountSwitcher(): [ store.session.clear() } }, - [track, setIsSwitching, navigation, store, setDrawerOpen], + [track, setIsSwitching, navigation, store, setDrawerOpen, closeModal], ) return [isSwitching, setIsSwitching, onPressSwitchAccount] diff --git a/src/lib/hooks/useOTAUpdate.ts b/src/lib/hooks/useOTAUpdate.ts index 0ce97a4c..a3584fc9 100644 --- a/src/lib/hooks/useOTAUpdate.ts +++ b/src/lib/hooks/useOTAUpdate.ts @@ -1,15 +1,15 @@ import * as Updates from 'expo-updates' import {useCallback, useEffect} from 'react' import {AppState} from 'react-native' -import {useStores} from 'state/index' import {logger} from '#/logger' +import {useModalControls} from '#/state/modals' export function useOTAUpdate() { - const store = useStores() + const {openModal} = useModalControls() // HELPER FUNCTIONS const showUpdatePopup = useCallback(() => { - store.shell.openModal({ + openModal({ name: 'confirm', title: 'Update Available', message: @@ -20,7 +20,7 @@ export function useOTAUpdate() { }) }, }) - }, [store.shell]) + }, [openModal]) const checkForUpdate = useCallback(async () => { logger.debug('useOTAUpdate: Checking for update...') try { diff --git a/src/lib/media/alt-text.ts b/src/lib/media/alt-text.ts deleted file mode 100644 index 4109f667..00000000 --- a/src/lib/media/alt-text.ts +++ /dev/null @@ -1,12 +0,0 @@ -import {RootStoreModel} from 'state/index' -import {ImageModel} from 'state/models/media/image' - -export async function openAltTextModal( - store: RootStoreModel, - image: ImageModel, -) { - store.shell.openModal({ - name: 'alt-text-image', - image, - }) -} diff --git a/src/lib/media/picker.web.tsx b/src/lib/media/picker.web.tsx index d12685b0..50b9c73e 100644 --- a/src/lib/media/picker.web.tsx +++ b/src/lib/media/picker.web.tsx @@ -4,6 +4,7 @@ import {CameraOpts, CropperOptions} from './types' import {RootStoreModel} from 'state/index' import {Image as RNImage} from 'react-native-image-crop-picker' export {openPicker} from './picker.shared' +import {unstable__openModal} from '#/state/modals' export async function openCamera( _store: RootStoreModel, @@ -14,12 +15,12 @@ export async function openCamera( } export async function openCropper( - store: RootStoreModel, + _store: RootStoreModel, opts: CropperOptions, ): Promise<RNImage> { // TODO handle more opts return new Promise((resolve, reject) => { - store.shell.openModal({ + unstable__openModal({ name: 'crop-image', uri: opts.path, onSelect: (img?: RNImage) => { diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx new file mode 100644 index 00000000..f9bd1e3c --- /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 1b22fadb..f9c3efca 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 b5fa4e59..8ef322db 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() } }) diff --git a/src/view/com/auth/create/Step2.tsx b/src/view/com/auth/create/Step2.tsx index 60e19756..b2054150 100644 --- a/src/view/com/auth/create/Step2.tsx +++ b/src/view/com/auth/create/Step2.tsx @@ -10,8 +10,8 @@ import {usePalette} from 'lib/hooks/usePalette' import {TextInput} from '../util/TextInput' import {Policies} from './Policies' import {ErrorMessage} from 'view/com/util/error/ErrorMessage' -import {useStores} from 'state/index' import {isWeb} from 'platform/detection' +import {useModalControls} from '#/state/modals' /** STEP 2: Your account * @field Invite code or waitlist @@ -28,11 +28,11 @@ export const Step2 = observer(function Step2Impl({ model: CreateAccountModel }) { const pal = usePalette('default') - const store = useStores() + const {openModal} = useModalControls() const onPressWaitlist = React.useCallback(() => { - store.shell.openModal({name: 'waitlist'}) - }, [store]) + openModal({name: 'waitlist'}) + }, [openModal]) return ( <View> diff --git a/src/view/com/auth/login/Login.tsx b/src/view/com/auth/login/Login.tsx index acc05b6c..24a657c6 100644 --- a/src/view/com/auth/login/Login.tsx +++ b/src/view/com/auth/login/Login.tsx @@ -31,6 +31,7 @@ import {useTheme} from 'lib/ThemeContext' import {cleanError} from 'lib/strings/errors' import {isWeb} from 'platform/detection' import {logger} from '#/logger' +import {useModalControls} from '#/state/modals' enum Forms { Login, @@ -303,9 +304,10 @@ const LoginForm = ({ const [identifier, setIdentifier] = useState<string>(initialHandle) const [password, setPassword] = useState<string>('') const passwordInputRef = useRef<TextInput>(null) + const {openModal} = useModalControls() const onPressSelectService = () => { - store.shell.openModal({ + openModal({ name: 'server-input', initialService: serviceUrl, onSelect: setServiceUrl, @@ -528,7 +530,6 @@ const LoginForm = ({ } const ForgotPasswordForm = ({ - store, error, serviceUrl, serviceDescription, @@ -551,13 +552,14 @@ const ForgotPasswordForm = ({ const [isProcessing, setIsProcessing] = useState<boolean>(false) const [email, setEmail] = useState<string>('') const {screen} = useAnalytics() + const {openModal} = useModalControls() useEffect(() => { screen('Signin:ForgotPassword') }, [screen]) const onPressSelectService = () => { - store.shell.openModal({ + openModal({ name: 'server-input', initialService: serviceUrl, onSelect: setServiceUrl, diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 632e72fd..68f70682 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -49,6 +49,7 @@ import {LabelsBtn} from './labels/LabelsBtn' import {SelectLangBtn} from './select-language/SelectLangBtn' import {EmojiPickerButton} from './text-input/web/EmojiPicker.web' import {insertMentionAt} from 'lib/strings/mention-manip' +import {useModals, useModalControls} from '#/state/modals' import {useRequireAltTextEnabled} from '#/state/preferences' import { useLanguagePrefs, @@ -64,6 +65,8 @@ export const ComposePost = observer(function ComposePost({ quote: initQuote, mention: initMention, }: Props) { + const {activeModals} = useModals() + const {openModal, closeModal} = useModalControls() const {track} = useAnalytics() const pal = usePalette('default') const {isDesktop, isMobile} = useWebMediaQueries() @@ -118,18 +121,18 @@ export const ComposePost = observer(function ComposePost({ const onPressCancel = useCallback(() => { if (graphemeLength > 0 || !gallery.isEmpty) { - if (store.shell.activeModals.some(modal => modal.name === 'confirm')) { - store.shell.closeModal() + if (activeModals.some(modal => modal.name === 'confirm')) { + closeModal() } if (Keyboard) { Keyboard.dismiss() } - store.shell.openModal({ + openModal({ name: 'confirm', title: 'Discard draft', onPressConfirm: onClose, onPressCancel: () => { - store.shell.closeModal() + closeModal() }, message: "Are you sure you'd like to discard this draft?", confirmBtnText: 'Discard', @@ -138,7 +141,7 @@ export const ComposePost = observer(function ComposePost({ } else { onClose() } - }, [store, onClose, graphemeLength, gallery]) + }, [openModal, closeModal, activeModals, onClose, graphemeLength, gallery]) // android back button useEffect(() => { if (!isAndroid) { diff --git a/src/view/com/composer/labels/LabelsBtn.tsx b/src/view/com/composer/labels/LabelsBtn.tsx index 96908d47..4b6ad81c 100644 --- a/src/view/com/composer/labels/LabelsBtn.tsx +++ b/src/view/com/composer/labels/LabelsBtn.tsx @@ -3,11 +3,11 @@ import {Keyboard, StyleSheet} from 'react-native' import {observer} from 'mobx-react-lite' import {Button} from 'view/com/util/forms/Button' import {usePalette} from 'lib/hooks/usePalette' -import {useStores} from 'state/index' import {ShieldExclamation} from 'lib/icons' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome' import {isNative} from 'platform/detection' +import {useModalControls} from '#/state/modals' export const LabelsBtn = observer(function LabelsBtn({ labels, @@ -19,7 +19,7 @@ export const LabelsBtn = observer(function LabelsBtn({ onChange: (v: string[]) => void }) { const pal = usePalette('default') - const store = useStores() + const {openModal} = useModalControls() return ( <Button @@ -34,7 +34,7 @@ export const LabelsBtn = observer(function LabelsBtn({ Keyboard.dismiss() } } - store.shell.openModal({name: 'self-label', labels, hasMedia, onChange}) + openModal({name: 'self-label', labels, hasMedia, onChange}) }}> <ShieldExclamation style={pal.link} size={26} /> {labels.length > 0 ? ( diff --git a/src/view/com/composer/photos/Gallery.tsx b/src/view/com/composer/photos/Gallery.tsx index fcd99842..069a0547 100644 --- a/src/view/com/composer/photos/Gallery.tsx +++ b/src/view/com/composer/photos/Gallery.tsx @@ -7,11 +7,11 @@ import {s, colors} from 'lib/styles' import {StyleSheet, TouchableOpacity, View} from 'react-native' import {Image} from 'expo-image' import {Text} from 'view/com/util/text/Text' -import {openAltTextModal} from 'lib/media/alt-text' import {Dimensions} from 'lib/media/types' -import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {useModalControls} from '#/state/modals' +import {isNative} from 'platform/detection' const IMAGE_GAP = 8 @@ -47,9 +47,9 @@ const GalleryInner = observer(function GalleryImpl({ gallery, containerInfo, }: GalleryInnerProps) { - const store = useStores() const pal = usePalette('default') const {isMobile} = useWebMediaQueries() + const {openModal} = useModalControls() let side: number @@ -117,7 +117,10 @@ const GalleryInner = observer(function GalleryImpl({ accessibilityHint="" onPress={() => { Keyboard.dismiss() - openAltTextModal(store, image) + openModal({ + name: 'alt-text-image', + image, + }) }} style={[styles.altTextControl, altTextControlStyle]}> <Text style={styles.altTextControlLabel} accessible={false}> @@ -137,7 +140,17 @@ const GalleryInner = observer(function GalleryImpl({ accessibilityRole="button" accessibilityLabel="Edit image" accessibilityHint="" - onPress={() => gallery.edit(image)} + onPress={() => { + if (isNative) { + gallery.crop(image) + } else { + openModal({ + name: 'edit-image', + image, + gallery, + }) + } + }} style={styles.imageControl}> <FontAwesomeIcon icon="pen" @@ -165,7 +178,10 @@ const GalleryInner = observer(function GalleryImpl({ accessibilityHint="" onPress={() => { Keyboard.dismiss() - openAltTextModal(store, image) + openModal({ + name: 'alt-text-image', + image, + }) }} style={styles.altTextHiddenRegion} /> diff --git a/src/view/com/composer/select-language/SelectLangBtn.tsx b/src/view/com/composer/select-language/SelectLangBtn.tsx index 64654238..6c45f338 100644 --- a/src/view/com/composer/select-language/SelectLangBtn.tsx +++ b/src/view/com/composer/select-language/SelectLangBtn.tsx @@ -12,9 +12,9 @@ import { DropdownItemButton, } from 'view/com/util/forms/DropdownButton' import {usePalette} from 'lib/hooks/usePalette' -import {useStores} from 'state/index' import {isNative} from 'platform/detection' import {codeToLanguageName} from '../../../../locale/helpers' +import {useModalControls} from '#/state/modals' import { useLanguagePrefs, useSetLanguagePrefs, @@ -24,7 +24,7 @@ import { export const SelectLangBtn = observer(function SelectLangBtn() { const pal = usePalette('default') - const store = useStores() + const {openModal} = useModalControls() const langPrefs = useLanguagePrefs() const setLangPrefs = useSetLanguagePrefs() @@ -34,8 +34,8 @@ export const SelectLangBtn = observer(function SelectLangBtn() { Keyboard.dismiss() } } - store.shell.openModal({name: 'post-languages-settings'}) - }, [store]) + openModal({name: 'post-languages-settings'}) + }, [openModal]) const postLanguagesPref = toPostLanguages(langPrefs.postLanguage) const items: DropdownItem[] = useMemo(() => { diff --git a/src/view/com/feeds/FeedSourceCard.tsx b/src/view/com/feeds/FeedSourceCard.tsx index 2c4335dc..63af5261 100644 --- a/src/view/com/feeds/FeedSourceCard.tsx +++ b/src/view/com/feeds/FeedSourceCard.tsx @@ -10,12 +10,12 @@ import {observer} from 'mobx-react-lite' import {FeedSourceModel} from 'state/models/content/feed-source' import {useNavigation} from '@react-navigation/native' import {NavigationProp} from 'lib/routes/types' -import {useStores} from 'state/index' import {pluralize} from 'lib/strings/helpers' import {AtUri} from '@atproto/api' import * as Toast from 'view/com/util/Toast' import {sanitizeHandle} from 'lib/strings/handles' import {logger} from '#/logger' +import {useModalControls} from '#/state/modals' export const FeedSourceCard = observer(function FeedSourceCardImpl({ item, @@ -30,13 +30,13 @@ export const FeedSourceCard = observer(function FeedSourceCardImpl({ showDescription?: boolean showLikes?: boolean }) { - const store = useStores() const pal = usePalette('default') const navigation = useNavigation<NavigationProp>() + const {openModal} = useModalControls() const onToggleSaved = React.useCallback(async () => { if (item.isSaved) { - store.shell.openModal({ + openModal({ name: 'confirm', title: 'Remove from my feeds', message: `Remove ${item.displayName} from my feeds?`, @@ -59,7 +59,7 @@ export const FeedSourceCard = observer(function FeedSourceCardImpl({ logger.error('Failed to save feed', {error: e}) } } - }, [store, item]) + }, [openModal, item]) return ( <Pressable diff --git a/src/view/com/lists/ListItems.tsx b/src/view/com/lists/ListItems.tsx index 192cdd9d..3658e552 100644 --- a/src/view/com/lists/ListItems.tsx +++ b/src/view/com/lists/ListItems.tsx @@ -17,11 +17,11 @@ import {Button} from '../util/forms/Button' import {ListModel} from 'state/models/content/list' import {useAnalytics} from 'lib/analytics/analytics' import {usePalette} from 'lib/hooks/usePalette' -import {useStores} from 'state/index' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {s} from 'lib/styles' import {OnScrollCb} from 'lib/hooks/useOnMainScroll' import {logger} from '#/logger' +import {useModalControls} from '#/state/modals' const LOADING_ITEM = {_reactKey: '__loading__'} const EMPTY_ITEM = {_reactKey: '__empty__'} @@ -54,10 +54,10 @@ export const ListItems = observer(function ListItemsImpl({ desktopFixedHeightOffset?: number }) { const pal = usePalette('default') - const store = useStores() const {track} = useAnalytics() const [isRefreshing, setIsRefreshing] = React.useState(false) const {isMobile} = useWebMediaQueries() + const {openModal} = useModalControls() const data = React.useMemo(() => { let items: any[] = [] @@ -115,7 +115,7 @@ export const ListItems = observer(function ListItemsImpl({ const onPressEditMembership = React.useCallback( (profile: AppBskyActorDefs.ProfileViewBasic) => { - store.shell.openModal({ + openModal({ name: 'user-add-remove-lists', subject: profile.did, displayName: profile.displayName || profile.handle, @@ -131,7 +131,7 @@ export const ListItems = observer(function ListItemsImpl({ }, }) }, - [store, list], + [openModal, list], ) // rendering diff --git a/src/view/com/modals/AddAppPasswords.tsx b/src/view/com/modals/AddAppPasswords.tsx index 29763620..621c61b9 100644 --- a/src/view/com/modals/AddAppPasswords.tsx +++ b/src/view/com/modals/AddAppPasswords.tsx @@ -13,6 +13,7 @@ import { import Clipboard from '@react-native-clipboard/clipboard' import * as Toast from '../util/Toast' import {logger} from '#/logger' +import {useModalControls} from '#/state/modals' export const snapPoints = ['70%'] @@ -54,6 +55,7 @@ const shadesOfBlue: string[] = [ export function Component({}: {}) { const pal = usePalette('default') const store = useStores() + const {closeModal} = useModalControls() const [name, setName] = useState( shadesOfBlue[Math.floor(Math.random() * shadesOfBlue.length)], ) @@ -69,8 +71,8 @@ export function Component({}: {}) { }, [appPassword]) const onDone = React.useCallback(() => { - store.shell.closeModal() - }, [store]) + closeModal() + }, [closeModal]) const createAppPassword = async () => { // if name is all whitespace, we don't allow it diff --git a/src/view/com/modals/AltImage.tsx b/src/view/com/modals/AltImage.tsx index c084e84a..9c377a12 100644 --- a/src/view/com/modals/AltImage.tsx +++ b/src/view/com/modals/AltImage.tsx @@ -17,9 +17,9 @@ import {MAX_ALT_TEXT} from 'lib/constants' import {useTheme} from 'lib/ThemeContext' import {Text} from '../util/text/Text' import LinearGradient from 'react-native-linear-gradient' -import {useStores} from 'state/index' import {isAndroid, isWeb} from 'platform/detection' import {ImageModel} from 'state/models/media/image' +import {useModalControls} from '#/state/modals' export const snapPoints = ['fullscreen'] @@ -29,10 +29,10 @@ interface Props { export function Component({image}: Props) { const pal = usePalette('default') - const store = useStores() const theme = useTheme() const [altText, setAltText] = useState(image.altText) const windim = useWindowDimensions() + const {closeModal} = useModalControls() const imageStyles = useMemo<ImageStyle>(() => { const maxWidth = isWeb ? 450 : windim.width @@ -53,11 +53,11 @@ export function Component({image}: Props) { const onPressSave = useCallback(() => { image.setAltText(altText) - store.shell.closeModal() - }, [store, image, altText]) + closeModal() + }, [closeModal, image, altText]) const onPressCancel = () => { - store.shell.closeModal() + closeModal() } return ( diff --git a/src/view/com/modals/BirthDateSettings.tsx b/src/view/com/modals/BirthDateSettings.tsx index 6927ba8d..7b0778f8 100644 --- a/src/view/com/modals/BirthDateSettings.tsx +++ b/src/view/com/modals/BirthDateSettings.tsx @@ -15,12 +15,14 @@ import {usePalette} from 'lib/hooks/usePalette' import {isWeb} from 'platform/detection' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {cleanError} from 'lib/strings/errors' +import {useModalControls} from '#/state/modals' export const snapPoints = ['50%'] export const Component = observer(function Component({}: {}) { const pal = usePalette('default') const store = useStores() + const {closeModal} = useModalControls() const [date, setDate] = useState<Date>( store.preferences.birthDate || new Date(), ) @@ -33,7 +35,7 @@ export const Component = observer(function Component({}: {}) { setIsProcessing(true) try { await store.preferences.setBirthDate(date) - store.shell.closeModal() + closeModal() } catch (e) { setError(cleanError(String(e))) } finally { diff --git a/src/view/com/modals/ChangeEmail.tsx b/src/view/com/modals/ChangeEmail.tsx index 01257055..ec37aeed 100644 --- a/src/view/com/modals/ChangeEmail.tsx +++ b/src/view/com/modals/ChangeEmail.tsx @@ -12,6 +12,7 @@ import {usePalette} from 'lib/hooks/usePalette' import {isWeb} from 'platform/detection' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {cleanError} from 'lib/strings/errors' +import {useModalControls} from '#/state/modals' enum Stages { InputEmail, @@ -32,6 +33,7 @@ export const Component = observer(function Component({}: {}) { const [isProcessing, setIsProcessing] = useState<boolean>(false) const [error, setError] = useState<string>('') const {isMobile} = useWebMediaQueries() + const {openModal, closeModal} = useModalControls() const onRequestChange = async () => { if (email === store.session.currentSession?.email) { @@ -90,8 +92,8 @@ export const Component = observer(function Component({}: {}) { } const onVerify = async () => { - store.shell.closeModal() - store.shell.openModal({name: 'verify-email'}) + closeModal() + openModal({name: 'verify-email'}) } return ( @@ -207,7 +209,7 @@ export const Component = observer(function Component({}: {}) { <Button testID="cancelBtn" type="default" - onPress={() => store.shell.closeModal()} + onPress={() => closeModal()} accessibilityLabel="Cancel" accessibilityHint="" label="Cancel" diff --git a/src/view/com/modals/ChangeHandle.tsx b/src/view/com/modals/ChangeHandle.tsx index c54c1c04..6184cb3b 100644 --- a/src/view/com/modals/ChangeHandle.tsx +++ b/src/view/com/modals/ChangeHandle.tsx @@ -22,6 +22,7 @@ import {useTheme} from 'lib/ThemeContext' import {useAnalytics} from 'lib/analytics/analytics' import {cleanError} from 'lib/strings/errors' import {logger} from '#/logger' +import {useModalControls} from '#/state/modals' export const snapPoints = ['100%'] @@ -30,6 +31,7 @@ export function Component({onChanged}: {onChanged: () => void}) { const [error, setError] = useState<string>('') const pal = usePalette('default') const {track} = useAnalytics() + const {closeModal} = useModalControls() const [isProcessing, setProcessing] = useState<boolean>(false) const [retryDescribeTrigger, setRetryDescribeTrigger] = React.useState<any>( @@ -85,8 +87,8 @@ export function Component({onChanged}: {onChanged: () => void}) { // events // = const onPressCancel = React.useCallback(() => { - store.shell.closeModal() - }, [store]) + closeModal() + }, [closeModal]) const onPressRetryConnect = React.useCallback( () => setRetryDescribeTrigger({}), [setRetryDescribeTrigger], @@ -110,7 +112,7 @@ export function Component({onChanged}: {onChanged: () => void}) { await store.agent.updateHandle({ handle: newHandle, }) - store.shell.closeModal() + closeModal() onChanged() } catch (err: any) { setError(cleanError(err)) @@ -127,6 +129,7 @@ export function Component({onChanged}: {onChanged: () => void}) { isCustom, onChanged, track, + closeModal, ]) // rendering diff --git a/src/view/com/modals/Confirm.tsx b/src/view/com/modals/Confirm.tsx index c1324b1c..6b942057 100644 --- a/src/view/com/modals/Confirm.tsx +++ b/src/view/com/modals/Confirm.tsx @@ -6,13 +6,13 @@ import { View, } from 'react-native' import {Text} from '../util/text/Text' -import {useStores} from 'state/index' import {s, colors} from 'lib/styles' import {ErrorMessage} from '../util/error/ErrorMessage' import {cleanError} from 'lib/strings/errors' import {usePalette} from 'lib/hooks/usePalette' import {isWeb} from 'platform/detection' -import type {ConfirmModal} from 'state/models/ui/shell' +import type {ConfirmModal} from '#/state/modals' +import {useModalControls} from '#/state/modals' export const snapPoints = ['50%'] @@ -26,7 +26,7 @@ export function Component({ cancelBtnText, }: ConfirmModal) { const pal = usePalette('default') - const store = useStores() + const {closeModal} = useModalControls() const [isProcessing, setIsProcessing] = useState<boolean>(false) const [error, setError] = useState<string>('') const onPress = async () => { @@ -34,7 +34,7 @@ export function Component({ setIsProcessing(true) try { await onPressConfirm() - store.shell.closeModal() + closeModal() return } catch (e: any) { setError(cleanError(e)) diff --git a/src/view/com/modals/ContentFilteringSettings.tsx b/src/view/com/modals/ContentFilteringSettings.tsx index 9075d027..0891a647 100644 --- a/src/view/com/modals/ContentFilteringSettings.tsx +++ b/src/view/com/modals/ContentFilteringSettings.tsx @@ -16,6 +16,7 @@ import {isIOS} from 'platform/detection' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import * as Toast from '../util/Toast' import {logger} from '#/logger' +import {useModalControls} from '#/state/modals' export const snapPoints = ['90%'] @@ -24,14 +25,15 @@ export const Component = observer( const store = useStores() const {isMobile} = useWebMediaQueries() const pal = usePalette('default') + const {closeModal} = useModalControls() React.useEffect(() => { store.preferences.sync() }, [store]) const onPressDone = React.useCallback(() => { - store.shell.closeModal() - }, [store]) + closeModal() + }, [closeModal]) return ( <View testID="contentFilteringModal" style={[pal.view, styles.container]}> @@ -89,8 +91,9 @@ const AdultContentEnabledPref = observer( function AdultContentEnabledPrefImpl() { const store = useStores() const pal = usePalette('default') + const {openModal} = useModalControls() - const onSetAge = () => store.shell.openModal({name: 'birth-date-settings'}) + const onSetAge = () => openModal({name: 'birth-date-settings'}) const onToggleAdultContent = async () => { if (isIOS) { diff --git a/src/view/com/modals/CreateOrEditList.tsx b/src/view/com/modals/CreateOrEditList.tsx index 1ea12695..cdad3777 100644 --- a/src/view/com/modals/CreateOrEditList.tsx +++ b/src/view/com/modals/CreateOrEditList.tsx @@ -24,6 +24,7 @@ import {useTheme} from 'lib/ThemeContext' import {useAnalytics} from 'lib/analytics/analytics' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {cleanError, isNetworkError} from 'lib/strings/errors' +import {useModalControls} from '#/state/modals' const MAX_NAME = 64 // todo const MAX_DESCRIPTION = 300 // todo @@ -40,6 +41,7 @@ export function Component({ list?: ListModel }) { const store = useStores() + const {closeModal} = useModalControls() const {isMobile} = useWebMediaQueries() const [error, setError] = useState<string>('') const pal = usePalette('default') @@ -67,8 +69,8 @@ export function Component({ const [newAvatar, setNewAvatar] = useState<RNImage | undefined | null>() const onPressCancel = useCallback(() => { - store.shell.closeModal() - }, [store]) + closeModal() + }, [closeModal]) const onSelectNewAvatar = useCallback( async (img: RNImage | null) => { @@ -123,7 +125,7 @@ export function Component({ Toast.show(`${purposeLabel} list created`) onSave?.(res.uri) } - store.shell.closeModal() + closeModal() } catch (e: any) { if (isNetworkError(e)) { setError( @@ -141,6 +143,7 @@ export function Component({ error, onSave, store, + closeModal, activePurpose, isCurateList, purposeLabel, diff --git a/src/view/com/modals/DeleteAccount.tsx b/src/view/com/modals/DeleteAccount.tsx index 50a4cd60..9a8a8b4b 100644 --- a/src/view/com/modals/DeleteAccount.tsx +++ b/src/view/com/modals/DeleteAccount.tsx @@ -17,6 +17,7 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {ErrorMessage} from '../util/error/ErrorMessage' import {cleanError} from 'lib/strings/errors' import {resetToTab} from '../../../Navigation' +import {useModalControls} from '#/state/modals' export const snapPoints = ['60%'] @@ -24,6 +25,7 @@ export function Component({}: {}) { const pal = usePalette('default') const theme = useTheme() const store = useStores() + const {closeModal} = useModalControls() const {isMobile} = useWebMediaQueries() const [isEmailSent, setIsEmailSent] = React.useState<boolean>(false) const [confirmCode, setConfirmCode] = React.useState<string>('') @@ -55,14 +57,14 @@ export function Component({}: {}) { Toast.show('Your account has been deleted') resetToTab('HomeTab') store.session.clear() - store.shell.closeModal() + closeModal() } catch (e: any) { setError(cleanError(e)) } setIsProcessing(false) } const onCancel = () => { - store.shell.closeModal() + closeModal() } return ( <View style={[styles.container, pal.view]}> diff --git a/src/view/com/modals/EditImage.tsx b/src/view/com/modals/EditImage.tsx index dcb6668c..a2a458f4 100644 --- a/src/view/com/modals/EditImage.tsx +++ b/src/view/com/modals/EditImage.tsx @@ -6,7 +6,6 @@ import {gradients, s} from 'lib/styles' import {useTheme} from 'lib/ThemeContext' import {Text} from '../util/text/Text' import LinearGradient from 'react-native-linear-gradient' -import {useStores} from 'state/index' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import ImageEditor, {Position} from 'react-avatar-editor' import {TextInput} from './util' @@ -19,6 +18,7 @@ import {Slider} from '@miblanchard/react-native-slider' import {MaterialIcons} from '@expo/vector-icons' import {observer} from 'mobx-react-lite' import {getKeys} from 'lib/type-assertions' +import {useModalControls} from '#/state/modals' export const snapPoints = ['80%'] @@ -52,9 +52,9 @@ export const Component = observer(function EditImageImpl({ }: Props) { const pal = usePalette('default') const theme = useTheme() - const store = useStores() const windowDimensions = useWindowDimensions() const {isMobile} = useWebMediaQueries() + const {closeModal} = useModalControls() const { aspectRatio, @@ -128,8 +128,8 @@ export const Component = observer(function EditImageImpl({ }, [image]) const onCloseModal = useCallback(() => { - store.shell.closeModal() - }, [store.shell]) + closeModal() + }, [closeModal]) const onPressCancel = useCallback(async () => { await gallery.previous(image) diff --git a/src/view/com/modals/EditProfile.tsx b/src/view/com/modals/EditProfile.tsx index dfd5305f..f08bb232 100644 --- a/src/view/com/modals/EditProfile.tsx +++ b/src/view/com/modals/EditProfile.tsx @@ -13,7 +13,6 @@ import LinearGradient from 'react-native-linear-gradient' import {Image as RNImage} from 'react-native-image-crop-picker' import {Text} from '../util/text/Text' import {ErrorMessage} from '../util/error/ErrorMessage' -import {useStores} from 'state/index' import {ProfileModel} from 'state/models/content/profile' import {s, colors, gradients} from 'lib/styles' import {enforceLen} from 'lib/strings/helpers' @@ -27,6 +26,7 @@ import {useAnalytics} from 'lib/analytics/analytics' import {cleanError, isNetworkError} from 'lib/strings/errors' import Animated, {FadeOut} from 'react-native-reanimated' import {isWeb} from 'platform/detection' +import {useModalControls} from '#/state/modals' const AnimatedTouchableOpacity = Animated.createAnimatedComponent(TouchableOpacity) @@ -40,11 +40,11 @@ export function Component({ profileView: ProfileModel onUpdate?: () => void }) { - const store = useStores() const [error, setError] = useState<string>('') const pal = usePalette('default') const theme = useTheme() const {track} = useAnalytics() + const {closeModal} = useModalControls() const [isProcessing, setProcessing] = useState<boolean>(false) const [displayName, setDisplayName] = useState<string>( @@ -66,7 +66,7 @@ export function Component({ RNImage | undefined | null >() const onPressCancel = () => { - store.shell.closeModal() + closeModal() } const onSelectNewAvatar = useCallback( async (img: RNImage | null) => { @@ -123,7 +123,7 @@ export function Component({ ) Toast.show('Profile updated') onUpdate?.() - store.shell.closeModal() + closeModal() } catch (e: any) { if (isNetworkError(e)) { setError( @@ -141,7 +141,7 @@ export function Component({ error, profileView, onUpdate, - store, + closeModal, displayName, description, newUserAvatar, diff --git a/src/view/com/modals/InviteCodes.tsx b/src/view/com/modals/InviteCodes.tsx index a8aa164c..227b2527 100644 --- a/src/view/com/modals/InviteCodes.tsx +++ b/src/view/com/modals/InviteCodes.tsx @@ -15,6 +15,7 @@ import {ScrollView} from './util' import {usePalette} from 'lib/hooks/usePalette' import {isWeb} from 'platform/detection' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {useModalControls} from '#/state/modals' import {useInvitesState, useInvitesAPI} from '#/state/invites' import {UserInfoText} from '../util/UserInfoText' import {makeProfileLink} from '#/lib/routes/links' @@ -25,11 +26,12 @@ export const snapPoints = ['70%'] export function Component({}: {}) { const pal = usePalette('default') const store = useStores() + const {closeModal} = useModalControls() const {isTabletOrDesktop} = useWebMediaQueries() const onClose = React.useCallback(() => { - store.shell.closeModal() - }, [store]) + closeModal() + }, [closeModal]) if (store.me.invites.length === 0) { return ( diff --git a/src/view/com/modals/LinkWarning.tsx b/src/view/com/modals/LinkWarning.tsx index 67a156af..751c69b3 100644 --- a/src/view/com/modals/LinkWarning.tsx +++ b/src/view/com/modals/LinkWarning.tsx @@ -5,12 +5,12 @@ import {observer} from 'mobx-react-lite' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {Text} from '../util/text/Text' import {Button} from '../util/forms/Button' -import {useStores} from 'state/index' import {s, colors} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' import {isWeb} from 'platform/detection' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {isPossiblyAUrl, splitApexDomain} from 'lib/strings/url-helpers' +import {useModalControls} from '#/state/modals' export const snapPoints = ['50%'] @@ -22,12 +22,12 @@ export const Component = observer(function Component({ href: string }) { const pal = usePalette('default') - const store = useStores() + const {closeModal} = useModalControls() const {isMobile} = useWebMediaQueries() const potentiallyMisleading = isPossiblyAUrl(text) const onPressVisit = () => { - store.shell.closeModal() + closeModal() Linking.openURL(href) } @@ -83,7 +83,7 @@ export const Component = observer(function Component({ <Button testID="cancelBtn" type="default" - onPress={() => store.shell.closeModal()} + onPress={() => closeModal()} accessibilityLabel="Cancel" accessibilityHint="" label="Cancel" diff --git a/src/view/com/modals/ListAddUser.tsx b/src/view/com/modals/ListAddUser.tsx index a04e2d18..8864ebc7 100644 --- a/src/view/com/modals/ListAddUser.tsx +++ b/src/view/com/modals/ListAddUser.tsx @@ -26,6 +26,7 @@ import {cleanError} from 'lib/strings/errors' import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeHandle} from 'lib/strings/handles' import {HITSLOP_20} from '#/lib/constants' +import {useModalControls} from '#/state/modals' export const snapPoints = ['90%'] @@ -38,6 +39,7 @@ export const Component = observer(function Component({ }) { const pal = usePalette('default') const store = useStores() + const {closeModal} = useModalControls() const {isMobile} = useWebMediaQueries() const [query, setQuery] = useState('') const autocompleteView = useMemo<UserAutocompleteModel>( @@ -146,7 +148,7 @@ export const Component = observer(function Component({ <Button testID="doneBtn" type="default" - onPress={() => store.shell.closeModal()} + onPress={() => closeModal()} accessibilityLabel="Done" accessibilityHint="" label="Done" diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index 5aaa09e8..c1999c5d 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -3,13 +3,13 @@ import {StyleSheet} from 'react-native' import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context' import {observer} from 'mobx-react-lite' import BottomSheet from '@gorhom/bottom-sheet' -import {useStores} from 'state/index' import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop' import {usePalette} from 'lib/hooks/usePalette' import {timeout} from 'lib/async/timeout' import {navigate} from '../../../Navigation' import once from 'lodash.once' +import {useModals, useModalControls} from '#/state/modals' import * as ConfirmModal from './Confirm' import * as EditProfileModal from './EditProfile' import * as ProfilePreviewModal from './ProfilePreview' @@ -41,17 +41,17 @@ const DEFAULT_SNAPPOINTS = ['90%'] const HANDLE_HEIGHT = 24 export const ModalsContainer = observer(function ModalsContainer() { - const store = useStores() + const {isModalActive, activeModals} = useModals() + const {closeModal} = useModalControls() const bottomSheetRef = useRef<BottomSheet>(null) const pal = usePalette('default') const safeAreaInsets = useSafeAreaInsets() - const activeModal = - store.shell.activeModals[store.shell.activeModals.length - 1] + const activeModal = activeModals[activeModals.length - 1] const navigateOnce = once(navigate) - const onBottomSheetAnimate = (fromIndex: number, toIndex: number) => { + const onBottomSheetAnimate = (_fromIndex: number, toIndex: number) => { if (activeModal?.name === 'profile-preview' && toIndex === 1) { // begin loading the profile screen behind the scenes navigateOnce('Profile', {name: activeModal.did}) @@ -59,7 +59,7 @@ export const ModalsContainer = observer(function ModalsContainer() { } const onBottomSheetChange = async (snapPoint: number) => { if (snapPoint === -1) { - store.shell.closeModal() + closeModal() } else if (activeModal?.name === 'profile-preview' && snapPoint === 1) { await navigateOnce('Profile', {name: activeModal.did}) // There is no particular callback for when the view has actually been presented. @@ -67,21 +67,21 @@ export const ModalsContainer = observer(function ModalsContainer() { // It's acceptable because the data is already being fetched + it usually takes longer anyway. // TODO: Figure out why avatar/cover don't always show instantly from cache. await timeout(200) - store.shell.closeModal() + closeModal() } } const onClose = () => { bottomSheetRef.current?.close() - store.shell.closeModal() + closeModal() } useEffect(() => { - if (store.shell.isModalActive) { + if (isModalActive) { bottomSheetRef.current?.expand() } else { bottomSheetRef.current?.close() } - }, [store.shell.isModalActive, bottomSheetRef, activeModal?.name]) + }, [isModalActive, bottomSheetRef, activeModal?.name]) let needsSafeTopInset = false let snapPoints: (string | number)[] = DEFAULT_SNAPPOINTS @@ -184,12 +184,12 @@ export const ModalsContainer = observer(function ModalsContainer() { snapPoints={snapPoints} topInset={topInset} handleHeight={HANDLE_HEIGHT} - index={store.shell.isModalActive ? 0 : -1} + index={isModalActive ? 0 : -1} enablePanDownToClose android_keyboardInputMode="adjustResize" keyboardBlurBehavior="restore" backdropComponent={ - store.shell.isModalActive ? createCustomBackdrop(onClose) : undefined + isModalActive ? createCustomBackdrop(onClose) : undefined } handleIndicatorStyle={{backgroundColor: pal.text.color}} handleStyle={[styles.handle, pal.view]} diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index ede84537..65c4ee44 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -1,11 +1,11 @@ import React from 'react' import {TouchableWithoutFeedback, StyleSheet, View} from 'react-native' import {observer} from 'mobx-react-lite' -import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' -import type {Modal as ModalIface} from 'state/models/ui/shell' +import type {Modal as ModalIface} from '#/state/modals' +import {useModals, useModalControls} from '#/state/modals' import * as ConfirmModal from './Confirm' import * as EditProfileModal from './EditProfile' import * as ProfilePreviewModal from './ProfilePreview' @@ -34,15 +34,15 @@ import * as ChangeEmailModal from './ChangeEmail' import * as LinkWarningModal from './LinkWarning' export const ModalsContainer = observer(function ModalsContainer() { - const store = useStores() + const {isModalActive, activeModals} = useModals() - if (!store.shell.isModalActive) { + if (!isModalActive) { return null } return ( <> - {store.shell.activeModals.map((modal, i) => ( + {activeModals.map((modal, i) => ( <Modal key={`modal-${i}`} modal={modal} /> ))} </> @@ -50,11 +50,12 @@ export const ModalsContainer = observer(function ModalsContainer() { }) function Modal({modal}: {modal: ModalIface}) { - const store = useStores() + const {isModalActive} = useModals() + const {closeModal} = useModalControls() const pal = usePalette('default') const {isMobile} = useWebMediaQueries() - if (!store.shell.isModalActive) { + if (!isModalActive) { return null } @@ -62,7 +63,7 @@ function Modal({modal}: {modal: ModalIface}) { if (modal.name === 'crop-image' || modal.name === 'edit-image') { return // dont close on mask presses during crop } - store.shell.closeModal() + closeModal() } const onInnerPress = () => { // TODO: can we use prevent default? diff --git a/src/view/com/modals/ModerationDetails.tsx b/src/view/com/modals/ModerationDetails.tsx index c01312d6..35ddfe2a 100644 --- a/src/view/com/modals/ModerationDetails.tsx +++ b/src/view/com/modals/ModerationDetails.tsx @@ -1,7 +1,6 @@ import React from 'react' import {StyleSheet, View} from 'react-native' import {ModerationUI} from '@atproto/api' -import {useStores} from 'state/index' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {s} from 'lib/styles' import {Text} from '../util/text/Text' @@ -10,6 +9,7 @@ import {usePalette} from 'lib/hooks/usePalette' import {isWeb} from 'platform/detection' import {listUriToHref} from 'lib/strings/url-helpers' import {Button} from '../util/forms/Button' +import {useModalControls} from '#/state/modals' export const snapPoints = [300] @@ -20,7 +20,7 @@ export function Component({ context: 'account' | 'content' moderation: ModerationUI }) { - const store = useStores() + const {closeModal} = useModalControls() const {isMobile} = useWebMediaQueries() const pal = usePalette('default') @@ -99,10 +99,7 @@ export function Component({ {description} </Text> <View style={s.flex1} /> - <Button - type="primary" - style={styles.btn} - onPress={() => store.shell.closeModal()}> + <Button type="primary" style={styles.btn} onPress={() => closeModal()}> <Text type="button-lg" style={[pal.textLight, s.textCenter, s.white]}> Okay </Text> diff --git a/src/view/com/modals/Repost.tsx b/src/view/com/modals/Repost.tsx index b1862ecb..13728b62 100644 --- a/src/view/com/modals/Repost.tsx +++ b/src/view/com/modals/Repost.tsx @@ -1,12 +1,12 @@ import React from 'react' import {StyleSheet, TouchableOpacity, View} from 'react-native' import LinearGradient from 'react-native-linear-gradient' -import {useStores} from 'state/index' import {s, colors, gradients} from 'lib/styles' import {Text} from '../util/text/Text' import {usePalette} from 'lib/hooks/usePalette' import {RepostIcon} from 'lib/icons' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {useModalControls} from '#/state/modals' export const snapPoints = [250] @@ -20,10 +20,10 @@ export function Component({ isReposted: boolean // TODO: Add author into component }) { - const store = useStores() const pal = usePalette('default') + const {closeModal} = useModalControls() const onPress = async () => { - store.shell.closeModal() + closeModal() } return ( diff --git a/src/view/com/modals/SelfLabel.tsx b/src/view/com/modals/SelfLabel.tsx index 820f2895..242b6a38 100644 --- a/src/view/com/modals/SelfLabel.tsx +++ b/src/view/com/modals/SelfLabel.tsx @@ -2,7 +2,6 @@ import React, {useState} from 'react' import {StyleSheet, TouchableOpacity, View} from 'react-native' import {observer} from 'mobx-react-lite' import {Text} from '../util/text/Text' -import {useStores} from 'state/index' import {s, colors} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' @@ -10,6 +9,7 @@ import {isWeb} from 'platform/detection' import {Button} from '../util/forms/Button' import {SelectableBtn} from '../util/forms/SelectableBtn' import {ScrollView} from 'view/com/modals/util' +import {useModalControls} from '#/state/modals' const ADULT_CONTENT_LABELS = ['sexual', 'nudity', 'porn'] @@ -25,7 +25,7 @@ export const Component = observer(function Component({ onChange: (labels: string[]) => void }) { const pal = usePalette('default') - const store = useStores() + const {closeModal} = useModalControls() const {isMobile} = useWebMediaQueries() const [selected, setSelected] = useState(labels) @@ -143,7 +143,7 @@ export const Component = observer(function Component({ <TouchableOpacity testID="confirmBtn" onPress={() => { - store.shell.closeModal() + closeModal() }} style={styles.btn} accessibilityRole="button" diff --git a/src/view/com/modals/ServerInput.tsx b/src/view/com/modals/ServerInput.tsx index 13b21fe2..0f8db30b 100644 --- a/src/view/com/modals/ServerInput.tsx +++ b/src/view/com/modals/ServerInput.tsx @@ -6,26 +6,26 @@ import { } from '@fortawesome/react-native-fontawesome' import {ScrollView, TextInput} from './util' import {Text} from '../util/text/Text' -import {useStores} from 'state/index' import {s, colors} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' import {useTheme} from 'lib/ThemeContext' import {LOCAL_DEV_SERVICE, STAGING_SERVICE, PROD_SERVICE} from 'state/index' import {LOGIN_INCLUDE_DEV_SERVERS} from 'lib/build-flags' +import {useModalControls} from '#/state/modals' export const snapPoints = ['80%'] export function Component({onSelect}: {onSelect: (url: string) => void}) { const theme = useTheme() const pal = usePalette('default') - const store = useStores() const [customUrl, setCustomUrl] = useState<string>('') + const {closeModal} = useModalControls() const doSelect = (url: string) => { if (!url.startsWith('http://') && !url.startsWith('https://')) { url = `https://${url}` } - store.shell.closeModal() + closeModal() onSelect(url) } diff --git a/src/view/com/modals/UserAddRemoveLists.tsx b/src/view/com/modals/UserAddRemoveLists.tsx index aeec2e87..f86e8843 100644 --- a/src/view/com/modals/UserAddRemoveLists.tsx +++ b/src/view/com/modals/UserAddRemoveLists.tsx @@ -21,6 +21,7 @@ import {usePalette} from 'lib/hooks/usePalette' import {isWeb, isAndroid} from 'platform/detection' import isEqual from 'lodash.isequal' import {logger} from '#/logger' +import {useModalControls} from '#/state/modals' export const snapPoints = ['fullscreen'] @@ -36,6 +37,7 @@ export const Component = observer(function UserAddRemoveListsImpl({ onRemove?: (listUri: string) => void }) { const store = useStores() + const {closeModal} = useModalControls() const pal = usePalette('default') const palPrimary = usePalette('primary') const palInverted = usePalette('inverted') @@ -69,8 +71,8 @@ export const Component = observer(function UserAddRemoveListsImpl({ }, [memberships, listsList, store, setSelected, setMembershipsLoaded]) const onPressCancel = useCallback(() => { - store.shell.closeModal() - }, [store]) + closeModal() + }, [closeModal]) const onPressSave = useCallback(async () => { let changes @@ -87,8 +89,8 @@ export const Component = observer(function UserAddRemoveListsImpl({ for (const uri of changes.removed) { onRemove?.(uri) } - store.shell.closeModal() - }, [store, selected, memberships, onAdd, onRemove]) + closeModal() + }, [closeModal, selected, memberships, onAdd, onRemove]) const onToggleSelected = useCallback( (uri: string) => { diff --git a/src/view/com/modals/VerifyEmail.tsx b/src/view/com/modals/VerifyEmail.tsx index 9fe8811b..3adaffb1 100644 --- a/src/view/com/modals/VerifyEmail.tsx +++ b/src/view/com/modals/VerifyEmail.tsx @@ -20,6 +20,7 @@ import {usePalette} from 'lib/hooks/usePalette' import {isWeb} from 'platform/detection' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {cleanError} from 'lib/strings/errors' +import {useModalControls} from '#/state/modals' export const snapPoints = ['90%'] @@ -43,6 +44,7 @@ export const Component = observer(function Component({ const [isProcessing, setIsProcessing] = useState<boolean>(false) const [error, setError] = useState<string>('') const {isMobile} = useWebMediaQueries() + const {openModal, closeModal} = useModalControls() const onSendEmail = async () => { setError('') @@ -67,7 +69,7 @@ export const Component = observer(function Component({ }) store.session.updateLocalAccountData({emailConfirmed: true}) Toast.show('Email verified') - store.shell.closeModal() + closeModal() } catch (e) { setError(cleanError(String(e))) } finally { @@ -76,8 +78,8 @@ export const Component = observer(function Component({ } const onEmailIncorrect = () => { - store.shell.closeModal() - store.shell.openModal({name: 'change-email'}) + closeModal() + openModal({name: 'change-email'}) } return ( @@ -224,7 +226,7 @@ export const Component = observer(function Component({ <Button testID="cancelBtn" type="default" - onPress={() => store.shell.closeModal()} + onPress={() => closeModal()} accessibilityLabel={ stage === Stages.Reminder ? 'Not right now' : 'Cancel' } diff --git a/src/view/com/modals/Waitlist.tsx b/src/view/com/modals/Waitlist.tsx index 0fb371fe..219bdc58 100644 --- a/src/view/com/modals/Waitlist.tsx +++ b/src/view/com/modals/Waitlist.tsx @@ -12,19 +12,19 @@ import { } from '@fortawesome/react-native-fontawesome' import LinearGradient from 'react-native-linear-gradient' import {Text} from '../util/text/Text' -import {useStores} from 'state/index' import {s, gradients} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' import {useTheme} from 'lib/ThemeContext' import {ErrorMessage} from '../util/error/ErrorMessage' import {cleanError} from 'lib/strings/errors' +import {useModalControls} from '#/state/modals' export const snapPoints = ['80%'] export function Component({}: {}) { const pal = usePalette('default') const theme = useTheme() - const store = useStores() + const {closeModal} = useModalControls() const [email, setEmail] = React.useState<string>('') const [isEmailSent, setIsEmailSent] = React.useState<boolean>(false) const [isProcessing, setIsProcessing] = React.useState<boolean>(false) @@ -54,7 +54,7 @@ export function Component({}: {}) { setIsProcessing(false) } const onCancel = () => { - store.shell.closeModal() + closeModal() } return ( diff --git a/src/view/com/modals/crop-image/CropImage.web.tsx b/src/view/com/modals/crop-image/CropImage.web.tsx index 8e35201d..c88d002a 100644 --- a/src/view/com/modals/crop-image/CropImage.web.tsx +++ b/src/view/com/modals/crop-image/CropImage.web.tsx @@ -7,10 +7,10 @@ import {Text} from 'view/com/util/text/Text' import {Dimensions} from 'lib/media/types' import {getDataUriSize} from 'lib/media/util' import {s, gradients} from 'lib/styles' -import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' import {SquareIcon, RectWideIcon, RectTallIcon} from 'lib/icons' import {Image as RNImage} from 'react-native-image-crop-picker' +import {useModalControls} from '#/state/modals' enum AspectRatio { Square = 'square', @@ -33,7 +33,7 @@ export function Component({ uri: string onSelect: (img?: RNImage) => void }) { - const store = useStores() + const {closeModal} = useModalControls() const pal = usePalette('default') const [as, setAs] = React.useState<AspectRatio>(AspectRatio.Square) const [scale, setScale] = React.useState<number>(1) @@ -43,7 +43,7 @@ export function Component({ const onPressCancel = () => { onSelect(undefined) - store.shell.closeModal() + closeModal() } const onPressDone = () => { const canvas = editorRef.current?.getImageScaledToCanvas() @@ -59,7 +59,7 @@ export function Component({ } else { onSelect(undefined) } - store.shell.closeModal() + closeModal() } let cropperStyle diff --git a/src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx b/src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx index 65924561..d37d51e4 100644 --- a/src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx +++ b/src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx @@ -1,7 +1,6 @@ import React from 'react' import {StyleSheet, View} from 'react-native' import {ScrollView} from '../util' -import {useStores} from 'state/index' import {Text} from '../../util/text/Text' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' @@ -9,6 +8,7 @@ import {deviceLocales} from 'platform/detection' import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages' import {LanguageToggle} from './LanguageToggle' import {ConfirmLanguagesButton} from './ConfirmLanguagesButton' +import {useModalControls} from '#/state/modals' import { useLanguagePrefs, useSetLanguagePrefs, @@ -18,14 +18,14 @@ import { export const snapPoints = ['100%'] export function Component({}: {}) { - const store = useStores() + const {closeModal} = useModalControls() const langPrefs = useLanguagePrefs() const setLangPrefs = useSetLanguagePrefs() const pal = usePalette('default') const {isMobile} = useWebMediaQueries() const onPressDone = React.useCallback(() => { - store.shell.closeModal() - }, [store]) + closeModal() + }, [closeModal]) const languages = React.useMemo(() => { const langs = LANGUAGES.filter( diff --git a/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx b/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx index 435fb9e1..4a39da75 100644 --- a/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx +++ b/src/view/com/modals/lang-settings/PostLanguagesSettings.tsx @@ -2,7 +2,6 @@ import React from 'react' import {StyleSheet, View} from 'react-native' import {observer} from 'mobx-react-lite' import {ScrollView} from '../util' -import {useStores} from 'state/index' import {Text} from '../../util/text/Text' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' @@ -10,6 +9,7 @@ import {deviceLocales} from 'platform/detection' import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages' import {ConfirmLanguagesButton} from './ConfirmLanguagesButton' import {ToggleButton} from 'view/com/util/forms/ToggleButton' +import {useModalControls} from '#/state/modals' import { useLanguagePrefs, useSetLanguagePrefs, @@ -20,14 +20,14 @@ import { export const snapPoints = ['100%'] export const Component = observer(function PostLanguagesSettingsImpl() { - const store = useStores() + const {closeModal} = useModalControls() const langPrefs = useLanguagePrefs() const setLangPrefs = useSetLanguagePrefs() const pal = usePalette('default') const {isMobile} = useWebMediaQueries() const onPressDone = React.useCallback(() => { - store.shell.closeModal() - }, [store]) + closeModal() + }, [closeModal]) const languages = React.useMemo(() => { const langs = LANGUAGES.filter( diff --git a/src/view/com/modals/report/Modal.tsx b/src/view/com/modals/report/Modal.tsx index 98aa2d47..8dc3f53f 100644 --- a/src/view/com/modals/report/Modal.tsx +++ b/src/view/com/modals/report/Modal.tsx @@ -14,6 +14,7 @@ import {SendReportButton} from './SendReportButton' import {InputIssueDetails} from './InputIssueDetails' import {ReportReasonOptions} from './ReasonOptions' import {CollectionId} from './types' +import {useModalControls} from '#/state/modals' const DMCA_LINK = 'https://blueskyweb.xyz/support/copyright' @@ -37,6 +38,7 @@ type ReportComponentProps = export function Component(content: ReportComponentProps) { const store = useStores() + const {closeModal} = useModalControls() const pal = usePalette('default') const {isMobile} = useWebMediaQueries() const [isProcessing, setIsProcessing] = useState(false) @@ -60,7 +62,7 @@ export function Component(content: ReportComponentProps) { try { if (issue === '__copyright__') { Linking.openURL(DMCA_LINK) - store.shell.closeModal() + closeModal() return } const $type = !isAccountReport @@ -76,7 +78,7 @@ export function Component(content: ReportComponentProps) { }) Toast.show("Thank you for your report! We'll look into it promptly.") - store.shell.closeModal() + closeModal() return } catch (e: any) { setError(cleanError(e)) diff --git a/src/view/com/posts/FeedErrorMessage.tsx b/src/view/com/posts/FeedErrorMessage.tsx index 9e75d950..84e438fc 100644 --- a/src/view/com/posts/FeedErrorMessage.tsx +++ b/src/view/com/posts/FeedErrorMessage.tsx @@ -11,6 +11,7 @@ import {useNavigation} from '@react-navigation/native' import {NavigationProp} from 'lib/routes/types' import {useStores} from 'state/index' import {logger} from '#/logger' +import {useModalControls} from '#/state/modals' const MESSAGES = { [KnownError.Unknown]: '', @@ -57,13 +58,14 @@ function FeedgenErrorMessage({ const msg = MESSAGES[knownError] const uri = (feed.params as GetCustomFeed.QueryParams).feed const [ownerDid] = safeParseFeedgenUri(uri) + const {openModal, closeModal} = useModalControls() const onViewProfile = React.useCallback(() => { navigation.navigate('Profile', {name: ownerDid}) }, [navigation, ownerDid]) const onRemoveFeed = React.useCallback(async () => { - store.shell.openModal({ + openModal({ name: 'confirm', title: 'Remove feed', message: 'Remove this feed from your saved feeds?', @@ -78,10 +80,10 @@ function FeedgenErrorMessage({ } }, onPressCancel() { - store.shell.closeModal() + closeModal() }, }) - }, [store, uri]) + }, [store, openModal, closeModal, uri]) return ( <View diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx index 1a1d38e4..1ee20978 100644 --- a/src/view/com/profile/ProfileHeader.tsx +++ b/src/view/com/profile/ProfileHeader.tsx @@ -40,6 +40,7 @@ import {makeProfileLink} from 'lib/routes/links' import {Link} from '../util/Link' import {ProfileHeaderSuggestedFollows} from './ProfileHeaderSuggestedFollows' import {logger} from '#/logger' +import {useModalControls} from '#/state/modals' interface Props { view: ProfileModel @@ -113,6 +114,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({ const pal = usePalette('default') const palInverted = usePalette('inverted') const store = useStores() + const {openModal} = useModalControls() const navigation = useNavigation<NavigationProp>() const {track} = useAnalytics() const invalidHandle = isInvalidHandle(view.handle) @@ -157,12 +159,12 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({ const onPressEditProfile = React.useCallback(() => { track('ProfileHeader:EditProfileButtonClicked') - store.shell.openModal({ + openModal({ name: 'edit-profile', profileView: view, onUpdate: onRefreshAll, }) - }, [track, store, view, onRefreshAll]) + }, [track, openModal, view, onRefreshAll]) const trackPress = React.useCallback( (f: 'Followers' | 'Follows') => { @@ -181,12 +183,12 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({ const onPressAddRemoveLists = React.useCallback(() => { track('ProfileHeader:AddToListsButtonClicked') - store.shell.openModal({ + openModal({ name: 'user-add-remove-lists', subject: view.did, displayName: view.displayName || view.handle, }) - }, [track, view, store]) + }, [track, view, openModal]) const onPressMuteAccount = React.useCallback(async () => { track('ProfileHeader:MuteAccountButtonClicked') @@ -212,7 +214,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({ const onPressBlockAccount = React.useCallback(async () => { track('ProfileHeader:BlockAccountButtonClicked') - store.shell.openModal({ + openModal({ name: 'confirm', title: 'Block Account', message: @@ -228,11 +230,11 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({ } }, }) - }, [track, view, store, onRefreshAll]) + }, [track, view, openModal, onRefreshAll]) const onPressUnblockAccount = React.useCallback(async () => { track('ProfileHeader:UnblockAccountButtonClicked') - store.shell.openModal({ + openModal({ name: 'confirm', title: 'Unblock Account', message: @@ -248,15 +250,15 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({ } }, }) - }, [track, view, store, onRefreshAll]) + }, [track, view, openModal, onRefreshAll]) const onPressReportAccount = React.useCallback(() => { track('ProfileHeader:ReportAccountButtonClicked') - store.shell.openModal({ + openModal({ name: 'report', did: view.did, }) - }, [track, store, view]) + }, [track, openModal, view]) const isMe = React.useMemo( () => store.me.did === view.did, diff --git a/src/view/com/testing/TestCtrls.e2e.tsx b/src/view/com/testing/TestCtrls.e2e.tsx index db9b6b4b..2f36609e 100644 --- a/src/view/com/testing/TestCtrls.e2e.tsx +++ b/src/view/com/testing/TestCtrls.e2e.tsx @@ -2,6 +2,7 @@ import React from 'react' import {Pressable, View} from 'react-native' import {useStores} from 'state/index' import {navigate} from '../../../Navigation' +import {useModalControls} from '#/state/modals' /** * This utility component is only included in the test simulator @@ -13,6 +14,7 @@ const BTN = {height: 1, width: 1, backgroundColor: 'red'} export function TestCtrls() { const store = useStores() + const {openModal} = useModalControls() const onPressSignInAlice = async () => { await store.session.login({ service: 'http://localhost:3000', @@ -85,7 +87,7 @@ export function TestCtrls() { /> <Pressable testID="e2eOpenInviteCodesModal" - onPress={() => store.shell.openModal({name: 'invite-codes'})} + onPress={() => openModal({name: 'invite-codes'})} accessibilityRole="button" style={BTN} /> diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx index 1777f665..074ab232 100644 --- a/src/view/com/util/Link.tsx +++ b/src/view/com/util/Link.tsx @@ -21,7 +21,6 @@ import {Text} from './text/Text' import {TypographyVariant} from 'lib/ThemeContext' import {NavigationProp} from 'lib/routes/types' import {router} from '../../../routes' -import {useStores, RootStoreModel} from 'state/index' import { convertBskyAppUrlIfNeeded, isExternalUrl, @@ -31,6 +30,7 @@ import {isAndroid, isWeb} from 'platform/detection' import {sanitizeUrl} from '@braintree/sanitize-url' import {PressableWithHover} from './PressableWithHover' import FixedTouchableHighlight from '../pager/FixedTouchableHighlight' +import {useModalControls} from '#/state/modals' type Event = | React.MouseEvent<HTMLAnchorElement, MouseEvent> @@ -60,17 +60,17 @@ export const Link = memo(function Link({ anchorNoUnderline, ...props }: Props) { - const store = useStores() + const {closeModal} = useModalControls() const navigation = useNavigation<NavigationProp>() const anchorHref = asAnchor ? sanitizeUrl(href) : undefined const onPress = React.useCallback( (e?: Event) => { if (typeof href === 'string') { - return onPressInner(store, navigation, sanitizeUrl(href), e) + return onPressInner(closeModal, navigation, sanitizeUrl(href), e) } }, - [store, navigation, href], + [closeModal, navigation, href], ) if (noFeedback) { @@ -160,8 +160,8 @@ export const TextLink = memo(function TextLink({ warnOnMismatchingLabel?: boolean } & TextProps) { const {...props} = useLinkProps({to: sanitizeUrl(href)}) - const store = useStores() const navigation = useNavigation<NavigationProp>() + const {openModal, closeModal} = useModalControls() if (warnOnMismatchingLabel && typeof text !== 'string') { console.error('Unable to detect mismatching label') @@ -174,7 +174,7 @@ export const TextLink = memo(function TextLink({ linkRequiresWarning(href, typeof text === 'string' ? text : '') if (requiresWarning) { e?.preventDefault?.() - store.shell.openModal({ + openModal({ name: 'link-warning', text: typeof text === 'string' ? text : '', href, @@ -185,9 +185,17 @@ export const TextLink = memo(function TextLink({ // @ts-ignore function signature differs by platform -prf return onPress() } - return onPressInner(store, navigation, sanitizeUrl(href), e) + return onPressInner(closeModal, navigation, sanitizeUrl(href), e) }, - [onPress, store, navigation, href, text, warnOnMismatchingLabel], + [ + onPress, + closeModal, + openModal, + navigation, + href, + text, + warnOnMismatchingLabel, + ], ) const hrefAttrs = useMemo(() => { const isExternal = isExternalUrl(href) @@ -285,7 +293,7 @@ export const TextLinkOnWebOnly = memo(function DesktopWebTextLink({ // needed customizations // -prf function onPressInner( - store: RootStoreModel, + closeModal = () => {}, navigation: NavigationProp, href: string, e?: Event, @@ -318,7 +326,7 @@ function onPressInner( if (newTab || href.startsWith('http') || href.startsWith('mailto')) { Linking.openURL(href) } else { - store.shell.closeModal() // close any active modals + closeModal() // close any active modals // @ts-ignore we're not able to type check on this one -prf navigation.dispatch(StackActions.push(...router.matchPath(href))) diff --git a/src/view/com/util/UserPreviewLink.tsx b/src/view/com/util/UserPreviewLink.tsx index f43f9e80..9c5efe55 100644 --- a/src/view/com/util/UserPreviewLink.tsx +++ b/src/view/com/util/UserPreviewLink.tsx @@ -1,9 +1,9 @@ import React from 'react' import {Pressable, StyleProp, ViewStyle} from 'react-native' -import {useStores} from 'state/index' import {Link} from './Link' import {isWeb} from 'platform/detection' import {makeProfileLink} from 'lib/routes/links' +import {useModalControls} from '#/state/modals' interface UserPreviewLinkProps { did: string @@ -13,7 +13,7 @@ interface UserPreviewLinkProps { export function UserPreviewLink( props: React.PropsWithChildren<UserPreviewLinkProps>, ) { - const store = useStores() + const {openModal} = useModalControls() if (isWeb) { return ( @@ -29,7 +29,7 @@ export function UserPreviewLink( return ( <Pressable onPress={() => - store.shell.openModal({ + openModal({ name: 'profile-preview', did: props.did, }) diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx index 1fffa312..45abed64 100644 --- a/src/view/com/util/forms/PostDropdownBtn.tsx +++ b/src/view/com/util/forms/PostDropdownBtn.tsx @@ -2,7 +2,6 @@ import React from 'react' import {StyleProp, View, ViewStyle} from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {toShareUrl} from 'lib/strings/url-helpers' -import {useStores} from 'state/index' import {useTheme} from 'lib/ThemeContext' import {shareUrl} from 'lib/sharing' import { @@ -10,6 +9,7 @@ import { DropdownItem as NativeDropdownItem, } from './NativeDropdown' import {EventStopper} from '../EventStopper' +import {useModalControls} from '#/state/modals' export function PostDropdownBtn({ testID, @@ -37,9 +37,9 @@ export function PostDropdownBtn({ onDeletePost: () => void style?: StyleProp<ViewStyle> }) { - const store = useStores() const theme = useTheme() const defaultCtrlColor = theme.palette.default.postCtrl + const {openModal} = useModalControls() const dropdownItems: NativeDropdownItem[] = [ { @@ -108,7 +108,7 @@ export function PostDropdownBtn({ !isAuthor && { label: 'Report post', onPress() { - store.shell.openModal({ + openModal({ name: 'report', uri: itemUri, cid: itemCid, @@ -129,7 +129,7 @@ export function PostDropdownBtn({ isAuthor && { label: 'Delete post', onPress() { - store.shell.openModal({ + openModal({ name: 'confirm', title: 'Delete this post?', message: 'Are you sure? This can not be undone.', diff --git a/src/view/com/util/moderation/ContentHider.tsx b/src/view/com/util/moderation/ContentHider.tsx index 4f917844..b6fe0dd8 100644 --- a/src/view/com/util/moderation/ContentHider.tsx +++ b/src/view/com/util/moderation/ContentHider.tsx @@ -6,7 +6,7 @@ import {ModerationUI} from '@atproto/api' import {Text} from '../text/Text' import {ShieldExclamation} from 'lib/icons' import {describeModerationCause} from 'lib/moderation' -import {useStores} from 'state/index' +import {useModalControls} from '#/state/modals' export function ContentHider({ testID, @@ -22,10 +22,10 @@ export function ContentHider({ style?: StyleProp<ViewStyle> childContainerStyle?: StyleProp<ViewStyle> }>) { - const store = useStores() const pal = usePalette('default') const {isMobile} = useWebMediaQueries() const [override, setOverride] = React.useState(false) + const {openModal} = useModalControls() if (!moderation.blur || (ignoreMute && moderation.cause?.type === 'muted')) { return ( @@ -43,7 +43,7 @@ export function ContentHider({ if (!moderation.noOverride) { setOverride(v => !v) } else { - store.shell.openModal({ + openModal({ name: 'moderation-details', context: 'content', moderation, @@ -62,7 +62,7 @@ export function ContentHider({ ]}> <Pressable onPress={() => { - store.shell.openModal({ + openModal({ name: 'moderation-details', context: 'content', moderation, diff --git a/src/view/com/util/moderation/PostAlerts.tsx b/src/view/com/util/moderation/PostAlerts.tsx index 0dba367f..2c9a7185 100644 --- a/src/view/com/util/moderation/PostAlerts.tsx +++ b/src/view/com/util/moderation/PostAlerts.tsx @@ -5,7 +5,7 @@ import {Text} from '../text/Text' import {usePalette} from 'lib/hooks/usePalette' import {ShieldExclamation} from 'lib/icons' import {describeModerationCause} from 'lib/moderation' -import {useStores} from 'state/index' +import {useModalControls} from '#/state/modals' export function PostAlerts({ moderation, @@ -15,8 +15,8 @@ export function PostAlerts({ includeMute?: boolean style?: StyleProp<ViewStyle> }) { - const store = useStores() const pal = usePalette('default') + const {openModal} = useModalControls() const shouldAlert = !!moderation.cause && moderation.alert if (!shouldAlert) { @@ -27,7 +27,7 @@ export function PostAlerts({ return ( <Pressable onPress={() => { - store.shell.openModal({ + openModal({ name: 'moderation-details', context: 'content', moderation, diff --git a/src/view/com/util/moderation/PostHider.tsx b/src/view/com/util/moderation/PostHider.tsx index d224286b..a9ccf2eb 100644 --- a/src/view/com/util/moderation/PostHider.tsx +++ b/src/view/com/util/moderation/PostHider.tsx @@ -8,7 +8,7 @@ import {Text} from '../text/Text' import {addStyle} from 'lib/styles' import {describeModerationCause} from 'lib/moderation' import {ShieldExclamation} from 'lib/icons' -import {useStores} from 'state/index' +import {useModalControls} from '#/state/modals' interface Props extends ComponentProps<typeof Link> { // testID?: string @@ -25,10 +25,10 @@ export function PostHider({ children, ...props }: Props) { - const store = useStores() const pal = usePalette('default') const {isMobile} = useWebMediaQueries() const [override, setOverride] = React.useState(false) + const {openModal} = useModalControls() if (!moderation.blur) { return ( @@ -63,7 +63,7 @@ export function PostHider({ ]}> <Pressable onPress={() => { - store.shell.openModal({ + openModal({ name: 'moderation-details', context: 'content', moderation, diff --git a/src/view/com/util/moderation/ProfileHeaderAlerts.tsx b/src/view/com/util/moderation/ProfileHeaderAlerts.tsx index 6b7f4e7e..d2406e7a 100644 --- a/src/view/com/util/moderation/ProfileHeaderAlerts.tsx +++ b/src/view/com/util/moderation/ProfileHeaderAlerts.tsx @@ -8,7 +8,7 @@ import { describeModerationCause, getProfileModerationCauses, } from 'lib/moderation' -import {useStores} from 'state/index' +import {useModalControls} from '#/state/modals' export function ProfileHeaderAlerts({ moderation, @@ -17,8 +17,8 @@ export function ProfileHeaderAlerts({ moderation: ProfileModeration style?: StyleProp<ViewStyle> }) { - const store = useStores() const pal = usePalette('default') + const {openModal} = useModalControls() const causes = getProfileModerationCauses(moderation) if (!causes.length) { @@ -34,7 +34,7 @@ export function ProfileHeaderAlerts({ testID="profileHeaderAlert" key={desc.name} onPress={() => { - store.shell.openModal({ + openModal({ name: 'moderation-details', context: 'content', moderation: {cause}, diff --git a/src/view/com/util/moderation/ScreenHider.tsx b/src/view/com/util/moderation/ScreenHider.tsx index 0224b9fe..c3d23b84 100644 --- a/src/view/com/util/moderation/ScreenHider.tsx +++ b/src/view/com/util/moderation/ScreenHider.tsx @@ -18,7 +18,7 @@ import {NavigationProp} from 'lib/routes/types' import {Text} from '../text/Text' import {Button} from '../forms/Button' import {describeModerationCause} from 'lib/moderation' -import {useStores} from 'state/index' +import {useModalControls} from '#/state/modals' export function ScreenHider({ testID, @@ -34,12 +34,12 @@ export function ScreenHider({ style?: StyleProp<ViewStyle> containerStyle?: StyleProp<ViewStyle> }>) { - const store = useStores() const pal = usePalette('default') const palInverted = usePalette('inverted') const [override, setOverride] = React.useState(false) const navigation = useNavigation<NavigationProp>() const {isMobile} = useWebMediaQueries() + const {openModal} = useModalControls() if (!moderation.blur || override) { return ( @@ -72,7 +72,7 @@ export function ScreenHider({ .{' '} <TouchableWithoutFeedback onPress={() => { - store.shell.openModal({ + openModal({ name: 'moderation-details', context: 'account', moderation, diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index 5769a478..7bcea0e7 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -16,6 +16,7 @@ import {useStores} from 'state/index' import {RepostButton} from './RepostButton' import {Haptics} from 'lib/haptics' import {HITSLOP_10, HITSLOP_20} from 'lib/constants' +import {useModalControls} from '#/state/modals' interface PostCtrlsOpts { itemUri: string @@ -51,6 +52,7 @@ interface PostCtrlsOpts { export function PostCtrls(opts: PostCtrlsOpts) { const store = useStores() const theme = useTheme() + const {closeModal} = useModalControls() const defaultCtrlColor = React.useMemo( () => ({ color: theme.palette.default.postCtrl, @@ -58,17 +60,17 @@ export function PostCtrls(opts: PostCtrlsOpts) { [theme], ) as StyleProp<ViewStyle> const onRepost = useCallback(() => { - store.shell.closeModal() + closeModal() if (!opts.isReposted) { Haptics.default() opts.onPressToggleRepost().catch(_e => undefined) } else { opts.onPressToggleRepost().catch(_e => undefined) } - }, [opts, store.shell]) + }, [opts, closeModal]) const onQuote = useCallback(() => { - store.shell.closeModal() + closeModal() store.shell.openComposer({ quote: { uri: opts.itemUri, @@ -86,6 +88,7 @@ export function PostCtrls(opts: PostCtrlsOpts) { opts.itemUri, opts.text, store.shell, + closeModal, ]) const onPressToggleLikeWrapper = async () => { diff --git a/src/view/com/util/post-ctrls/RepostButton.tsx b/src/view/com/util/post-ctrls/RepostButton.tsx index 9c4ed8e5..0a763725 100644 --- a/src/view/com/util/post-ctrls/RepostButton.tsx +++ b/src/view/com/util/post-ctrls/RepostButton.tsx @@ -5,8 +5,8 @@ import {s, colors} from 'lib/styles' import {useTheme} from 'lib/ThemeContext' import {Text} from '../text/Text' import {pluralize} from 'lib/strings/helpers' -import {useStores} from 'state/index' import {HITSLOP_10, HITSLOP_20} from 'lib/constants' +import {useModalControls} from '#/state/modals' interface Props { isReposted: boolean @@ -23,8 +23,8 @@ export const RepostButton = ({ onRepost, onQuote, }: Props) => { - const store = useStores() const theme = useTheme() + const {openModal} = useModalControls() const defaultControlColor = React.useMemo( () => ({ @@ -34,13 +34,13 @@ export const RepostButton = ({ ) const onPressToggleRepostWrapper = useCallback(() => { - store.shell.openModal({ + openModal({ name: 'repost', onRepost: onRepost, onQuote: onQuote, isReposted, }) - }, [onRepost, onQuote, isReposted, store.shell]) + }, [onRepost, onQuote, isReposted, openModal]) return ( <TouchableOpacity diff --git a/src/view/screens/AppPasswords.tsx b/src/view/screens/AppPasswords.tsx index b654055c..338adfba 100644 --- a/src/view/screens/AppPasswords.tsx +++ b/src/view/screens/AppPasswords.tsx @@ -17,6 +17,7 @@ import {useFocusEffect} from '@react-navigation/native' import {ViewHeader} from '../com/util/ViewHeader' import {CenteredView} from 'view/com/util/Views' import {useSetMinimalShellMode} from '#/state/shell' +import {useModalControls} from '#/state/modals' import {useLanguagePrefs} from '#/state/preferences' type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'> @@ -27,6 +28,7 @@ export const AppPasswords = withAuthRequired( const setMinimalShellMode = useSetMinimalShellMode() const {screen} = useAnalytics() const {isTabletOrDesktop} = useWebMediaQueries() + const {openModal} = useModalControls() useFocusEffect( React.useCallback(() => { @@ -36,8 +38,8 @@ export const AppPasswords = withAuthRequired( ) const onAdd = React.useCallback(async () => { - store.shell.openModal({name: 'add-app-password'}) - }, [store]) + openModal({name: 'add-app-password'}) + }, [openModal]) // no app passwords (empty) state if (store.me.appPasswords.length === 0) { @@ -162,10 +164,11 @@ function AppPassword({ }) { const pal = usePalette('default') const store = useStores() + const {openModal} = useModalControls() const {contentLanguages} = useLanguagePrefs() const onDelete = React.useCallback(async () => { - store.shell.openModal({ + openModal({ name: 'confirm', title: 'Delete App Password', message: `Are you sure you want to delete the app password "${name}"?`, @@ -174,7 +177,7 @@ function AppPassword({ Toast.show('App password deleted') }, }) - }, [store, name]) + }, [store, openModal, name]) const primaryLocale = contentLanguages.length > 0 ? contentLanguages[0] : 'en-US' diff --git a/src/view/screens/LanguageSettings.tsx b/src/view/screens/LanguageSettings.tsx index 4cf17894..c94364e9 100644 --- a/src/view/screens/LanguageSettings.tsx +++ b/src/view/screens/LanguageSettings.tsx @@ -2,7 +2,6 @@ import React from 'react' import {StyleSheet, View} from 'react-native' import {observer} from 'mobx-react-lite' import {Text} from '../com/util/text/Text' -import {useStores} from 'state/index' import {s} from 'lib/styles' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' @@ -19,6 +18,7 @@ import {useFocusEffect} from '@react-navigation/native' import {LANGUAGES} from 'lib/../locale/languages' import RNPickerSelect, {PickerSelectProps} from 'react-native-picker-select' import {useSetMinimalShellMode} from '#/state/shell' +import {useModalControls} from '#/state/modals' import {useLanguagePrefs, useSetLanguagePrefs} from '#/state/preferences' type Props = NativeStackScreenProps<CommonNavigatorParams, 'LanguageSettings'> @@ -27,12 +27,12 @@ export const LanguageSettingsScreen = observer(function LanguageSettingsImpl( _: Props, ) { const pal = usePalette('default') - const store = useStores() const langPrefs = useLanguagePrefs() const setLangPrefs = useSetLanguagePrefs() const {isTabletOrDesktop} = useWebMediaQueries() const {screen, track} = useAnalytics() const setMinimalShellMode = useSetMinimalShellMode() + const {openModal} = useModalControls() useFocusEffect( React.useCallback(() => { @@ -43,8 +43,8 @@ export const LanguageSettingsScreen = observer(function LanguageSettingsImpl( const onPressContentLanguages = React.useCallback(() => { track('Settings:ContentlanguagesButtonClicked') - store.shell.openModal({name: 'content-languages-settings'}) - }, [track, store]) + openModal({name: 'content-languages-settings'}) + }, [track, openModal]) const onChangePrimaryLanguage = React.useCallback( (value: Parameters<PickerSelectProps['onValueChange']>[0]) => { diff --git a/src/view/screens/Lists.tsx b/src/view/screens/Lists.tsx index a64b0ca3..a29b0d6c 100644 --- a/src/view/screens/Lists.tsx +++ b/src/view/screens/Lists.tsx @@ -17,6 +17,7 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {SimpleViewHeader} from 'view/com/util/SimpleViewHeader' import {s} from 'lib/styles' import {useSetMinimalShellMode} from '#/state/shell' +import {useModalControls} from '#/state/modals' type Props = NativeStackScreenProps<CommonNavigatorParams, 'Lists'> export const ListsScreen = withAuthRequired( @@ -26,6 +27,7 @@ export const ListsScreen = withAuthRequired( const setMinimalShellMode = useSetMinimalShellMode() const {isMobile} = useWebMediaQueries() const navigation = useNavigation<NavigationProp>() + const {openModal} = useModalControls() const listsLists: ListsListModel = React.useMemo( () => new ListsListModel(store, 'my-curatelists'), @@ -40,7 +42,7 @@ export const ListsScreen = withAuthRequired( ) const onPressNewList = React.useCallback(() => { - store.shell.openModal({ + openModal({ name: 'create-or-edit-list', purpose: 'app.bsky.graph.defs#curatelist', onSave: (uri: string) => { @@ -53,7 +55,7 @@ export const ListsScreen = withAuthRequired( } catch {} }, }) - }, [store, navigation]) + }, [openModal, navigation]) return ( <View style={s.hContentRegion} testID="listsScreen"> diff --git a/src/view/screens/Moderation.tsx b/src/view/screens/Moderation.tsx index 142f3bce..eb952c06 100644 --- a/src/view/screens/Moderation.tsx +++ b/src/view/screens/Moderation.tsx @@ -8,7 +8,6 @@ import { import {observer} from 'mobx-react-lite' import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' import {withAuthRequired} from 'view/com/auth/withAuthRequired' -import {useStores} from 'state/index' import {s} from 'lib/styles' import {CenteredView} from '../com/util/Views' import {ViewHeader} from '../com/util/ViewHeader' @@ -18,15 +17,16 @@ import {usePalette} from 'lib/hooks/usePalette' import {useAnalytics} from 'lib/analytics/analytics' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useSetMinimalShellMode} from '#/state/shell' +import {useModalControls} from '#/state/modals' type Props = NativeStackScreenProps<CommonNavigatorParams, 'Moderation'> export const ModerationScreen = withAuthRequired( observer(function Moderation({}: Props) { const pal = usePalette('default') - const store = useStores() const setMinimalShellMode = useSetMinimalShellMode() const {screen, track} = useAnalytics() const {isTabletOrDesktop} = useWebMediaQueries() + const {openModal} = useModalControls() useFocusEffect( React.useCallback(() => { @@ -37,8 +37,8 @@ export const ModerationScreen = withAuthRequired( const onPressContentFiltering = React.useCallback(() => { track('Moderation:ContentfilteringButtonClicked') - store.shell.openModal({name: 'content-filtering-settings'}) - }, [track, store]) + openModal({name: 'content-filtering-settings'}) + }, [track, openModal]) return ( <CenteredView diff --git a/src/view/screens/ModerationModlists.tsx b/src/view/screens/ModerationModlists.tsx index 8794c6d1..3892e47c 100644 --- a/src/view/screens/ModerationModlists.tsx +++ b/src/view/screens/ModerationModlists.tsx @@ -17,6 +17,7 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {SimpleViewHeader} from 'view/com/util/SimpleViewHeader' import {s} from 'lib/styles' import {useSetMinimalShellMode} from '#/state/shell' +import {useModalControls} from '#/state/modals' type Props = NativeStackScreenProps<CommonNavigatorParams, 'ModerationModlists'> export const ModerationModlistsScreen = withAuthRequired( @@ -26,6 +27,7 @@ export const ModerationModlistsScreen = withAuthRequired( const setMinimalShellMode = useSetMinimalShellMode() const {isMobile} = useWebMediaQueries() const navigation = useNavigation<NavigationProp>() + const {openModal} = useModalControls() const mutelists: ListsListModel = React.useMemo( () => new ListsListModel(store, 'my-modlists'), @@ -40,7 +42,7 @@ export const ModerationModlistsScreen = withAuthRequired( ) const onPressNewList = React.useCallback(() => { - store.shell.openModal({ + openModal({ name: 'create-or-edit-list', purpose: 'app.bsky.graph.defs#modlist', onSave: (uri: string) => { @@ -53,7 +55,7 @@ export const ModerationModlistsScreen = withAuthRequired( } catch {} }, }) - }, [store, navigation]) + }, [openModal, navigation]) return ( <View style={s.hContentRegion} testID="moderationModlistsScreen"> diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx index a4d146d6..3d108164 100644 --- a/src/view/screens/ProfileFeed.tsx +++ b/src/view/screens/ProfileFeed.tsx @@ -47,6 +47,7 @@ import {sanitizeHandle} from 'lib/strings/handles' import {makeProfileLink} from 'lib/routes/links' import {ComposeIcon2} from 'lib/icons' import {logger} from '#/logger' +import {useModalControls} from '#/state/modals' const SECTION_TITLES = ['Posts', 'About'] @@ -137,6 +138,7 @@ export const ProfileFeedScreenInner = observer( route, feedOwnerDid, }: Props & {feedOwnerDid: string}) { + const {openModal} = useModalControls() const pal = usePalette('default') const store = useStores() const {track} = useAnalytics() @@ -210,12 +212,12 @@ export const ProfileFeedScreenInner = observer( const onPressReport = React.useCallback(() => { if (!feedInfo) return - store.shell.openModal({ + openModal({ name: 'report', uri: feedInfo.uri, cid: feedInfo.cid, }) - }, [store, feedInfo]) + }, [openModal, feedInfo]) const onCurrentPageSelected = React.useCallback( (index: number) => { diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx index b84732d5..a165502b 100644 --- a/src/view/screens/ProfileList.tsx +++ b/src/view/screens/ProfileList.tsx @@ -46,6 +46,7 @@ import {ComposeIcon2} from 'lib/icons' import {ListItems} from 'view/com/lists/ListItems' import {logger} from '#/logger' import {useSetMinimalShellMode} from '#/state/shell' +import {useModalControls} from '#/state/modals' const SECTION_TITLES_CURATE = ['Posts', 'About'] const SECTION_TITLES_MOD = ['About'] @@ -110,6 +111,7 @@ export const ProfileListScreenInner = observer( const {rkey} = route.params const feedSectionRef = React.useRef<SectionRef>(null) const aboutSectionRef = React.useRef<SectionRef>(null) + const {openModal} = useModalControls() const list: ListModel = useMemo(() => { const model = new ListModel( @@ -136,7 +138,7 @@ export const ProfileListScreenInner = observer( ) const onPressAddUser = useCallback(() => { - store.shell.openModal({ + openModal({ name: 'list-add-user', list, onAdd() { @@ -145,7 +147,7 @@ export const ProfileListScreenInner = observer( } }, }) - }, [store, list, feed]) + }, [openModal, list, feed]) const onCurrentPageSelected = React.useCallback( (index: number) => { @@ -268,8 +270,8 @@ const Header = observer(function HeaderImpl({ }) { const pal = usePalette('default') const palInverted = usePalette('inverted') - const store = useStores() const navigation = useNavigation<NavigationProp>() + const {openModal, closeModal} = useModalControls() const onTogglePinned = useCallback(async () => { Haptics.default() @@ -280,7 +282,7 @@ const Header = observer(function HeaderImpl({ }, [list]) const onSubscribeMute = useCallback(() => { - store.shell.openModal({ + openModal({ name: 'confirm', title: 'Mute these accounts?', message: @@ -297,10 +299,10 @@ const Header = observer(function HeaderImpl({ } }, onPressCancel() { - store.shell.closeModal() + closeModal() }, }) - }, [store, list]) + }, [openModal, closeModal, list]) const onUnsubscribeMute = useCallback(async () => { try { @@ -314,7 +316,7 @@ const Header = observer(function HeaderImpl({ }, [list]) const onSubscribeBlock = useCallback(() => { - store.shell.openModal({ + openModal({ name: 'confirm', title: 'Block these accounts?', message: @@ -331,10 +333,10 @@ const Header = observer(function HeaderImpl({ } }, onPressCancel() { - store.shell.closeModal() + closeModal() }, }) - }, [store, list]) + }, [openModal, closeModal, list]) const onUnsubscribeBlock = useCallback(async () => { try { @@ -348,17 +350,17 @@ const Header = observer(function HeaderImpl({ }, [list]) const onPressEdit = useCallback(() => { - store.shell.openModal({ + openModal({ name: 'create-or-edit-list', list, onSave() { list.refresh() }, }) - }, [store, list]) + }, [openModal, list]) const onPressDelete = useCallback(() => { - store.shell.openModal({ + openModal({ name: 'confirm', title: 'Delete List', message: 'Are you sure?', @@ -372,16 +374,16 @@ const Header = observer(function HeaderImpl({ } }, }) - }, [store, list, navigation]) + }, [openModal, list, navigation]) const onPressReport = useCallback(() => { if (!list.data) return - store.shell.openModal({ + openModal({ name: 'report', uri: list.uri, cid: list.data.cid, }) - }, [store, list]) + }, [openModal, list]) const onPressShare = useCallback(() => { const url = toShareUrl(`/profile/${list.creatorDid}/lists/${rkey}`) diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index 570f8b7e..f912996e 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings.tsx @@ -46,6 +46,7 @@ import Clipboard from '@react-native-clipboard/clipboard' import {makeProfileLink} from 'lib/routes/links' import {AccountDropdownBtn} from 'view/com/util/AccountDropdownBtn' import {logger} from '#/logger' +import {useModalControls} from '#/state/modals' import { useSetMinimalShellMode, useColorMode, @@ -82,6 +83,7 @@ export const SettingsScreen = withAuthRequired( const [debugHeaderEnabled, toggleDebugHeader] = useDebugHeaderSetting( store.agent, ) + const {openModal} = useModalControls() const primaryBg = useCustomPalette<ViewStyle>({ light: {backgroundColor: colors.blue0}, @@ -117,7 +119,7 @@ export const SettingsScreen = withAuthRequired( const onPressChangeHandle = React.useCallback(() => { track('Settings:ChangeHandleButtonClicked') - store.shell.openModal({ + openModal({ name: 'change-handle', onChanged() { setIsSwitching(true) @@ -135,12 +137,12 @@ export const SettingsScreen = withAuthRequired( ) }, }) - }, [track, store, setIsSwitching]) + }, [track, store, openModal, setIsSwitching]) const onPressInviteCodes = React.useCallback(() => { track('Settings:InvitecodesButtonClicked') - store.shell.openModal({name: 'invite-codes'}) - }, [track, store]) + openModal({name: 'invite-codes'}) + }, [track, openModal]) const onPressLanguageSettings = React.useCallback(() => { navigation.navigate('LanguageSettings') @@ -152,8 +154,8 @@ export const SettingsScreen = withAuthRequired( }, [track, store]) const onPressDeleteAccount = React.useCallback(() => { - store.shell.openModal({name: 'delete-account'}) - }, [store]) + openModal({name: 'delete-account'}) + }, [openModal]) const onPressResetPreferences = React.useCallback(async () => { await store.preferences.reset() @@ -229,8 +231,7 @@ export const SettingsScreen = withAuthRequired( <Text type="lg" style={pal.text}> {store.session.currentSession?.email}{' '} </Text> - <Link - onPress={() => store.shell.openModal({name: 'change-email'})}> + <Link onPress={() => openModal({name: 'change-email'})}> <Text type="lg" style={pal.link}> Change </Text> @@ -240,10 +241,7 @@ export const SettingsScreen = withAuthRequired( <Text type="lg-medium" style={pal.text}> Birthday:{' '} </Text> - <Link - onPress={() => - store.shell.openModal({name: 'birth-date-settings'}) - }> + <Link onPress={() => openModal({name: 'birth-date-settings'})}> <Text type="lg" style={pal.link}> Show </Text> @@ -649,6 +647,7 @@ const EmailConfirmationNotice = observer( const palInverted = usePalette('inverted') const store = useStores() const {isMobile} = useWebMediaQueries() + const {openModal} = useModalControls() if (!store.session.emailNeedsConfirmation) { return null @@ -684,7 +683,7 @@ const EmailConfirmationNotice = observer( accessibilityRole="button" accessibilityLabel="Verify my email" accessibilityHint="" - onPress={() => store.shell.openModal({name: 'verify-email'})}> + onPress={() => openModal({name: 'verify-email'})}> <FontAwesomeIcon icon="envelope" color={palInverted.colors.text} diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx index 7f5e6c5e..c8b3e091 100644 --- a/src/view/shell/Drawer.tsx +++ b/src/view/shell/Drawer.tsx @@ -44,6 +44,7 @@ import {useNavigationTabState} from 'lib/hooks/useNavigationTabState' import {isWeb} from 'platform/detection' import {formatCount, formatCountShortOnly} from 'view/com/util/numeric/format' import {useSetDrawerOpen} from '#/state/shell' +import {useModalControls} from '#/state/modals' export const DrawerContent = observer(function DrawerContentImpl() { const theme = useTheme() @@ -442,11 +443,12 @@ const InviteCodes = observer(function InviteCodesImpl({ const setDrawerOpen = useSetDrawerOpen() const pal = usePalette('default') const {invitesAvailable} = store.me + const {openModal} = useModalControls() const onPress = React.useCallback(() => { track('Menu:ItemClicked', {url: '#invite-codes'}) setDrawerOpen(false) - store.shell.openModal({name: 'invite-codes'}) - }, [store, track, setDrawerOpen]) + openModal({name: 'invite-codes'}) + }, [openModal, track, setDrawerOpen]) return ( <TouchableOpacity testID="menuItemInviteCodes" diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx index d360ceea..fedfcdfc 100644 --- a/src/view/shell/bottom-bar/BottomBar.tsx +++ b/src/view/shell/bottom-bar/BottomBar.tsx @@ -24,12 +24,14 @@ import {styles} from './BottomBarStyles' import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' import {useNavigationTabState} from 'lib/hooks/useNavigationTabState' import {UserAvatar} from 'view/com/util/UserAvatar' +import {useModalControls} from '#/state/modals' type TabOptions = 'Home' | 'Search' | 'Notifications' | 'MyProfile' | 'Feeds' export const BottomBar = observer(function BottomBarImpl({ navigation, }: BottomTabBarProps) { + const {openModal} = useModalControls() const store = useStores() const pal = usePalette('default') const safeAreaInsets = useSafeAreaInsets() @@ -72,8 +74,8 @@ export const BottomBar = observer(function BottomBarImpl({ onPressTab('MyProfile') }, [onPressTab]) const onLongPressProfile = React.useCallback(() => { - store.shell.openModal({name: 'switch-account'}) - }, [store]) + openModal({name: 'switch-account'}) + }, [openModal]) return ( <Animated.View diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx index 84d7d785..a4b3e574 100644 --- a/src/view/shell/desktop/RightNav.tsx +++ b/src/view/shell/desktop/RightNav.tsx @@ -13,6 +13,7 @@ import {useStores} from 'state/index' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {pluralize} from 'lib/strings/helpers' import {formatCount} from 'view/com/util/numeric/format' +import {useModalControls} from '#/state/modals' export const DesktopRightNav = observer(function DesktopRightNavImpl() { const store = useStores() @@ -83,12 +84,13 @@ export const DesktopRightNav = observer(function DesktopRightNavImpl() { const InviteCodes = observer(function InviteCodesImpl() { const store = useStores() const pal = usePalette('default') + const {openModal} = useModalControls() const {invitesAvailable} = store.me const onPress = React.useCallback(() => { - store.shell.openModal({name: 'invite-codes'}) - }, [store]) + openModal({name: 'invite-codes'}) + }, [openModal]) return ( <TouchableOpacity style={[styles.inviteCodes, pal.border]} diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx index 703edf27..498bc11b 100644 --- a/src/view/shell/index.tsx +++ b/src/view/shell/index.tsx @@ -32,12 +32,14 @@ import { useIsDrawerSwipeDisabled, } from '#/state/shell' import {isAndroid} from 'platform/detection' +import {useModalControls} from '#/state/modals' const ShellInner = observer(function ShellInnerImpl() { const store = useStores() 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() @@ -60,13 +62,14 @@ const ShellInner = observer(function ShellInnerImpl() { if (isAndroid) { listener = BackHandler.addEventListener('hardwareBackPress', () => { setIsDrawerOpen(false) + closeModal() return store.shell.closeAnyActiveElement() }) } return () => { listener.remove() } - }, [store, setIsDrawerOpen]) + }, [store, setIsDrawerOpen, closeModal]) return ( <> diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx index 1731ea24..10489489 100644 --- a/src/view/shell/index.web.tsx +++ b/src/view/shell/index.web.tsx @@ -22,11 +22,13 @@ import { useSetDrawerOpen, useOnboardingState, } from '#/state/shell' +import {useModalControls} from '#/state/modals' const ShellInner = observer(function ShellInnerImpl() { const store = useStores() const isDrawerOpen = useIsDrawerOpen() const setDrawerOpen = useSetDrawerOpen() + const {closeModal} = useModalControls() const onboardingState = useOnboardingState() const {isDesktop, isMobile} = useWebMediaQueries() const navigator = useNavigation<NavigationProp>() @@ -35,9 +37,10 @@ const ShellInner = observer(function ShellInnerImpl() { useEffect(() => { navigator.addListener('state', () => { setDrawerOpen(false) + closeModal() store.shell.closeAnyActiveElement() }) - }, [navigator, store.shell, setDrawerOpen]) + }, [navigator, store.shell, setDrawerOpen, closeModal]) const showBottomBar = isMobile && !onboardingState.isActive const showSideNavs =