Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add modal state provider, replace usage except methods #1833

Merged
Merged
31 changes: 17 additions & 14 deletions src/App.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,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'

SplashScreen.preventAutoHideAsync()

Expand All @@ -46,20 +47,22 @@ const App = observer(function AppImpl() {
}
return (
<ShellStateProvider>
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={rootStore.shell.colorMode}>
<RootSiblingParent>
<analytics.Provider>
<RootStoreProvider value={rootStore}>
<GestureHandlerRootView style={s.h100pct}>
<TestCtrls />
<Shell />
</GestureHandlerRootView>
</RootStoreProvider>
</analytics.Provider>
</RootSiblingParent>
</ThemeProvider>
</QueryClientProvider>
<ModalStateProvider>
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={rootStore.shell.colorMode}>
<RootSiblingParent>
<analytics.Provider>
<RootStoreProvider value={rootStore}>
<GestureHandlerRootView style={s.h100pct}>
<TestCtrls />
<Shell />
</GestureHandlerRootView>
</RootStoreProvider>
</analytics.Provider>
</RootSiblingParent>
</ThemeProvider>
</QueryClientProvider>
</ModalStateProvider>
</ShellStateProvider>
)
})
Expand Down
31 changes: 17 additions & 14 deletions src/App.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,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'

const App = observer(function AppImpl() {
const [rootStore, setRootStore] = useState<RootStoreModel | undefined>(
Expand All @@ -36,20 +37,22 @@ const App = observer(function AppImpl() {

return (
<ShellStateProvider>
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={rootStore.shell.colorMode}>
<RootSiblingParent>
<analytics.Provider>
<RootStoreProvider value={rootStore}>
<SafeAreaProvider>
<Shell />
</SafeAreaProvider>
<ToastContainer />
</RootStoreProvider>
</analytics.Provider>
</RootSiblingParent>
</ThemeProvider>
</QueryClientProvider>
<ModalStateProvider>
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={rootStore.shell.colorMode}>
<RootSiblingParent>
<analytics.Provider>
<RootStoreProvider value={rootStore}>
<SafeAreaProvider>
<Shell />
</SafeAreaProvider>
<ToastContainer />
</RootStoreProvider>
</analytics.Provider>
</RootSiblingParent>
</ThemeProvider>
</QueryClientProvider>
</ModalStateProvider>
</ShellStateProvider>
)
})
Expand Down
267 changes: 267 additions & 0 deletions src/state/modals/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
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: () => {},
})

export function Provider({children}: React.PropsWithChildren<{}>) {
const [isModalActive, setIsModalActive] = React.useState(false)
estrattonbailey marked this conversation as resolved.
Show resolved Hide resolved
const [activeModals, setActiveModals] = React.useState<Modal[]>([])

const openModal = React.useCallback(
(modal: Modal) => {
DeviceEventEmitter.emit('navigation')
setActiveModals(activeModals => [...activeModals, modal])
setIsModalActive(true)
},
[setIsModalActive, setActiveModals],
)

const closeModal = React.useCallback(() => {
let totalActiveModals = 0
setActiveModals(activeModals => {
activeModals.pop()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is mutative. We shouldn't mutate inside updaters, which need to be pure functions.

We should enable Strict Mode to catch these better.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah good shout, this should be improved at some point. We should also do the portaling thing we talked about.

This is just a direct port of the existing functionality, so I'd like to just leave as is until we can come back and do it right.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can make this a non-mutation now though right? Not terribly hard

totalActiveModals = activeModals.length
return activeModals
})
setIsModalActive(totalActiveModals > 0)
}, [setIsModalActive, setActiveModals])

return (
<ModalContext.Provider
value={{
estrattonbailey marked this conversation as resolved.
Show resolved Hide resolved
isModalActive,
activeModals,
}}>
<ModalControlContext.Provider
value={{
estrattonbailey marked this conversation as resolved.
Show resolved Hide resolved
openModal,
closeModal,
}}>
{children}
</ModalControlContext.Provider>
</ModalContext.Provider>
)
}

export function useModals() {
return React.useContext(ModalContext)
}

export function useModalControls() {
return React.useContext(ModalControlContext)
}
Loading
Loading