diff --git a/web/account/log-in-form.react.js b/web/account/log-in-form.react.js --- a/web/account/log-in-form.react.js +++ b/web/account/log-in-form.react.js @@ -63,7 +63,7 @@ password, ...extraInfo, }); - modalContext.clearModal(); + modalContext.popModal(); return result; } catch (e) { if (e.message === 'invalid_parameters') { diff --git a/web/app.react.js b/web/app.react.js --- a/web/app.react.js +++ b/web/app.react.js @@ -79,7 +79,7 @@ +activeThreadCurrentlyUnread: boolean, // Redux dispatch functions +dispatch: Dispatch, - +modal: ?React.Node, + +modals: $ReadOnlyArray, }; class App extends React.PureComponent { componentDidMount() { @@ -141,7 +141,7 @@ {content} - {this.props.modal} + {this.props.modals} ); @@ -237,6 +237,13 @@ const dispatch = useDispatch(); const modalContext = useModalContext(); + const modals = React.useMemo( + () => + modalContext.modals.map(([modal, key]) => ( + {modal} + )), + [modalContext.modals], + ); return ( ); }, diff --git a/web/calendar/day.react.js b/web/calendar/day.react.js --- a/web/calendar/day.react.js +++ b/web/calendar/day.react.js @@ -45,7 +45,7 @@ +nextLocalID: number, +timeZone: ?string, +dispatch: Dispatch, - +setModal: (modal: ?React.Node) => void, + +pushModal: (modal: React.Node) => void, }; type State = { +pickerOpen: boolean, @@ -216,7 +216,7 @@ createNewEntry = (threadID: string) => { if (!this.props.loggedIn) { - this.props.setModal(); + this.props.pushModal(); return; } const viewerID = this.props.viewerID; @@ -234,7 +234,7 @@ onHistory = (event: SyntheticEvent) => { event.preventDefault(); - this.props.setModal( + this.props.pushModal( , ); }; @@ -273,7 +273,7 @@ nextLocalID={nextLocalID} timeZone={timeZone} dispatch={dispatch} - setModal={modalContext.setModal} + pushModal={modalContext.pushModal} /> ); }, diff --git a/web/calendar/entry.react.js b/web/calendar/entry.react.js --- a/web/calendar/entry.react.js +++ b/web/calendar/entry.react.js @@ -68,8 +68,8 @@ +createEntry: (info: CreateEntryInfo) => Promise, +saveEntry: (info: SaveEntryInfo) => Promise, +deleteEntry: (info: DeleteEntryInfo) => Promise, - +setModal: (modal: ?React.Node) => void, - +clearModal: () => void, + +pushModal: (modal: React.Node) => void, + +popModal: () => void, }; type State = { +focused: boolean, @@ -259,7 +259,7 @@ onChange: (event: SyntheticEvent) => void = event => { if (!this.props.loggedIn) { - this.props.setModal(); + this.props.pushModal(); return; } const target = event.target; @@ -386,9 +386,9 @@ type: concurrentModificationResetActionType, payload: { id: entryID, dbText: e.payload.db }, }); - this.props.clearModal(); + this.props.popModal(); }; - this.props.setModal( + this.props.pushModal( , ); } @@ -399,7 +399,7 @@ onDelete: (event: SyntheticEvent) => void = event => { event.preventDefault(); if (!this.props.loggedIn) { - this.props.setModal(); + this.props.pushModal(); return; } this.dispatchDelete(this.props.entryInfo.id, true); @@ -440,7 +440,7 @@ onHistory: (event: SyntheticEvent) => void = event => { event.preventDefault(); - this.props.setModal( + this.props.pushModal( ); }, diff --git a/web/calendar/filter-panel.react.js b/web/calendar/filter-panel.react.js --- a/web/calendar/filter-panel.react.js +++ b/web/calendar/filter-panel.react.js @@ -42,7 +42,7 @@ +filteredThreadIDs: ?$ReadOnlySet, +includeDeleted: boolean, +dispatch: Dispatch, - +setModal: (modal: ?React.Node) => void, + +pushModal: (modal: React.Node) => void, }; type State = { +query: string, @@ -202,7 +202,7 @@ } onClickSettings = (threadID: string) => { - this.props.setModal(); + this.props.pushModal(); }; onChangeQuery = (event: SyntheticEvent) => { @@ -377,7 +377,7 @@ filterThreadSearchIndex={filterThreadSearchIndex} includeDeleted={includeDeleted} dispatch={dispatch} - setModal={modalContext.setModal} + pushModal={modalContext.pushModal} /> ); }, diff --git a/web/chat/chat-thread-list-see-more-sidebars.react.js b/web/chat/chat-thread-list-see-more-sidebars.react.js --- a/web/chat/chat-thread-list-see-more-sidebars.react.js +++ b/web/chat/chat-thread-list-see-more-sidebars.react.js @@ -16,11 +16,11 @@ }; function ChatThreadListSeeMoreSidebars(props: Props): React.Node { const { unread, showingSidebarsInline, threadInfo } = props; - const { setModal } = useModalContext(); + const { pushModal } = useModalContext(); const onClick = React.useCallback( - () => setModal(), - [setModal, threadInfo], + () => pushModal(), + [pushModal, threadInfo], ); const buttonText = showingSidebarsInline ? 'See more...' : 'See sidebars...'; return ( diff --git a/web/chat/thread-menu.react.js b/web/chat/thread-menu.react.js --- a/web/chat/thread-menu.react.js +++ b/web/chat/thread-menu.react.js @@ -41,13 +41,13 @@ }; function ThreadMenu(props: ThreadMenuProps): React.Node { - const { setModal, clearModal } = useModalContext(); + const { pushModal, popModal } = useModalContext(); const { threadInfo } = props; const { onPromoteSidebar } = usePromoteSidebar(threadInfo); const onClickSettings = React.useCallback( - () => setModal(), - [setModal, threadInfo.id], + () => pushModal(), + [pushModal, threadInfo.id], ); const settingsItem = React.useMemo(() => { @@ -63,10 +63,10 @@ const onClickMembers = React.useCallback( () => - setModal( - , + pushModal( + , ), - [clearModal, setModal, threadInfo.id], + [popModal, pushModal, threadInfo.id], ); const membersItem = React.useMemo(() => { if (threadInfo.type === threadTypes.PERSONAL) { @@ -93,8 +93,8 @@ }, [childThreads]); const onClickSidebars = React.useCallback( - () => setModal(), - [setModal, threadInfo], + () => pushModal(), + [pushModal, threadInfo], ); const sidebarItem = React.useMemo(() => { @@ -122,10 +122,10 @@ const onClickViewSubchannels = React.useCallback( () => - setModal( - , + pushModal( + , ), - [clearModal, setModal, threadInfo.id], + [popModal, pushModal, threadInfo.id], ); const viewSubchannelsItem = React.useMemo(() => { @@ -163,19 +163,19 @@ leaveThreadActionTypes, callLeaveThread(threadInfo.id), ); - clearModal(); - }, [callLeaveThread, clearModal, dispatchActionPromise, threadInfo.id]); + popModal(); + }, [callLeaveThread, popModal, dispatchActionPromise, threadInfo.id]); const onClickLeaveThread = React.useCallback( () => - setModal( + pushModal( , ), - [clearModal, onConfirmLeaveThread, setModal, threadInfo], + [popModal, onConfirmLeaveThread, pushModal, threadInfo], ); const leaveThreadItem = React.useMemo(() => { diff --git a/web/input/input-state-container.react.js b/web/input/input-state-container.react.js --- a/web/input/input-state-container.react.js +++ b/web/input/input-state-container.react.js @@ -108,7 +108,7 @@ text: string, ) => Promise, +newThread: (request: ClientNewThreadRequest) => Promise, - +setModal: (modal: ?React.Node) => void, + +pushModal: (modal: React.Node) => void, }; type State = { +pendingUploads: { @@ -512,14 +512,14 @@ files: $ReadOnlyArray, ): Promise { const selectionTime = Date.now(); - const { setModal } = this.props; + const { pushModal } = this.props; const appendResults = await Promise.all( files.map(file => this.appendFile(file, selectionTime)), ); if (appendResults.some(({ result }) => !result.success)) { - setModal(); + pushModal(); const time = Date.now() - selectionTime; const reports = []; @@ -1264,7 +1264,7 @@ newThread={callNewThread} dispatch={dispatch} dispatchActionPromise={dispatchActionPromise} - setModal={modalContext.setModal} + pushModal={modalContext.pushModal} /> ); }, diff --git a/web/media/multimedia-modal.react.js b/web/media/multimedia-modal.react.js --- a/web/media/multimedia-modal.react.js +++ b/web/media/multimedia-modal.react.js @@ -13,7 +13,7 @@ type Props = { ...BaseProps, - +clearModal: (modal: ?React.Node) => void, + +popModal: (modal: ?React.Node) => void, }; class MultimediaModal extends React.PureComponent { @@ -35,7 +35,7 @@ > @@ -50,7 +50,7 @@ event: SyntheticEvent, ) => void = event => { if (event.target === this.overlay) { - this.props.clearModal(); + this.props.popModal(); } }; @@ -58,7 +58,7 @@ event: SyntheticKeyboardEvent, ) => void = event => { if (event.keyCode === 27) { - this.props.clearModal(); + this.props.popModal(); } }; } @@ -66,7 +66,7 @@ function ConnectedMultiMediaModal(props: BaseProps): React.Node { const modalContext = useModalContext(); - return ; + return ; } export default ConnectedMultiMediaModal; diff --git a/web/media/multimedia.react.js b/web/media/multimedia.react.js --- a/web/media/multimedia.react.js +++ b/web/media/multimedia.react.js @@ -24,7 +24,7 @@ }; type Props = { ...BaseProps, - +setModal: (modal: ?React.Node) => void, + +pushModal: (modal: React.Node) => void, }; class Multimedia extends React.PureComponent { @@ -44,7 +44,7 @@ render(): React.Node { let progressIndicator, errorIndicator, removeButton; - const { pendingUpload, remove, setModal } = this.props; + const { pendingUpload, remove } = this.props; if (pendingUpload) { const { progressPercent, failed } = pendingUpload; @@ -79,16 +79,15 @@ css.multimediaImage, this.props.multimediaImageCSSClass, ]; - let onClick; - if (setModal) { - imageContainerClasses.push(css.clickable); - onClick = this.onClick; - } + imageContainerClasses.push(css.clickable); const containerClasses = [css.multimedia, this.props.multimediaCSSClass]; return ( - + {removeButton} @@ -111,16 +110,15 @@ onClick: (event: SyntheticEvent) => void = event => { event.stopPropagation(); - const { setModal, uri } = this.props; - invariant(setModal, 'should be set'); - setModal(); + const { pushModal, uri } = this.props; + pushModal(); }; } function ConnectedMultimediaContainer(props: BaseProps): React.Node { const modalContext = useModalContext(); - return ; + return ; } export default ConnectedMultimediaContainer; diff --git a/web/modals/account/log-in-first-modal.react.js b/web/modals/account/log-in-first-modal.react.js --- a/web/modals/account/log-in-first-modal.react.js +++ b/web/modals/account/log-in-first-modal.react.js @@ -13,14 +13,14 @@ type Props = { ...BaseProps, - +setModal: (modal: ?React.Node) => void, - +clearModal: () => void, + +pushModal: (modal: React.Node) => void, + +popModal: () => void, }; class LogInFirstModal extends React.PureComponent { render(): React.Node { return ( - +

{`In order to ${this.props.inOrderTo}, you'll first need to `} @@ -40,7 +40,7 @@ onClickLogIn: (event: SyntheticEvent) => void = event => { event.preventDefault(); - this.props.setModal(); + this.props.pushModal(); }; } @@ -50,8 +50,8 @@ return ( ); } diff --git a/web/modals/account/log-in-modal.react.js b/web/modals/account/log-in-modal.react.js --- a/web/modals/account/log-in-modal.react.js +++ b/web/modals/account/log-in-modal.react.js @@ -9,7 +9,7 @@ function LoginModal(): React.Node { const modalContext = useModalContext(); return ( - + ); diff --git a/web/modals/account/user-settings-modal.react.js b/web/modals/account/user-settings-modal.react.js --- a/web/modals/account/user-settings-modal.react.js +++ b/web/modals/account/user-settings-modal.react.js @@ -70,7 +70,7 @@ ) => Promise, +changeUserPassword: (passwordUpdate: PasswordUpdate) => Promise, +logOut: (preRequestUserState: PreRequestUserState) => Promise, - +clearModal: () => void, + +popModal: () => void, }; type State = { +newPassword: string, @@ -113,7 +113,7 @@ logOut = async () => { await this.props.logOut(this.props.preRequestUserState); - this.props.clearModal(); + this.props.popModal(); }; render() { @@ -204,7 +204,7 @@ } return ( - +

    ); }, diff --git a/web/modals/chat/invalid-upload.react.js b/web/modals/chat/invalid-upload.react.js --- a/web/modals/chat/invalid-upload.react.js +++ b/web/modals/chat/invalid-upload.react.js @@ -8,16 +8,16 @@ import css from './invalid-upload.css'; type Props = { - +clearModal: () => void, + +popModal: () => void, }; class InvalidUploadModal extends React.PureComponent { render(): React.Node { return ( - +

    We don't support that file type yet :(

    )), - [clearModal, listData], + [popModal, listData], ); const viewerID = useSelector( @@ -115,7 +115,7 @@ } return ( - +
    +

    It looks like somebody is attempting to modify that field at the same diff --git a/web/modals/history/history-modal.react.js b/web/modals/history/history-modal.react.js --- a/web/modals/history/history-modal.react.js +++ b/web/modals/history/history-modal.react.js @@ -275,7 +275,7 @@ fetchEntries={callFetchEntries} fetchRevisionsForEntry={callFetchRevisionsForEntry} dispatchActionPromise={dispatchActionPromise} - onClose={modalContext.clearModal} + onClose={modalContext.popModal} /> ); }, diff --git a/web/modals/modal-provider.react.js b/web/modals/modal-provider.react.js --- a/web/modals/modal-provider.react.js +++ b/web/modals/modal-provider.react.js @@ -3,35 +3,51 @@ import invariant from 'invariant'; import * as React from 'react'; +import { getUUID } from 'lib/utils/uuid'; + type Props = { +children: React.Node, }; type ModalContextType = { - +modal: ?React.Node, - +setModal: (?React.Node) => void, - +clearModal: () => void, + +modals: $ReadOnlyArray<[React.Node, string]>, + +pushModal: React.Node => void, + +popModal: () => void, + +clearModals: () => void, }; const ModalContext: React.Context = React.createContext( { - modal: null, - setModal: () => {}, - clearModal: () => {}, + modals: [], + pushModal: () => {}, + popModal: () => {}, + clearModals: () => {}, }, ); function ModalProvider(props: Props): React.Node { const { children } = props; - const [modal, setModal] = React.useState(null); - const clearModal = React.useCallback(() => setModal(null), []); + const [modals, setModals] = React.useState< + $ReadOnlyArray<[React.Node, string]>, + >([]); + const popModal = React.useCallback( + () => setModals(oldModals => oldModals.slice(0, oldModals.length - 1)), + [], + ); + const pushModal = React.useCallback(newModal => { + const key = getUUID(); + setModals(oldModals => [...oldModals, [newModal, key]]); + }, []); + + const clearModals = React.useCallback(() => setModals([]), []); const value = React.useMemo( () => ({ - modal, - setModal, - clearModal, + modals, + pushModal, + popModal, + clearModals, }), - [modal, clearModal], + [modals, pushModal, popModal, clearModals], ); return ( diff --git a/web/modals/threads/subchannels/subchannel.react.js b/web/modals/threads/subchannels/subchannel.react.js --- a/web/modals/threads/subchannels/subchannel.react.js +++ b/web/modals/threads/subchannels/subchannel.react.js @@ -26,16 +26,16 @@ } = chatThreadItem; const timeZone = useSelector(state => state.timeZone); - const { clearModal } = useModalContext(); + const { popModal } = useModalContext(); const navigateToThread = useOnClickThread(threadInfo); const onClickThread = React.useCallback( event => { - clearModal(); + popModal(); navigateToThread(event); }, - [clearModal, navigateToThread], + [popModal, navigateToThread], ); const lastActivity = React.useMemo( diff --git a/web/modals/threads/thread-settings-modal.react.js b/web/modals/threads/thread-settings-modal.react.js --- a/web/modals/threads/thread-settings-modal.react.js +++ b/web/modals/threads/thread-settings-modal.react.js @@ -397,7 +397,7 @@ invariant(threadInfo, 'threadInfo should exist in deleteThreadAction'); try { const response = await callDeleteThread(threadInfo.id, accountPassword); - modalContext.clearModal(); + modalContext.popModal(); return response; } catch (e) { setErrorMessage( @@ -430,7 +430,7 @@ threadID: threadInfo.id, changes: queuedChanges, }); - modalContext.clearModal(); + modalContext.popModal(); return response; } catch (e) { setErrorMessage('unknown_error'); @@ -469,7 +469,7 @@ if (!threadInfo) { return ( - +

    You no longer have permission to view this thread

    @@ -487,7 +487,7 @@ deleteThread={callDeleteThread} changeThreadSettings={callChangeThreadSettings} dispatchActionPromise={dispatchActionPromise} - onClose={modalContext.clearModal} + onClose={modalContext.popModal} errorMessage={errorMessage} setErrorMessage={setErrorMessage} accountPassword={accountPassword} diff --git a/web/settings/account-settings.react.js b/web/settings/account-settings.react.js --- a/web/settings/account-settings.react.js +++ b/web/settings/account-settings.react.js @@ -19,10 +19,10 @@ sendLogoutRequest(preRequestUserState); }, [sendLogoutRequest, preRequestUserState]); - const { setModal } = useModalContext(); + const { pushModal } = useModalContext(); const showPasswordChangeModal = React.useCallback( - () => setModal(), - [setModal], + () => pushModal(), + [pushModal], ); const currentUserInfo = useSelector(state => state.currentUserInfo); diff --git a/web/settings/password-change-modal.js b/web/settings/password-change-modal.js --- a/web/settings/password-change-modal.js +++ b/web/settings/password-change-modal.js @@ -30,7 +30,7 @@ +inputDisabled: boolean, +dispatchActionPromise: DispatchActionPromise, +changeUserPassword: (passwordUpdate: PasswordUpdate) => Promise, - +clearModal: () => void, + +popModal: () => void, }; type State = { +newPassword: string, @@ -74,11 +74,7 @@ const { inputDisabled } = this.props; return ( - +
    @@ -201,7 +197,7 @@ }, currentPassword: this.state.currentPassword, }); - this.props.clearModal(); + this.props.popModal(); } catch (e) { if (e.message === 'invalid_credentials') { this.setState( @@ -256,7 +252,7 @@ inputDisabled={inputDisabled} changeUserPassword={callChangeUserPassword} dispatchActionPromise={dispatchActionPromise} - clearModal={modalContext.clearModal} + popModal={modalContext.popModal} /> ); }, diff --git a/web/sidebar/community-picker.react.js b/web/sidebar/community-picker.react.js --- a/web/sidebar/community-picker.react.js +++ b/web/sidebar/community-picker.react.js @@ -9,11 +9,11 @@ import css from './community-picker.css'; function CommunityPicker(): React.Node { - const { setModal } = useModalContext(); + const { pushModal } = useModalContext(); const setModalToUserSettings = React.useCallback(() => { - setModal(); - }, [setModal]); + pushModal(); + }, [pushModal]); return (