diff --git a/web/app.react.js b/web/app.react.js --- a/web/app.react.js +++ b/web/app.react.js @@ -1,16 +1,14 @@ // @flow +import { config as faConfig } from '@fortawesome/fontawesome-svg-core'; import '@fontsource/inter'; import '@fontsource/inter/500.css'; import '@fontsource/inter/600.css'; - import '@fontsource/ibm-plex-sans'; import '@fontsource/ibm-plex-sans/500.css'; import '@fontsource/ibm-plex-sans/600.css'; - import 'basscss/css/basscss.min.css'; import './theme.css'; -import { config as faConfig } from '@fortawesome/fontawesome-svg-core'; import _isEqual from 'lodash/fp/isEqual'; import * as React from 'react'; import { DndProvider } from 'react-dnd'; @@ -35,6 +33,7 @@ import Chat from './chat/chat.react'; import InputStateContainer from './input/input-state-container.react'; import LoadingIndicator from './loading-indicator.react'; +import { 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 +82,9 @@ +activeThreadCurrentlyUnread: boolean, // Redux dispatch functions +dispatch: Dispatch, -}; -type State = { +modal: ?React.Node, }; -class App extends React.PureComponent { - state: State = { - modal: null, - }; - +class App extends React.PureComponent { componentDidMount() { const { navInfo, @@ -136,16 +129,14 @@ if (this.props.loggedIn) { content = this.renderMainContent(); } else { - content = ( - - ); + content = ; } return ( {content} - {this.state.modal} + {this.props.modal} ); } @@ -153,11 +144,9 @@ renderMainContent() { let mainContent; if (this.props.navInfo.tab === 'calendar') { - mainContent = ( - - ); + mainContent = ; } else if (this.props.navInfo.tab === 'chat') { - mainContent = ; + mainContent = ; } return ( @@ -177,19 +166,15 @@ - +
{mainContent}
- + ); } - - setModal = (modal: ?React.Node) => { - this.setState({ modal }); - }; } const fetchEntriesLoadingStatusSelector = createLoadingStatusSelector( @@ -226,6 +211,7 @@ ); const dispatch = useDispatch(); + const modalContext = useModalContext(); return ( ); }, 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 @@ , @@ -166,7 +164,7 @@ let calendarContentStyle = null; let filterButtonStyle = null; if (this.state.filterPanelOpen) { - 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, - +setModal: (modal: ?React.Node) => void, +startingTabIndex: number, }; type Props = { @@ -45,6 +45,8 @@ +nextLocalID: number, +timeZone: ?string, +dispatch: Dispatch, + +clearModal: () => void, + +setModal: (modal: ?React.Node) => void, }; type State = { +pickerOpen: boolean, @@ -107,7 +109,6 @@ { if (!this.props.loggedIn) { - this.props.setModal( - , - ); + this.props.setModal(); return; } const viewerID = this.props.viewerID; @@ -240,11 +236,7 @@ onHistory = (event: SyntheticEvent) => { event.preventDefault(); this.props.setModal( - , + , ); }; @@ -258,10 +250,6 @@ entry.focus(); } }; - - clearModal = () => { - this.props.setModal(null); - }; } const ConnectedDay: React.ComponentType = React.memo( @@ -275,6 +263,7 @@ const nextLocalID = useSelector(state => state.nextLocalID); const timeZone = useSelector(state => state.timeZone); const dispatch = useDispatch(); + const modalContext = useModalContext(); return ( ); }, 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, +saveEntry: (info: SaveEntryInfo) => Promise, +deleteEntry: (info: DeleteEntryInfo) => Promise, + +setModal: (modal: ?React.Node) => void, + +clearModal: () => void, }; type State = { +focused: boolean, @@ -257,12 +259,7 @@ onChange: (event: SyntheticEvent) => void = event => { if (!this.props.loggedIn) { - this.props.setModal( - , - ); + this.props.setModal(); 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( - , + , ); } throw e; @@ -405,12 +399,7 @@ onDelete: (event: SyntheticEvent) => void = event => { event.preventDefault(); if (!this.props.loggedIn) { - this.props.setModal( - , - ); + this.props.setModal(); 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 ( ); }, 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, +filterThreadSearchIndex: () => SearchIndex, +filteredThreadIDs: ?$ReadOnlySet, +includeDeleted: boolean, +dispatch: Dispatch, + +setModal: (modal: ?React.Node) => void, }; type State = { +query: string, @@ -204,9 +202,7 @@ } onClickSettings = (threadID: string) => { - this.props.setModal( - , - ); + this.props.setModal(); }; onChangeQuery = (event: SyntheticEvent) => { @@ -238,10 +234,6 @@ }, }); }; - - clearModal = () => { - this.props.setModal(null); - }; } type ItemProps = { @@ -369,25 +361,24 @@ }; } -const ConnectedFilterPanel: React.ComponentType = React.memo( - function ConnectedFilterPanel(props) { - const filteredThreadIDs = useSelector(filteredThreadIDsSelector); - const filterThreadInfos = useSelector(webFilterThreadInfos); - const filterThreadSearchIndex = useSelector(webFilterThreadSearchIndex); - const includeDeleted = useSelector(includeDeletedSelector); - const dispatch = useDispatch(); +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 ( - - ); - }, -); + return ( + + ); +} export default ConnectedFilterPanel; 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 @@ ); } - const { threadInfo, setModal } = this.props; + const { threadInfo } = this.props; invariant(threadInfo, 'ThreadInfo should be set if messageListData is'); return ( @@ -384,113 +379,106 @@ registerFetchKey(fetchMessagesBeforeCursorActionTypes); registerFetchKey(fetchMostRecentMessagesActionTypes); -const ConnectedChatMessageList: React.ComponentType = React.memo( - function ConnectedChatMessageList(props) { - const userAgent = useSelector(state => state.userAgent); - const supportsReverseFlex = React.useMemo(() => { - const browser = detectBrowser(userAgent); - return ( - !browser || - browser.name !== 'firefox' || - parseInt(browser.version) >= 81 - ); - }, [userAgent]); - - const timeZone = useSelector(state => state.timeZone); - - const activeChatThreadID = useSelector( - state => state.navInfo.activeChatThreadID, - ); - const baseThreadInfo = useSelector(state => { - const activeID = state.navInfo.activeChatThreadID; - if (!activeID) { - return null; - } - return threadInfoSelector(state)[activeID] ?? state.navInfo.pendingThread; - }); - const existingThreadInfoFinder = useExistingThreadInfoFinder( - baseThreadInfo, - ); - const threadInfo = React.useMemo( - () => - existingThreadInfoFinder({ - searching: false, - userInfoInputArray: [], - }), - [existingThreadInfoFinder], +function ConnectedChatMessageList(): React.Node { + const userAgent = useSelector(state => state.userAgent); + const supportsReverseFlex = React.useMemo(() => { + const browser = detectBrowser(userAgent); + return ( + !browser || browser.name !== 'firefox' || parseInt(browser.version) >= 81 ); + }, [userAgent]); - const messageListData = useMessageListData({ - threadInfo, - searching: false, - userInfoInputArray: [], - }); - - const startReached = useSelector(state => { - const activeID = state.navInfo.activeChatThreadID; - if (!activeID) { - return null; - } - - if (state.navInfo.pendingThread) { - return true; - } - - const threadMessageInfo = state.messageStore.threads[activeID]; - if (!threadMessageInfo) { - return null; - } - return threadMessageInfo.startReached; - }); + const timeZone = useSelector(state => state.timeZone); - const dispatchActionPromise = useDispatchActionPromise(); - const callFetchMessagesBeforeCursor = useServerCall( - fetchMessagesBeforeCursor, - ); - - const inputState = React.useContext(InputStateContext); - const [dndProps, connectDropTarget] = useDrop({ - accept: NativeTypes.FILE, - drop: item => { - const { files } = item; - if (inputState && files.length > 0) { - inputState.appendFiles(files); - } - }, - collect: monitor => ({ - isActive: monitor.isOver() && monitor.canDrop(), + const activeChatThreadID = useSelector( + state => state.navInfo.activeChatThreadID, + ); + const baseThreadInfo = useSelector(state => { + const activeID = state.navInfo.activeChatThreadID; + if (!activeID) { + return null; + } + return threadInfoSelector(state)[activeID] ?? state.navInfo.pendingThread; + }); + const existingThreadInfoFinder = useExistingThreadInfoFinder(baseThreadInfo); + const threadInfo = React.useMemo( + () => + existingThreadInfoFinder({ + searching: false, + userInfoInputArray: [], }), - }); + [existingThreadInfoFinder], + ); + + const messageListData = useMessageListData({ + threadInfo, + searching: false, + userInfoInputArray: [], + }); + + const startReached = useSelector(state => { + const activeID = state.navInfo.activeChatThreadID; + if (!activeID) { + return null; + } + + if (state.navInfo.pendingThread) { + return true; + } - const getTextMessageMarkdownRules = useTextMessageRulesFunc(threadInfo?.id); - const messageListContext = React.useMemo(() => { - if (!getTextMessageMarkdownRules) { - return undefined; + const threadMessageInfo = state.messageStore.threads[activeID]; + if (!threadMessageInfo) { + return null; + } + return threadMessageInfo.startReached; + }); + + const dispatchActionPromise = useDispatchActionPromise(); + const callFetchMessagesBeforeCursor = useServerCall( + fetchMessagesBeforeCursor, + ); + + const inputState = React.useContext(InputStateContext); + const [dndProps, connectDropTarget] = useDrop({ + accept: NativeTypes.FILE, + drop: item => { + const { files } = item; + if (inputState && files.length > 0) { + inputState.appendFiles(files); } - return { getTextMessageMarkdownRules }; - }, [getTextMessageMarkdownRules]); + }, + collect: monitor => ({ + isActive: monitor.isOver() && monitor.canDrop(), + }), + }); + + const getTextMessageMarkdownRules = useTextMessageRulesFunc(threadInfo?.id); + const messageListContext = React.useMemo(() => { + if (!getTextMessageMarkdownRules) { + return undefined; + } + return { getTextMessageMarkdownRules }; + }, [getTextMessageMarkdownRules]); - useWatchThread(threadInfo); + useWatchThread(threadInfo); - return ( - - - - ); - }, -); + return ( + + + + ); +} export default ConnectedChatMessageList; 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 @@ />
- +
); 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,22 +6,21 @@ 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 modalContext = useModalContext(); + const onClick = React.useCallback( - () => - setModal( - , - ), - [setModal, threadInfo], + () => modalContext.setModal(), + [modalContext, threadInfo], ); const buttonText = showingSidebarsInline ? 'See more...' : 'See sidebars...'; return ( 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 => ( - + )); if (threads.length === 0 && isBackground) { threads.push(); } return threads; - }, [threadList, isBackground, setModal]); + }, [threadList, isBackground]); return (
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 ( <> - + - + ); } 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 { 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 @@ = React.memo( function ConnectedMultimediaMessage(props) { const inputState = React.useContext(InputStateContext); + return ; }, ); 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, +newThread: (request: ClientNewThreadRequest) => Promise, + +setModal: (modal: ?React.Node) => void, }; type State = { +pendingUploads: { @@ -518,7 +519,7 @@ ); if (appendResults.some(({ result }) => !result.success)) { - setModal(); + setModal(); 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 ( ); }, 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 { overlay: ?HTMLDivElement; @@ -29,7 +35,7 @@ >
@@ -44,7 +50,7 @@ event: SyntheticEvent, ) => void = event => { if (event.target === this.overlay) { - this.close(); + this.props.clearModal(); } }; @@ -52,13 +58,16 @@ event: SyntheticKeyboardEvent, ) => void = event => { if (event.keyCode === 27) { - this.close(); + this.props.clearModal(); } }; +} - close: () => void = () => { - this.props.setModal(null); - }; +function ConnectedMultiMediaModal(props: BaseProps): React.Node { + const { uri } = props; + const modalContext = useModalContext(); + + return ; } -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,21 @@ } 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 Props = { - uri: string, +type BaseProps = { + +uri: string, pendingUpload?: ?PendingMultimediaUpload, remove?: (uploadID: string) => void, - setModal?: (modal: ?React.Node) => void, multimediaCSSClass: string, multimediaImageCSSClass: string, }; +type Props = { + ...BaseProps, + setModal?: (modal: ?React.Node) => void, +}; class Multimedia extends React.PureComponent { componentDidUpdate(prevProps: Props) { const { uri, pendingUpload } = this.props; @@ -108,8 +112,32 @@ const { setModal, uri } = this.props; invariant(setModal, 'should be set'); - setModal(); + setModal(); }; } -export default Multimedia; +const ConnectedMultimediaContainer: React.ComponentType = React.memo( + function ConnectedMultimediaContainer(props) { + const { + uri, + pendingUpload, + remove, + multimediaCSSClass, + multimediaImageCSSClass, + } = props; + const modalContext = useModalContext(); + + 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 @@ -2,43 +2,41 @@ 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 = { +inOrderTo: string, - +setModal: (modal: ?React.Node) => void, }; -class LogInFirstModal extends React.PureComponent { - render(): React.Node { - return ( - -
-

- {`In order to ${this.props.inOrderTo}, you'll first need to `} - - log in - - {'.'} -

-
-
- ); - } +function LogInFirstModal(props: Props): React.Node { + const modalContext = useModalContext(); - clearModal: () => void = () => { - this.props.setModal(null); - }; - - onClickLogIn: (event: SyntheticEvent) => void = event => { + const onClickLogIn: ( + event: SyntheticEvent, + ) => void = event => { event.preventDefault(); - this.props.setModal(); + modalContext.setModal(); }; + + return ( + +
+

+ {`In order to ${props.inOrderTo}, you'll first need to `} + + log in + + {'.'} +

+
+
+ ); } export default LogInFirstModal; 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, + +clearModal: () => void, }; type State = { +username: string, @@ -63,7 +61,7 @@ render() { return ( - +
@@ -175,7 +173,7 @@ password: this.state.password, ...extraInfo, }); - this.clearModal(); + this.props.clearModal(); return result; } catch (e) { if (e.message === 'invalid_parameters') { @@ -216,31 +214,26 @@ throw e; } } - - clearModal = () => { - this.props.setModal(null); - }; } const loadingStatusSelector = createLoadingStatusSelector(logInActionTypes); -const ConnectedLoginModal: React.ComponentType = React.memo( - function ConnectedLoginModal(props) { - const inputDisabled = useSelector(loadingStatusSelector) === 'loading'; - const loginExtraInfo = useSelector(webLogInExtraInfoSelector); - const callLogIn = useServerCall(logIn); - const dispatchActionPromise = useDispatchActionPromise(); - - return ( - - ); - }, -); +function ConnectedLoginModal(): React.Node { + const inputDisabled = useSelector(loadingStatusSelector) === 'loading'; + const loginExtraInfo = useSelector(webLogInExtraInfoSelector); + const callLogIn = useServerCall(logIn); + const dispatchActionPromise = useDispatchActionPromise(); + const modalContext = useModalContext(); + + return ( + + ); +} export default ConnectedLoginModal; 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,8 @@ ) => Promise, +changeUserPassword: (passwordUpdate: PasswordUpdate) => Promise, +logOut: (preRequestUserState: PreRequestUserState) => Promise, + +setModal: (modal: ?React.Node) => void, + +clearModal: () => void, }; type State = { +newPassword: string, @@ -115,7 +114,7 @@ logOut = async () => { await this.props.logOut(this.props.preRequestUserState); - this.clearModal(); + this.props.clearModal(); }; render() { @@ -206,7 +205,11 @@ } return ( - +
    { - this.props.setModal(null); - }; } const deleteAccountLoadingStatusSelector = createLoadingStatusSelector( @@ -413,33 +412,34 @@ changeUserPasswordActionTypes, ); -const ConnectedUserSettingsModal: React.ComponentType = React.memo( - function ConnectedUserSettingsModal(props) { - const currentUserInfo = useSelector(state => state.currentUserInfo); - const preRequestUserState = useSelector(preRequestUserStateSelector); - const inputDisabled = useSelector( - state => - deleteAccountLoadingStatusSelector(state) === 'loading' || - changeUserPasswordLoadingStatusSelector(state) === 'loading', - ); - const callDeleteAccount = useServerCall(deleteAccount); - const callChangeUserPassword = useServerCall(changeUserPassword); - const dispatchActionPromise = useDispatchActionPromise(); - const boundLogOut = useServerCall(logOut); - - return ( - - ); - }, -); +function ConnectedUserSettingsModal(): React.Node { + const currentUserInfo = useSelector(state => state.currentUserInfo); + const preRequestUserState = useSelector(preRequestUserStateSelector); + const inputDisabled = useSelector( + state => + deleteAccountLoadingStatusSelector(state) === 'loading' || + changeUserPasswordLoadingStatusSelector(state) === 'loading', + ); + const callDeleteAccount = useServerCall(deleteAccount); + const callChangeUserPassword = useServerCall(changeUserPassword); + const dispatchActionPromise = useDispatchActionPromise(); + const boundLogOut = useServerCall(logOut); + + const modalContext = useModalContext(); + + return ( + + ); +} export default ConnectedUserSettingsModal; 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,20 @@ 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, -}; -class InvalidUploadModal extends React.PureComponent { - render(): React.Node { - return ( - -
    -

    We don't support that file type yet :(

    -
    -
    - ); - } +function InvalidUploadModal(): React.Node { + const modalContext = useModalContext(); - clearModal: () => void = () => { - this.props.setModal(null); - }; + return ( + +
    +

    We don't support that file type yet :(

    +
    +
    + ); } export default InvalidUploadModal; 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,8 +15,13 @@ 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 BaseProps = { + +threadInfo: ThreadInfo, +}; + type Props = { +setModal: (modal: ?React.Node) => void, +threadInfo: ThreadInfo, @@ -117,7 +122,7 @@ } return ( - +
    ); } +function ConnectedSidebarListModal(props: BaseProps): React.Node { + const { threadInfo } = props; + const modalContext = useModalContext(); -export default SidebarListModal; + return ( + + ); +} +export default ConnectedSidebarListModal; 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 ( - +

    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>, + +onClose: () => void, }; type State = { +mode: HistoryMode, @@ -167,7 +168,7 @@ }); return ( - +

    {allHistoryButton} @@ -262,6 +263,7 @@ const callFetchEntries = useServerCall(fetchEntries); const callFetchRevisionsForEntry = useServerCall(fetchRevisionsForEntry); const dispatchActionPromise = useDispatchActionPromise(); + const modalContext = useModalContext(); return ( ); }, diff --git a/web/modals/modal-provider.react.js b/web/modals/modal-provider.react.js new file mode 100644 --- /dev/null +++ b/web/modals/modal-provider.react.js @@ -0,0 +1,51 @@ +// @flow + +import invariant from 'invariant'; +import * as React from 'react'; +type Props = { + +children: React.Node, +}; +type ModalContextType = { + +modal: ?React.Node, + +setModal: (?React.Node) => void, + +clearModal: () => void, +}; + +const ModalContext: React.Context = React.createContext( + { + modal: null, + setModal: () => {}, + clearModal: () => {}, + }, +); + +function ModalProvider(props: Props): React.Node { + const { children } = props; + const [modal, setModal] = React.useState(null); + const clearModal = React.useCallback(() => setModal(null), [setModal]); + + const contextSetModal = React.useCallback((component: ?React.Node) => { + setModal(component); + }, []); + + const value = React.useMemo(() => { + return { + modal, + setModal: contextSetModal, + clearModal, + }; + }, [contextSetModal, modal, clearModal]); + + return ( + {children} + ); +} + +function useModalContext(): ModalContextType { + const context = React.useContext(ModalContext); + invariant(context, 'ModalContext not found'); + + return context; +} + +export { ModalProvider, useModalContext }; diff --git a/web/modals/modal.react.js b/web/modals/modal.react.js --- a/web/modals/modal.react.js +++ b/web/modals/modal.react.js @@ -4,12 +4,13 @@ import invariant from 'invariant'; import * as React from 'react'; +import { useModalContext } from './modal-provider.react'; import css from './modal.css'; export type ModalSize = 'small' | 'large'; type Props = { +name: string, - +onClose: () => void, + +clearModal: () => void, +children?: React.Node, +size?: ModalSize, +fixedHeight?: boolean, @@ -27,7 +28,7 @@ } render(): React.Node { - const { size, children, onClose, fixedHeight, name } = this.props; + const { size, children, clearModal, fixedHeight, name } = this.props; const overlayClasses = classNames( css['modal-overlay'], @@ -52,7 +53,7 @@
    - + ×

    {name}

    @@ -72,7 +73,7 @@ event: SyntheticEvent, ) => void = event => { if (event.target === this.overlay) { - this.props.onClose(); + this.props.clearModal(); } }; @@ -80,9 +81,25 @@ event: SyntheticKeyboardEvent, ) => void = event => { if (event.keyCode === 27) { - this.props.onClose(); + this.props.clearModal(); } }; } -export default Modal; +const ConnectedModal = (props: Props): React.Node => { + const { name, size, children, fixedHeight } = props; + const modalContext = useModalContext(); + + return ( + + {children} + + ); +}; + +export default ConnectedModal; diff --git a/web/modals/threads/cant-leave-thread-modal.react.js b/web/modals/threads/cant-leave-thread-modal.react.js --- a/web/modals/threads/cant-leave-thread-modal.react.js +++ b/web/modals/threads/cant-leave-thread-modal.react.js @@ -10,7 +10,7 @@ }; function CantLeaveThreadModal(props: Props): React.Node { return ( - +

    You are the only admin left of this thread. Please promote somebody diff --git a/web/modals/threads/confirm-leave-thread-modal.react.js b/web/modals/threads/confirm-leave-thread-modal.react.js --- a/web/modals/threads/confirm-leave-thread-modal.react.js +++ b/web/modals/threads/confirm-leave-thread-modal.react.js @@ -14,7 +14,7 @@ }; function ConfirmLeaveThreadModal(props: Props): React.Node { return ( - +

    {'Are you sure you want to leave "'} diff --git a/web/modals/threads/new-thread-modal.react.js b/web/modals/threads/new-thread-modal.react.js --- a/web/modals/threads/new-thread-modal.react.js +++ b/web/modals/threads/new-thread-modal.react.js @@ -130,7 +130,7 @@ ); } return ( - +

    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, + +clearModal: () => void, }; type State = { +queuedChanges: ThreadChanges, @@ -377,7 +378,11 @@ } return ( - +
      {tabs}
    @@ -481,7 +486,7 @@ threadID: this.props.threadInfo.id, changes: this.state.queuedChanges, }); - this.props.onClose(); + this.props.clearModal(); return response; } catch (e) { this.setState( @@ -515,7 +520,7 @@ this.props.threadInfo.id, this.state.accountPassword, ); - this.props.onClose(); + this.props.clearModal(); return response; } catch (e) { const errorMessage = @@ -564,9 +569,11 @@ const threadInfo: ?ThreadInfo = useSelector( state => threadInfoSelector(state)[props.threadID], ); + const modalContext = useModalContext(); + if (!threadInfo) { return ( - +

    You no longer have permission to view this thread

    @@ -584,6 +591,7 @@ deleteThread={callDeleteThread} changeThreadSettings={callChangeThreadSettings} dispatchActionPromise={dispatchActionPromise} + clearModal={modalContext.clearModal} /> ); }, diff --git a/web/root.js b/web/root.js --- a/web/root.js +++ b/web/root.js @@ -9,6 +9,7 @@ import { reduxLoggerMiddleware } from 'lib/utils/action-logger'; import HotRoot from './hot'; +import { ModalProvider } from './modals/modal-provider.react'; import { reducer } from './redux/redux-setup'; import type { AppState, Action } from './redux/redux-setup'; @@ -21,7 +22,9 @@ const RootProvider = (): React.Node => ( - + + + ); 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,17 +4,16 @@ 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 modalContext = useModalContext(); const setModalToUserSettings = React.useCallback(() => { - setModal(); - }, [setModal]); + modalContext.setModal(); + }, [modalContext]); 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 ( ); 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, + +setModal: (modal: React.Node) => void, + +modal: ?React.Node, }; type State = { +platform: DeviceType, @@ -178,7 +176,7 @@
    - {this.props.currentModal} + {this.props.modal} ); } @@ -201,7 +199,7 @@ onClickLogIn = (event: SyntheticEvent) => { event.preventDefault(); - this.props.setModal(); + this.props.setModal(); }; onClickRequestAccess = (event: SyntheticEvent) => { @@ -260,21 +258,22 @@ const loadingStatusSelector = createLoadingStatusSelector( requestAccessActionTypes, ); -const ConnectedSplash: React.ComponentType = React.memo( - function ConnectedSplash(props) { - const loadingStatus = useSelector(loadingStatusSelector); - const callRequestAccess = useServerCall(requestAccess); - const dispatchActionPromise = useDispatchActionPromise(); +function ConnectedSplash(): React.Node { + const loadingStatus = useSelector(loadingStatusSelector); + const callRequestAccess = useServerCall(requestAccess); + const dispatchActionPromise = useDispatchActionPromise(); - return ( - - ); - }, -); + const modalContext = useModalContext(); + + return ( + + ); +} export default ConnectedSplash;