diff --git a/web/app.react.js b/web/app.react.js index 8acbc9d35..8c0dd96b7 100644 --- a/web/app.react.js +++ b/web/app.react.js @@ -1,356 +1,375 @@ // @flow import { config as faConfig } from '@fortawesome/fontawesome-svg-core'; import { faCalendar, faComments } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import classNames from 'classnames'; import invariant from 'invariant'; import _isEqual from 'lodash/fp/isEqual'; -import PropTypes from 'prop-types'; import * as React from 'react'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; +import { useDispatch } from 'react-redux'; import { fetchEntriesActionTypes, updateCalendarQueryActionTypes, } from 'lib/actions/entry-actions'; import { createLoadingStatusSelector, combineLoadingStatuses, } from 'lib/selectors/loading-selectors'; import { mostRecentReadThreadSelector, unreadCount, } from 'lib/selectors/thread-selectors'; import { isLoggedIn } from 'lib/selectors/user-selectors'; import type { LoadingStatus } from 'lib/types/loading-types'; +import type { Dispatch } from 'lib/types/redux-types'; import { verifyField, type ServerVerificationResult, - serverVerificationResultPropType, } from 'lib/types/verify-types'; -import type { DispatchActionPayload } from 'lib/utils/action-utils'; import { registerConfig } from 'lib/utils/config'; -import { connect } from 'lib/utils/redux-utils'; import AccountBar from './account-bar.react'; import Calendar from './calendar/calendar.react'; import Chat from './chat/chat.react'; import InputStateContainer from './input/input-state-container.react'; import LoadingIndicator from './loading-indicator.react'; import ResetPasswordModal from './modals/account/reset-password-modal.react'; import VerificationModal from './modals/account/verification-modal.react'; import FocusHandler from './redux/focus-handler.react'; -import { - type AppState, - type NavInfo, - navInfoPropType, - updateNavInfoActionType, -} from './redux/redux-setup'; +import { type NavInfo, updateNavInfoActionType } from './redux/redux-setup'; +import { useSelector } from './redux/redux-utils'; import VisibilityHandler from './redux/visibility-handler.react'; import history from './router-history'; import Splash from './splash/splash.react'; import css from './style.css'; import { canonicalURLFromReduxState, navInfoFromURL } from './url-utils'; // We want Webpack's css-loader and style-loader to handle the Fontawesome CSS, // so we disable the autoAddCss logic and import the CSS file. Otherwise every // icon flashes huge for a second before the CSS is loaded. import '@fortawesome/fontawesome-svg-core/styles.css'; faConfig.autoAddCss = false; registerConfig({ // We can't securely cache credentials on web, so we have no way to recover // from a cookie invalidation resolveInvalidatedCookie: null, // We use httponly cookies on web to protect against XSS attacks, so we have // no access to the cookies from JavaScript setCookieOnRequest: false, setSessionIDOnRequest: true, // Never reset the calendar range calendarRangeInactivityLimit: null, platformDetails: { platform: 'web' }, }); -type Props = { - location: { - pathname: string, +type BaseProps = {| + +location: { + +pathname: string, + ... }, +|}; +type Props = {| + ...BaseProps, // Redux state - navInfo: NavInfo, - serverVerificationResult: ?ServerVerificationResult, - entriesLoadingStatus: LoadingStatus, - loggedIn: boolean, - mostRecentReadThread: ?string, - activeThreadCurrentlyUnread: boolean, - viewerID: ?string, - unreadCount: number, + +navInfo: NavInfo, + +serverVerificationResult: ?ServerVerificationResult, + +entriesLoadingStatus: LoadingStatus, + +loggedIn: boolean, + +mostRecentReadThread: ?string, + +activeThreadCurrentlyUnread: boolean, + +viewerID: ?string, + +unreadCount: number, // Redux dispatch functions - dispatchActionPayload: DispatchActionPayload, -}; + +dispatch: Dispatch, +|}; type State = {| - currentModal: ?React.Node, + +currentModal: ?React.Node, |}; class App extends React.PureComponent { - static propTypes = { - location: PropTypes.shape({ - pathname: PropTypes.string.isRequired, - }).isRequired, - navInfo: navInfoPropType.isRequired, - serverVerificationResult: serverVerificationResultPropType, - entriesLoadingStatus: PropTypes.string.isRequired, - loggedIn: PropTypes.bool.isRequired, - mostRecentReadThread: PropTypes.string, - activeThreadCurrentlyUnread: PropTypes.bool.isRequired, - viewerID: PropTypes.string, - unreadCount: PropTypes.number.isRequired, - dispatchActionPayload: PropTypes.func.isRequired, - }; state: State = { currentModal: null, }; componentDidMount() { const { navInfo, serverVerificationResult } = this.props; if (navInfo.verify && serverVerificationResult) { if (serverVerificationResult.field === verifyField.RESET_PASSWORD) { this.showResetPasswordModal(); } else { const newURL = canonicalURLFromReduxState( { ...navInfo, verify: null }, this.props.location.pathname, ); history.replace(newURL); this.setModal(); } } if (this.props.loggedIn) { const newURL = canonicalURLFromReduxState( navInfo, this.props.location.pathname, ); if (this.props.location.pathname !== newURL) { history.replace(newURL); } } else if (this.props.location.pathname !== '/') { history.replace('/'); } } componentDidUpdate(prevProps: Props) { if (this.props.loggedIn) { if (this.props.location.pathname !== prevProps.location.pathname) { const newNavInfo = navInfoFromURL(this.props.location.pathname, { navInfo: this.props.navInfo, }); if (!_isEqual(newNavInfo)(this.props.navInfo)) { - this.props.dispatchActionPayload(updateNavInfoActionType, newNavInfo); + this.props.dispatch({ + type: updateNavInfoActionType, + payload: newNavInfo, + }); } } else if (!_isEqual(this.props.navInfo)(prevProps.navInfo)) { const newURL = canonicalURLFromReduxState( this.props.navInfo, this.props.location.pathname, ); if (newURL !== this.props.location.pathname) { history.push(newURL); } } } const justLoggedIn = this.props.loggedIn && !prevProps.loggedIn; if (justLoggedIn) { const newURL = canonicalURLFromReduxState( this.props.navInfo, this.props.location.pathname, ); if (this.props.location.pathname !== newURL) { history.replace(newURL); } } const justLoggedOut = !this.props.loggedIn && prevProps.loggedIn; if (justLoggedOut && this.props.location.pathname !== '/') { history.replace('/'); } const { navInfo, serverVerificationResult } = this.props; if ( serverVerificationResult && serverVerificationResult.field === verifyField.RESET_PASSWORD ) { if (navInfo.verify && !prevProps.navInfo.verify) { this.showResetPasswordModal(); } else if (!navInfo.verify && prevProps.navInfo.verify) { this.clearModal(); } } } showResetPasswordModal() { const newURL = canonicalURLFromReduxState( { ...this.props.navInfo, verify: null, }, this.props.location.pathname, ); const onClose = () => history.push(newURL); const onSuccess = () => history.replace(newURL); this.setModal( , ); } render() { let content; if (this.props.loggedIn) { content = this.renderMainContent(); } else { content = ( ); } return ( {content} {this.state.currentModal} ); } renderMainContent() { const calendarNavClasses = classNames({ [css['current-tab']]: this.props.navInfo.tab === 'calendar', }); const chatNavClasses = classNames({ [css['current-tab']]: this.props.navInfo.tab === 'chat', }); let mainContent; if (this.props.navInfo.tab === 'calendar') { mainContent = ( ); } else if (this.props.navInfo.tab === 'chat') { mainContent = ; } const { viewerID, unreadCount: curUnreadCount } = this.props; invariant(viewerID, 'should be set'); let chatBadge = null; if (curUnreadCount > 0) { chatBadge =
{curUnreadCount}
; } return (
{mainContent}
); } setModal = (modal: ?React.Node) => { this.setState({ currentModal: modal }); }; clearModal = () => { this.setModal(null); }; onClickCalendar = (event: SyntheticEvent) => { event.preventDefault(); - this.props.dispatchActionPayload(updateNavInfoActionType, { - tab: 'calendar', + this.props.dispatch({ + type: updateNavInfoActionType, + payload: { tab: 'calendar' }, }); }; onClickChat = (event: SyntheticEvent) => { event.preventDefault(); - this.props.dispatchActionPayload(updateNavInfoActionType, { - tab: 'chat', - activeChatThreadID: this.props.activeThreadCurrentlyUnread - ? this.props.mostRecentReadThread - : this.props.navInfo.activeChatThreadID, + this.props.dispatch({ + type: updateNavInfoActionType, + payload: { + tab: 'chat', + activeChatThreadID: this.props.activeThreadCurrentlyUnread + ? this.props.mostRecentReadThread + : this.props.navInfo.activeChatThreadID, + }, }); }; } const fetchEntriesLoadingStatusSelector = createLoadingStatusSelector( fetchEntriesActionTypes, ); const updateCalendarQueryLoadingStatusSelector = createLoadingStatusSelector( updateCalendarQueryActionTypes, ); -export default connect( - (state: AppState) => { - const activeChatThreadID = state.navInfo.activeChatThreadID; - return { - navInfo: state.navInfo, - serverVerificationResult: state.serverVerificationResult, - entriesLoadingStatus: combineLoadingStatuses( - fetchEntriesLoadingStatusSelector(state), - updateCalendarQueryLoadingStatusSelector(state), - ), - loggedIn: isLoggedIn(state), - mostRecentReadThread: mostRecentReadThreadSelector(state), - activeThreadCurrentlyUnread: - !activeChatThreadID || - state.threadStore.threadInfos[activeChatThreadID].currentUser.unread, - viewerID: state.currentUserInfo && state.currentUserInfo.id, - unreadCount: unreadCount(state), - }; - }, - null, - true, -)(App); +export default React.memo(function ConnectedApp(props: BaseProps) { + const activeChatThreadID = useSelector( + (state) => state.navInfo.activeChatThreadID, + ); + const navInfo = useSelector((state) => state.navInfo); + const serverVerificationResult = useSelector( + (state) => state.serverVerificationResult, + ); + + const fetchEntriesLoadingStatus = useSelector( + fetchEntriesLoadingStatusSelector, + ); + const updateCalendarQueryLoadingStatus = useSelector( + updateCalendarQueryLoadingStatusSelector, + ); + const entriesLoadingStatus = combineLoadingStatuses( + fetchEntriesLoadingStatus, + updateCalendarQueryLoadingStatus, + ); + + const loggedIn = useSelector(isLoggedIn); + const mostRecentReadThread = useSelector(mostRecentReadThreadSelector); + const activeThreadCurrentlyUnread = useSelector( + (state) => + !activeChatThreadID || + !!state.threadStore.threadInfos[activeChatThreadID].currentUser.unread, + ); + + const viewerID = useSelector( + (state) => state.currentUserInfo && state.currentUserInfo.id, + ); + const boundUnreadCount = useSelector(unreadCount); + + const dispatch = useDispatch(); + + return ( + + ); +});