diff --git a/web/app.react.js b/web/app.react.js --- a/web/app.react.js +++ b/web/app.react.js @@ -35,6 +35,7 @@ import Chat from './chat/chat.react'; import InputStateContainer from './input/input-state-container.react'; import LoadingIndicator from './loading-indicator.react'; +import { ModalProvider, useModalContext } from './modals/modal-provider.react'; import DisconnectedBar from './redux/disconnected-bar'; import DisconnectedBarVisibilityHandler from './redux/disconnected-bar-visibility-handler'; import FocusHandler from './redux/focus-handler.react'; @@ -83,15 +84,9 @@ +activeThreadCurrentlyUnread: boolean, // Redux dispatch functions +dispatch: Dispatch, -}; -type State = { +modal: ?React.Node, }; -class App extends React.PureComponent<Props, State> { - state: State = { - modal: null, - }; - +class App extends React.PureComponent<Props> { componentDidMount() { const { navInfo, @@ -136,16 +131,14 @@ if (this.props.loggedIn) { content = this.renderMainContent(); } else { - content = ( - <Splash setModal={this.setModal} currentModal={this.state.modal} /> - ); + content = <Splash />; } return ( <DndProvider backend={HTML5Backend}> <FocusHandler /> <VisibilityHandler /> {content} - {this.state.modal} + {this.props.modal} </DndProvider> ); } @@ -153,11 +146,9 @@ renderMainContent() { let mainContent; if (this.props.navInfo.tab === 'calendar') { - mainContent = ( - <Calendar setModal={this.setModal} url={this.props.location.pathname} /> - ); + mainContent = <Calendar url={this.props.location.pathname} />; } else if (this.props.navInfo.tab === 'chat') { - mainContent = <Chat setModal={this.setModal} />; + mainContent = <Chat />; } return ( @@ -177,23 +168,15 @@ </div> </div> </header> - <InputStateContainer setModal={this.setModal}> + <InputStateContainer> <div className={css['main-content-container']}> <div className={css['main-content']}>{mainContent}</div> </div> </InputStateContainer> - <LeftLayoutAside setModal={this.setModal} /> + <LeftLayoutAside /> </div> ); } - - setModal = (modal: ?React.Node) => { - this.setState({ modal }); - }; - - clearModal() { - this.setModal(null); - } } const fetchEntriesLoadingStatusSelector = createLoadingStatusSelector( @@ -230,6 +213,7 @@ ); const dispatch = useDispatch(); + const modalContext = useModalContext(); return ( <App @@ -240,9 +224,18 @@ mostRecentReadThread={mostRecentReadThread} activeThreadCurrentlyUnread={activeThreadCurrentlyUnread} dispatch={dispatch} + modal={modalContext.modal} /> ); }, ); -export default ConnectedApp; +function AppWithProvider(props: BaseProps): React.Node { + return ( + <ModalProvider> + <ConnectedApp {...props} /> + </ModalProvider> + ); +} + +export default AppWithProvider; diff --git a/web/calendar/calendar.react.js b/web/calendar/calendar.react.js --- a/web/calendar/calendar.react.js +++ b/web/calendar/calendar.react.js @@ -44,7 +44,6 @@ import FilterPanel from './filter-panel.react'; type BaseProps = { - +setModal: (modal: ?React.Node) => void, +url: string, }; type Props = { @@ -149,7 +148,6 @@ <Day dayString={dayString} entryInfos={entries} - setModal={this.props.setModal} key={curDayOfMonth} startingTabIndex={tabIndex} />, @@ -166,7 +164,7 @@ let calendarContentStyle = null; let filterButtonStyle = null; if (this.state.filterPanelOpen) { - filterPanel = <FilterPanel setModal={this.props.setModal} />; + filterPanel = <FilterPanel />; calendarContentStyle = { marginLeft: '300px' }; filterButtonStyle = { backgroundColor: 'rgba(0,0,0,0.67)' }; } 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 @@ -23,6 +23,7 @@ import LogInFirstModal from '../modals/account/log-in-first-modal.react'; import HistoryModal from '../modals/history/history-modal.react'; +import { useModalContext } from '../modals/modal-provider.react'; import { useSelector } from '../redux/redux-utils'; import { htmlTargetFromEvent } from '../vector-utils'; import { AddVector, HistoryVector } from '../vectors.react'; @@ -34,7 +35,6 @@ type BaseProps = { +dayString: string, +entryInfos: $ReadOnlyArray<EntryInfo>, - +setModal: (modal: ?React.Node) => void, +startingTabIndex: number, }; type Props = { @@ -45,6 +45,7 @@ +nextLocalID: number, +timeZone: ?string, +dispatch: Dispatch, + +setModal: (modal: ?React.Node) => void, }; type State = { +pickerOpen: boolean, @@ -107,7 +108,6 @@ <Entry entryInfo={entryInfo} focusOnFirstEntryNewerThan={this.focusOnFirstEntryNewerThan} - setModal={this.props.setModal} tabIndex={this.props.startingTabIndex + i} key={key} innerRef={this.entryRef} @@ -216,12 +216,7 @@ createNewEntry = (threadID: string) => { if (!this.props.loggedIn) { - this.props.setModal( - <LogInFirstModal - inOrderTo="edit this calendar" - setModal={this.props.setModal} - />, - ); + this.props.setModal(<LogInFirstModal inOrderTo="edit this calendar" />); return; } const viewerID = this.props.viewerID; @@ -240,11 +235,7 @@ onHistory = (event: SyntheticEvent<HTMLAnchorElement>) => { event.preventDefault(); this.props.setModal( - <HistoryModal - mode="day" - dayString={this.props.dayString} - onClose={this.clearModal} - />, + <HistoryModal mode="day" dayString={this.props.dayString} />, ); }; @@ -258,10 +249,6 @@ entry.focus(); } }; - - clearModal = () => { - this.props.setModal(null); - }; } const ConnectedDay: React.ComponentType<BaseProps> = React.memo<BaseProps>( @@ -275,6 +262,7 @@ const nextLocalID = useSelector(state => state.nextLocalID); const timeZone = useSelector(state => state.timeZone); const dispatch = useDispatch(); + const modalContext = useModalContext(); return ( <Day @@ -285,6 +273,7 @@ nextLocalID={nextLocalID} timeZone={timeZone} dispatch={dispatch} + setModal={modalContext.setModal} /> ); }, 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 @@ -45,6 +45,7 @@ import LogInFirstModal from '../modals/account/log-in-first-modal.react'; import ConcurrentModificationModal from '../modals/concurrent-modification-modal.react'; import HistoryModal from '../modals/history/history-modal.react'; +import { useModalContext } from '../modals/modal-provider.react'; import { useSelector } from '../redux/redux-utils'; import { nonThreadCalendarQuery } from '../selectors/nav-selectors'; import { HistoryVector, DeleteVector } from '../vectors.react'; @@ -54,7 +55,6 @@ +innerRef: (key: string, me: Entry) => void, +entryInfo: EntryInfo, +focusOnFirstEntryNewerThan: (time: number) => void, - +setModal: (modal: ?React.Node) => void, +tabIndex: number, }; type Props = { @@ -68,6 +68,8 @@ +createEntry: (info: CreateEntryInfo) => Promise<CreateEntryPayload>, +saveEntry: (info: SaveEntryInfo) => Promise<SaveEntryResult>, +deleteEntry: (info: DeleteEntryInfo) => Promise<DeleteEntryResult>, + +setModal: (modal: ?React.Node) => void, + +clearModal: () => void, }; type State = { +focused: boolean, @@ -257,12 +259,7 @@ onChange: (event: SyntheticEvent<HTMLTextAreaElement>) => void = event => { if (!this.props.loggedIn) { - this.props.setModal( - <LogInFirstModal - inOrderTo="edit this calendar" - setModal={this.props.setModal} - />, - ); + this.props.setModal(<LogInFirstModal inOrderTo="edit this calendar" />); return; } const target = event.target; @@ -389,13 +386,10 @@ type: concurrentModificationResetActionType, payload: { id: entryID, dbText: e.payload.db }, }); - this.clearModal(); + this.props.clearModal(); }; this.props.setModal( - <ConcurrentModificationModal - onClose={this.clearModal} - onRefresh={onRefresh} - />, + <ConcurrentModificationModal onRefresh={onRefresh} />, ); } throw e; @@ -405,12 +399,7 @@ onDelete: (event: SyntheticEvent<HTMLAnchorElement>) => void = event => { event.preventDefault(); if (!this.props.loggedIn) { - this.props.setModal( - <LogInFirstModal - inOrderTo="edit this calendar" - setModal={this.props.setModal} - />, - ); + this.props.setModal(<LogInFirstModal inOrderTo="edit this calendar" />); return; } this.dispatchDelete(this.props.entryInfo.id, true); @@ -459,15 +448,10 @@ this.props.entryInfo.month, this.props.entryInfo.day, )} - onClose={this.clearModal} currentEntryID={this.props.entryInfo.id} />, ); }; - - clearModal: () => void = () => { - this.props.setModal(null); - }; } export type InnerEntry = Entry; @@ -492,6 +476,8 @@ const dispatchActionPromise = useDispatchActionPromise(); const dispatch = useDispatch(); + const modalContext = useModalContext(); + return ( <Entry {...props} @@ -504,6 +490,8 @@ deleteEntry={callDeleteEntry} dispatchActionPromise={dispatchActionPromise} dispatch={dispatch} + setModal={modalContext.setModal} + clearModal={modalContext.clearModal} /> ); }, 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 @@ -26,6 +26,7 @@ } from 'lib/types/filter-types'; import type { Dispatch } from 'lib/types/redux-types'; +import { useModalContext } from '../modals/modal-provider.react'; import ThreadSettingsModal from '../modals/threads/thread-settings-modal.react'; import { useSelector } from '../redux/redux-utils'; import { @@ -35,16 +36,13 @@ import { MagnifyingGlass } from '../vectors.react'; import css from './filter-panel.css'; -type BaseProps = { - +setModal: (modal: ?React.Node) => void, -}; type Props = { - ...BaseProps, +filterThreadInfos: () => $ReadOnlyArray<FilterThreadInfo>, +filterThreadSearchIndex: () => SearchIndex, +filteredThreadIDs: ?$ReadOnlySet<string>, +includeDeleted: boolean, +dispatch: Dispatch, + +setModal: (modal: ?React.Node) => void, }; type State = { +query: string, @@ -204,9 +202,7 @@ } onClickSettings = (threadID: string) => { - this.props.setModal( - <ThreadSettingsModal threadID={threadID} onClose={this.clearModal} />, - ); + this.props.setModal(<ThreadSettingsModal threadID={threadID} />); }; onChangeQuery = (event: SyntheticEvent<HTMLInputElement>) => { @@ -238,10 +234,6 @@ }, }); }; - - clearModal = () => { - this.props.setModal(null); - }; } type ItemProps = { @@ -369,22 +361,23 @@ }; } -const ConnectedFilterPanel: React.ComponentType<BaseProps> = React.memo<BaseProps>( - function ConnectedFilterPanel(props) { +const ConnectedFilterPanel: React.ComponentType<{}> = React.memo<{}>( + function ConnectedFilterPanel(): React.Node { const filteredThreadIDs = useSelector(filteredThreadIDsSelector); const filterThreadInfos = useSelector(webFilterThreadInfos); const filterThreadSearchIndex = useSelector(webFilterThreadSearchIndex); const includeDeleted = useSelector(includeDeletedSelector); const dispatch = useDispatch(); + const modalContext = useModalContext(); return ( <FilterPanel - {...props} filteredThreadIDs={filteredThreadIDs} filterThreadInfos={filterThreadInfos} filterThreadSearchIndex={filterThreadSearchIndex} includeDeleted={includeDeleted} dispatch={dispatch} + setModal={modalContext.setModal} /> ); }, diff --git a/web/chat/chat-message-list.react.js b/web/chat/chat-message-list.react.js --- a/web/chat/chat-message-list.react.js +++ b/web/chat/chat-message-list.react.js @@ -47,11 +47,7 @@ import RelationshipPrompt from './relationship-prompt/relationship-prompt'; import ThreadTopBar from './thread-top-bar.react'; -type BaseProps = { - +setModal: (modal: ?React.Node) => void, -}; type PassedProps = { - ...BaseProps, // Redux state +activeChatThreadID: ?string, +threadInfo: ?ThreadInfo, @@ -191,7 +187,7 @@ </div> ); } - const { threadInfo, setModal } = this.props; + const { threadInfo } = this.props; invariant(threadInfo, 'ThreadInfo should be set if messageListData is'); return ( <Message @@ -199,7 +195,6 @@ threadInfo={threadInfo} setMouseOverMessagePosition={this.setMouseOverMessagePosition} mouseOverMessagePosition={this.state.mouseOverMessagePosition} - setModal={setModal} timeZone={this.props.timeZone} key={ChatMessageList.keyExtractor(item)} /> @@ -383,9 +378,8 @@ registerFetchKey(fetchMessagesBeforeCursorActionTypes); registerFetchKey(fetchMostRecentMessagesActionTypes); - -const ConnectedChatMessageList: React.ComponentType<BaseProps> = React.memo<BaseProps>( - function ConnectedChatMessageList(props) { +const ConnectedChatMessageList: React.ComponentType<{}> = React.memo<{}>( + function ConnectedChatMessageList(): React.Node { const userAgent = useSelector(state => state.userAgent); const supportsReverseFlex = React.useMemo(() => { const browser = detectBrowser(userAgent); @@ -475,7 +469,6 @@ return ( <MessageListContext.Provider value={messageListContext}> <ChatMessageList - {...props} activeChatThreadID={activeChatThreadID} threadInfo={threadInfo} messageListData={messageListData} diff --git a/web/chat/chat-tabs.react.js b/web/chat/chat-tabs.react.js --- a/web/chat/chat-tabs.react.js +++ b/web/chat/chat-tabs.react.js @@ -11,10 +11,7 @@ import ChatThreadTab from './chat-thread-tab.react'; import { ThreadListContext } from './thread-list-provider'; -type Props = { - +setModal: (modal: ?React.Node) => void, -}; -function ChatTabs(props: Props): React.Node { +function ChatTabs(): React.Node { let backgroundTitle = 'Background'; const unreadBackgroundCountVal = useSelector(unreadBackgroundCount); if (unreadBackgroundCountVal) { @@ -52,7 +49,7 @@ /> </div> <div className={css.threadList}> - <ChatThreadList setModal={props.setModal} /> + <ChatThreadList /> </div> </div> ); diff --git a/web/chat/chat-thread-list-item.react.js b/web/chat/chat-thread-list-item.react.js --- a/web/chat/chat-thread-list-item.react.js +++ b/web/chat/chat-thread-list-item.react.js @@ -21,10 +21,9 @@ type Props = { +item: ChatThreadItem, - +setModal: (modal: ?React.Node) => void, }; function ChatThreadListItem(props: Props): React.Node { - const { item, setModal } = props; + const { item } = props; const { threadInfo, lastUpdatedTimeIncludingSidebars, @@ -105,7 +104,6 @@ threadInfo={item.threadInfo} unread={sidebarItem.unread} showingSidebarsInline={sidebarItem.showingSidebarsInline} - setModal={setModal} key="seeMore" /> ); 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 @@ -6,21 +6,20 @@ import type { ThreadInfo } from 'lib/types/thread-types'; import SidebarListModal from '../modals/chat/sidebar-list-modal.react'; +import { useModalContext } from '../modals/modal-provider.react'; import css from './chat-thread-list.css'; type Props = { +threadInfo: ThreadInfo, +unread: boolean, +showingSidebarsInline: boolean, - +setModal: (modal: ?React.Node) => void, }; function ChatThreadListSeeMoreSidebars(props: Props): React.Node { - const { unread, showingSidebarsInline, setModal, threadInfo } = props; + const { unread, showingSidebarsInline, threadInfo } = props; + const { setModal } = useModalContext(); + const onClick = React.useCallback( - () => - setModal( - <SidebarListModal setModal={setModal} threadInfo={threadInfo} />, - ), + () => setModal(<SidebarListModal threadInfo={threadInfo} />), [setModal, threadInfo], ); const buttonText = showingSidebarsInline ? 'See more...' : 'See sidebars...'; diff --git a/web/chat/chat-thread-list.react.js b/web/chat/chat-thread-list.react.js --- a/web/chat/chat-thread-list.react.js +++ b/web/chat/chat-thread-list.react.js @@ -10,12 +10,7 @@ import { ThreadListContext } from './thread-list-provider'; import ThreadListSearch from './thread-list-search.react'; -type Props = { - +setModal: (modal: ?React.Node) => void, -}; - -function ChatThreadList(props: Props): React.Node { - const { setModal } = props; +function ChatThreadList(): React.Node { const threadListContext = React.useContext(ThreadListContext); invariant( threadListContext, @@ -31,17 +26,13 @@ const threadComponents: React.Node[] = React.useMemo(() => { const threads = threadList.map(item => ( - <ChatThreadListItem - item={item} - key={item.threadInfo.id} - setModal={setModal} - /> + <ChatThreadListItem item={item} key={item.threadInfo.id} /> )); if (threads.length === 0 && isBackground) { threads.push(<EmptyItem key="emptyItem" />); } return threads; - }, [threadList, isBackground, setModal]); + }, [threadList, isBackground]); return ( <div className={css.threadListContainer}> diff --git a/web/chat/chat.react.js b/web/chat/chat.react.js --- a/web/chat/chat.react.js +++ b/web/chat/chat.react.js @@ -6,16 +6,13 @@ import ChatTabs from './chat-tabs.react'; import { ThreadListProvider } from './thread-list-provider'; -type Props = { - +setModal: (modal: ?React.Node) => void, -}; -function Chat(props: Props): React.Node { +function Chat(): React.Node { return ( <> <ThreadListProvider> - <ChatTabs setModal={props.setModal} /> + <ChatTabs /> </ThreadListProvider> - <ChatMessageList setModal={props.setModal} /> + <ChatMessageList /> </> ); } diff --git a/web/chat/message.react.js b/web/chat/message.react.js --- a/web/chat/message.react.js +++ b/web/chat/message.react.js @@ -24,7 +24,6 @@ messagePositionInfo: MessagePositionInfo, ) => void, +mouseOverMessagePosition: ?OnMessagePositionWithContainerInfo, - +setModal: (modal: ?React.Node) => void, +timeZone: ?string, }; function Message(props: Props): React.Node { @@ -58,7 +57,6 @@ threadInfo={props.threadInfo} setMouseOverMessagePosition={props.setMouseOverMessagePosition} mouseOverMessagePosition={props.mouseOverMessagePosition} - setModal={props.setModal} /> ); } else { diff --git a/web/chat/multimedia-message.react.js b/web/chat/multimedia-message.react.js --- a/web/chat/multimedia-message.react.js +++ b/web/chat/multimedia-message.react.js @@ -24,7 +24,6 @@ messagePositionInfo: MessagePositionInfo, ) => void, +mouseOverMessagePosition: ?OnMessagePositionWithContainerInfo, - +setModal: (modal: ?React.Node) => void, }; type Props = { ...BaseProps, @@ -33,7 +32,7 @@ }; class MultimediaMessage extends React.PureComponent<Props> { render() { - const { item, setModal, inputState } = this.props; + const { item, inputState } = this.props; invariant( item.messageInfo.type === messageTypes.IMAGES || item.messageInfo.type === messageTypes.MULTIMEDIA, @@ -52,7 +51,6 @@ <Multimedia uri={singleMedia.uri} pendingUpload={pendingUpload} - setModal={setModal} multimediaCSSClass={css.multimedia} multimediaImageCSSClass={css.multimediaImage} key={singleMedia.id} 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 @@ -73,13 +73,13 @@ import { validateFile, preloadImage } from '../media/media-utils'; import InvalidUploadModal from '../modals/chat/invalid-upload.react'; +import { useModalContext } from '../modals/modal-provider.react'; import { useSelector } from '../redux/redux-utils'; import { nonThreadCalendarQuery } from '../selectors/nav-selectors'; import { type PendingMultimediaUpload, InputStateContext } from './input-state'; type BaseProps = { +children: React.Node, - +setModal: (modal: ?React.Node) => void, }; type Props = { ...BaseProps, @@ -108,6 +108,7 @@ text: string, ) => Promise<SendMessageResult>, +newThread: (request: ClientNewThreadRequest) => Promise<NewThreadResult>, + +setModal: (modal: ?React.Node) => void, }; type State = { +pendingUploads: { @@ -518,7 +519,7 @@ ); if (appendResults.some(({ result }) => !result.success)) { - setModal(<InvalidUploadModal setModal={setModal} />); + setModal(<InvalidUploadModal />); const time = Date.now() - selectionTime; const reports = []; @@ -1245,6 +1246,7 @@ const callNewThread = useServerCall(newThread); const dispatch = useDispatch(); const dispatchActionPromise = useDispatchActionPromise(); + const modalContext = useModalContext(); return ( <InputStateContainer @@ -1262,6 +1264,7 @@ newThread={callNewThread} dispatch={dispatch} dispatchActionPromise={dispatchActionPromise} + setModal={modalContext.setModal} /> ); }, 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 @@ -4,12 +4,18 @@ import * as React from 'react'; import { XCircle as XCircleIcon } from 'react-feather'; +import { useModalContext } from '../modals/modal-provider.react'; import css from './media.css'; -type Props = { +type BaseProps = { +uri: string, - +setModal: (modal: ?React.Node) => void, }; + +type Props = { + ...BaseProps, + +clearModal: (modal: ?React.Node) => void, +}; + class MultimediaModal extends React.PureComponent<Props> { overlay: ?HTMLDivElement; @@ -29,7 +35,7 @@ > <img src={this.props.uri} /> <XCircleIcon - onClick={this.close} + onClick={this.props.clearModal} className={css.closeMultimediaModal} /> </div> @@ -44,7 +50,7 @@ event: SyntheticEvent<HTMLDivElement>, ) => void = event => { if (event.target === this.overlay) { - this.close(); + this.props.clearModal(); } }; @@ -52,13 +58,15 @@ event: SyntheticKeyboardEvent<HTMLDivElement>, ) => void = event => { if (event.keyCode === 27) { - this.close(); + this.props.clearModal(); } }; +} - close: () => void = () => { - this.props.setModal(null); - }; +function ConnectedMultiMediaModal(props: BaseProps): React.Node { + const modalContext = useModalContext(); + + return <MultimediaModal {...props} clearModal={modalContext.clearModal} />; } -export default MultimediaModal; +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 @@ -11,17 +11,22 @@ } from 'react-feather'; import { type PendingMultimediaUpload } from '../input/input-state'; +import { useModalContext } from '../modals/modal-provider.react'; import css from './media.css'; import MultimediaModal from './multimedia-modal.react'; +type BaseProps = { + +uri: string, + +pendingUpload?: ?PendingMultimediaUpload, + +remove?: (uploadID: string) => void, + +multimediaCSSClass: string, + +multimediaImageCSSClass: string, +}; type Props = { - uri: string, - pendingUpload?: ?PendingMultimediaUpload, - remove?: (uploadID: string) => void, - setModal?: (modal: ?React.Node) => void, - multimediaCSSClass: string, - multimediaImageCSSClass: string, + ...BaseProps, + +setModal: (modal: ?React.Node) => void, }; + class Multimedia extends React.PureComponent<Props> { componentDidUpdate(prevProps: Props) { const { uri, pendingUpload } = this.props; @@ -108,8 +113,14 @@ const { setModal, uri } = this.props; invariant(setModal, 'should be set'); - setModal(<MultimediaModal uri={uri} setModal={setModal} />); + setModal(<MultimediaModal uri={uri} />); }; } -export default Multimedia; +function ConnectedMultimediaContainer(props: BaseProps): React.Node { + const modalContext = useModalContext(); + + return <Multimedia {...props} setModal={modalContext.setModal} />; +} + +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 @@ -2,18 +2,25 @@ import * as React from 'react'; +import { useModalContext } from '../../modals/modal-provider.react'; import css from '../../style.css'; import Modal from '../modal.react'; import LogInModal from './log-in-modal.react'; -type Props = { +type BaseProps = { +inOrderTo: string, +}; + +type Props = { + ...BaseProps, +setModal: (modal: ?React.Node) => void, + +clearModal: () => void, }; + class LogInFirstModal extends React.PureComponent<Props> { render(): React.Node { return ( - <Modal name="Log in" onClose={this.clearModal}> + <Modal name="Log in" onClose={this.props.clearModal}> <div className={css['modal-body']}> <p> {`In order to ${this.props.inOrderTo}, you'll first need to `} @@ -31,14 +38,22 @@ ); } - clearModal: () => void = () => { - this.props.setModal(null); - }; - onClickLogIn: (event: SyntheticEvent<HTMLAnchorElement>) => void = event => { event.preventDefault(); - this.props.setModal(<LogInModal setModal={this.props.setModal} />); + this.props.setModal(<LogInModal />); }; } -export default LogInFirstModal; +function ConnectedLoginFirstModal(props: BaseProps): React.Node { + const modalContext = useModalContext(); + + return ( + <LogInFirstModal + {...props} + setModal={modalContext.setModal} + clearModal={modalContext.clearModal} + /> + ); +} + +export default ConnectedLoginFirstModal; 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 @@ -25,18 +25,16 @@ import { useSelector } from '../../redux/redux-utils'; import { webLogInExtraInfoSelector } from '../../selectors/account-selectors'; import Input from '../input.react'; +import { useModalContext } from '../modal-provider.react'; import Modal from '../modal.react'; import css from './user-settings-modal.css'; -type BaseProps = { - +setModal: (modal: ?React.Node) => void, -}; type Props = { - ...BaseProps, +inputDisabled: boolean, +logInExtraInfo: () => LogInExtraInfo, +dispatchActionPromise: DispatchActionPromise, +logIn: (logInInfo: LogInInfo) => Promise<LogInResult>, + +clearModal: () => void, }; type State = { +username: string, @@ -63,7 +61,7 @@ render() { return ( - <Modal name="Log in" onClose={this.clearModal}> + <Modal name="Log in" onClose={this.props.clearModal}> <div className={css['modal-body']}> <form method="POST"> <div> @@ -175,7 +173,7 @@ password: this.state.password, ...extraInfo, }); - this.clearModal(); + this.props.clearModal(); return result; } catch (e) { if (e.message === 'invalid_parameters') { @@ -216,28 +214,24 @@ throw e; } } - - clearModal = () => { - this.props.setModal(null); - }; } const loadingStatusSelector = createLoadingStatusSelector(logInActionTypes); - -const ConnectedLoginModal: React.ComponentType<BaseProps> = React.memo<BaseProps>( - function ConnectedLoginModal(props) { +const ConnectedLoginModal: React.ComponentType<{}> = React.memo<{}>( + function ConnectedLoginModal(): React.Node { const inputDisabled = useSelector(loadingStatusSelector) === 'loading'; const loginExtraInfo = useSelector(webLogInExtraInfoSelector); const callLogIn = useServerCall(logIn); const dispatchActionPromise = useDispatchActionPromise(); + const modalContext = useModalContext(); return ( <LogInModal - {...props} inputDisabled={inputDisabled} logInExtraInfo={loginExtraInfo} logIn={callLogIn} dispatchActionPromise={dispatchActionPromise} + clearModal={modalContext.clearModal} /> ); }, 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 @@ -29,6 +29,7 @@ import Button from '../../components/button.react'; import { useSelector } from '../../redux/redux-utils'; import Input from '../input.react'; +import { useModalContext } from '../modal-provider.react'; import Modal from '../modal.react'; import css from './user-settings-modal.css'; @@ -58,11 +59,7 @@ }; } -type BaseProps = { - +setModal: (modal: ?React.Node) => void, -}; type Props = { - ...BaseProps, +currentUserInfo: ?CurrentUserInfo, +preRequestUserState: PreRequestUserState, +inputDisabled: boolean, @@ -73,6 +70,7 @@ ) => Promise<LogOutResult>, +changeUserPassword: (passwordUpdate: PasswordUpdate) => Promise<void>, +logOut: (preRequestUserState: PreRequestUserState) => Promise<LogOutResult>, + +clearModal: () => void, }; type State = { +newPassword: string, @@ -115,7 +113,7 @@ logOut = async () => { await this.props.logOut(this.props.preRequestUserState); - this.clearModal(); + this.props.clearModal(); }; render() { @@ -206,7 +204,7 @@ } return ( - <Modal name="Edit account" onClose={this.clearModal} size="large"> + <Modal name="Edit account" onClose={this.props.clearModal} size="large"> <ul className={css['tab-panel']}> <Tab name="General" @@ -328,7 +326,7 @@ }, currentPassword: this.state.currentPassword, }); - this.clearModal(); + this.props.clearModal(); } catch (e) { if (e.message === 'invalid_credentials') { this.setState( @@ -377,7 +375,7 @@ this.state.currentPassword, this.props.preRequestUserState, ); - this.clearModal(); + this.props.clearModal(); return response; } catch (e) { const errorMessage = @@ -400,10 +398,6 @@ throw e; } } - - clearModal = () => { - this.props.setModal(null); - }; } const deleteAccountLoadingStatusSelector = createLoadingStatusSelector( @@ -412,9 +406,8 @@ const changeUserPasswordLoadingStatusSelector = createLoadingStatusSelector( changeUserPasswordActionTypes, ); - -const ConnectedUserSettingsModal: React.ComponentType<BaseProps> = React.memo<BaseProps>( - function ConnectedUserSettingsModal(props) { +const ConnectedUserSettingsModal: React.ComponentType<{}> = React.memo<{}>( + function ConnectedUserSettingsModal(): React.Node { const currentUserInfo = useSelector(state => state.currentUserInfo); const preRequestUserState = useSelector(preRequestUserStateSelector); const inputDisabled = useSelector( @@ -427,9 +420,10 @@ const dispatchActionPromise = useDispatchActionPromise(); const boundLogOut = useServerCall(logOut); + const modalContext = useModalContext(); + return ( <UserSettingsModal - {...props} currentUserInfo={currentUserInfo} preRequestUserState={preRequestUserState} inputDisabled={inputDisabled} @@ -437,6 +431,7 @@ changeUserPassword={callChangeUserPassword} dispatchActionPromise={dispatchActionPromise} logOut={boundLogOut} + clearModal={modalContext.clearModal} /> ); }, 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 @@ -2,26 +2,29 @@ import * as React from 'react'; +import { useModalContext } from '../../modals/modal-provider.react'; import css from '../../style.css'; import Modal from '../modal.react'; type Props = { - +setModal: (modal: ?React.Node) => void, + +clearModal: () => void, }; class InvalidUploadModal extends React.PureComponent<Props> { render(): React.Node { return ( - <Modal name="Invalid upload" onClose={this.clearModal}> + <Modal name="Invalid upload" onClose={this.props.clearModal}> <div className={css['modal-body']}> <p>We don't support that file type yet :(</p> </div> </Modal> ); } +} + +function ConnectedInvalidUploadModal(): React.Node { + const modalContext = useModalContext(); - clearModal: () => void = () => { - this.props.setModal(null); - }; + return <InvalidUploadModal clearModal={modalContext.clearModal} />; } -export default InvalidUploadModal; +export default ConnectedInvalidUploadModal; diff --git a/web/modals/chat/sidebar-list-modal.react.js b/web/modals/chat/sidebar-list-modal.react.js --- a/web/modals/chat/sidebar-list-modal.react.js +++ b/web/modals/chat/sidebar-list-modal.react.js @@ -15,22 +15,20 @@ import { useSelector } from '../../redux/redux-utils'; import globalCSS from '../../style.css'; import { MagnifyingGlass } from '../../vectors.react'; +import { useModalContext } from '../modal-provider.react'; import Modal from '../modal.react'; type Props = { - +setModal: (modal: ?React.Node) => void, +threadInfo: ThreadInfo, }; + function SidebarListModal(props: Props): React.Node { - const { setModal, threadInfo } = props; + const { threadInfo } = props; const [searchState, setSearchState] = React.useState({ text: '', results: new Set<string>(), }); - - const clearModal = React.useCallback(() => { - setModal(null); - }, [setModal]); + const { clearModal } = useModalContext(); const sidebarInfos = useSelector( state => sidebarInfoSelector(state)[threadInfo.id] ?? [], diff --git a/web/modals/concurrent-modification-modal.react.js b/web/modals/concurrent-modification-modal.react.js --- a/web/modals/concurrent-modification-modal.react.js +++ b/web/modals/concurrent-modification-modal.react.js @@ -3,16 +3,18 @@ import * as React from 'react'; import css from '../style.css'; +import { useModalContext } from './modal-provider.react'; import Modal from './modal.react'; type Props = { +onRefresh: () => void, - +onClose: () => void, }; export default function ConcurrentModificationModal(props: Props): React.Node { + const modalContext = useModalContext(); + return ( - <Modal name="Concurrent modification" onClose={props.onClose}> + <Modal name="Concurrent modification" onClose={modalContext.clearModal}> <div className={css['modal-body']}> <p> 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 @@ -33,6 +33,7 @@ import { dateFromString } from 'lib/utils/date-utils'; import LoadingIndicator from '../../loading-indicator.react'; +import { useModalContext } from '../../modals/modal-provider.react'; import { useSelector } from '../../redux/redux-utils'; import { allDaysToEntries } from '../../selectors/entry-selectors'; import Modal from '../modal.react'; @@ -43,7 +44,6 @@ type BaseProps = { +mode: HistoryMode, +dayString: string, - +onClose: () => void, +currentEntryID?: ?string, }; type Props = { @@ -59,6 +59,7 @@ +fetchRevisionsForEntry: ( entryID: string, ) => Promise<$ReadOnlyArray<HistoryRevisionInfo>>, + +onClose: () => void, }; type State = { +mode: HistoryMode, @@ -262,6 +263,7 @@ const callFetchEntries = useServerCall(fetchEntries); const callFetchRevisionsForEntry = useServerCall(fetchRevisionsForEntry); const dispatchActionPromise = useDispatchActionPromise(); + const modalContext = useModalContext(); return ( <HistoryModal @@ -273,6 +275,7 @@ fetchEntries={callFetchEntries} fetchRevisionsForEntry={callFetchRevisionsForEntry} dispatchActionPromise={dispatchActionPromise} + onClose={modalContext.clearModal} /> ); }, 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 @@ -36,6 +36,7 @@ } from 'lib/utils/action-utils'; import { firstLine } from 'lib/utils/string-utils'; +import { useModalContext } from '../../modals/modal-provider.react'; import { useSelector } from '../../redux/redux-utils'; import css from '../../style.css'; import Modal from '../modal.react'; @@ -71,7 +72,6 @@ type BaseProps = { +threadID: string, - +onClose: () => void, }; type Props = { ...BaseProps, @@ -87,6 +87,7 @@ +changeThreadSettings: ( update: UpdateThreadRequest, ) => Promise<ChangeThreadSettingsPayload>, + +onClose: () => void, }; type State = { +queuedChanges: ThreadChanges, @@ -564,9 +565,11 @@ const threadInfo: ?ThreadInfo = useSelector( state => threadInfoSelector(state)[props.threadID], ); + const modalContext = useModalContext(); + if (!threadInfo) { return ( - <Modal onClose={props.onClose} name="Invalid thread"> + <Modal onClose={modalContext.clearModal} name="Invalid thread"> <div className={css['modal-body']}> <p>You no longer have permission to view this thread</p> </div> @@ -584,6 +587,7 @@ deleteThread={callDeleteThread} changeThreadSettings={callChangeThreadSettings} dispatchActionPromise={dispatchActionPromise} + onClose={modalContext.clearModal} /> ); }, 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 @@ -4,16 +4,15 @@ import Button from '../components/button.react'; import UserSettingsModal from '../modals/account/user-settings-modal.react.js'; +import { useModalContext } from '../modals/modal-provider.react'; import SWMansionIcon from '../SWMansionIcon.react'; import css from './community-picker.css'; -type Props = { +setModal: (modal: ?React.Node) => void }; - -function CommunityPicker(props: Props): React.Node { - const { setModal } = props; +function CommunityPicker(): React.Node { + const { setModal } = useModalContext(); const setModalToUserSettings = React.useCallback(() => { - setModal(<UserSettingsModal setModal={setModal} />); + setModal(<UserSettingsModal />); }, [setModal]); return ( diff --git a/web/sidebar/left-layout-aside.react.js b/web/sidebar/left-layout-aside.react.js --- a/web/sidebar/left-layout-aside.react.js +++ b/web/sidebar/left-layout-aside.react.js @@ -5,15 +5,11 @@ import AppSwitcher from './app-switcher.react'; import CommunityPicker from './community-picker.react'; import css from './left-layout-aside.css'; -type Props = { - +setModal: (modal: ?React.Node) => void, -}; -function LeftLayoutAside(props: Props): React.Node { - const { setModal } = props; +function LeftLayoutAside(): React.Node { return ( <aside className={css.container}> - <CommunityPicker setModal={setModal} /> + <CommunityPicker /> <AppSwitcher /> </aside> ); diff --git a/web/splash/splash.react.js b/web/splash/splash.react.js --- a/web/splash/splash.react.js +++ b/web/splash/splash.react.js @@ -20,20 +20,18 @@ import LoadingIndicator from '../loading-indicator.react'; import LogInModal from '../modals/account/log-in-modal.react'; +import { useModalContext } from '../modals/modal-provider.react'; import { useSelector } from '../redux/redux-utils'; import css from './splash.css'; const defaultRequestAccessScrollHeight = 390; -type BaseProps = { - +setModal: (modal: ?React.Node) => void, - +currentModal: ?React.Node, -}; type Props = { - ...BaseProps, +loadingStatus: LoadingStatus, +dispatchActionPromise: DispatchActionPromise, +requestAccess: (accessRequest: AccessRequest) => Promise<void>, + +setModal: (modal: React.Node) => void, + +modal: ?React.Node, }; type State = { +platform: DeviceType, @@ -178,7 +176,7 @@ </div> </div> </div> - {this.props.currentModal} + {this.props.modal} </React.Fragment> ); } @@ -201,7 +199,7 @@ onClickLogIn = (event: SyntheticEvent<HTMLAnchorElement>) => { event.preventDefault(); - this.props.setModal(<LogInModal setModal={this.props.setModal} />); + this.props.setModal(<LogInModal />); }; onClickRequestAccess = (event: SyntheticEvent<HTMLAnchorElement>) => { @@ -260,18 +258,22 @@ const loadingStatusSelector = createLoadingStatusSelector( requestAccessActionTypes, ); -const ConnectedSplash: React.ComponentType<BaseProps> = React.memo<BaseProps>( - function ConnectedSplash(props) { + +const ConnectedSplash: React.ComponentType<{}> = React.memo<{}>( + function ConnectedSplash(): React.Node { const loadingStatus = useSelector(loadingStatusSelector); const callRequestAccess = useServerCall(requestAccess); const dispatchActionPromise = useDispatchActionPromise(); + const modalContext = useModalContext(); + return ( <Splash - {...props} loadingStatus={loadingStatus} requestAccess={callRequestAccess} dispatchActionPromise={dispatchActionPromise} + setModal={modalContext.setModal} + modal={modalContext.modal} /> ); },