diff --git a/web/app.react.js b/web/app.react.js index 61f6a964e..29e38efea 100644 --- a/web/app.react.js +++ b/web/app.react.js @@ -1,281 +1,283 @@ // @flow 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'; 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 { 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 { registerConfig } from 'lib/utils/config'; import AppsDirectory from './apps/apps-directory.react'; import Calendar from './calendar/calendar.react'; import Chat from './chat/chat.react'; import NavigationArrows from './components/navigation-arrows.react'; import InputStateContainer from './input/input-state-container.react'; import LoadingIndicator from './loading-indicator.react'; import { MenuProvider } from './menu-provider.react'; import { ModalProvider, useModalContext } from './modals/modal-provider.react'; +import DeviceIDUpdater from './redux/device-id-updater'; import DisconnectedBar from './redux/disconnected-bar'; import DisconnectedBarVisibilityHandler from './redux/disconnected-bar-visibility-handler'; import FocusHandler from './redux/focus-handler.react'; import { useSelector } from './redux/redux-utils'; import VisibilityHandler from './redux/visibility-handler.react'; import history from './router-history'; import AccountSettings from './settings/account-settings.react'; import DangerZone from './settings/danger-zone.react'; import LeftLayoutAside from './sidebar/left-layout-aside.react'; import Splash from './splash/splash.react'; import './typography.css'; import css from './style.css'; import getTitle from './title/getTitle'; import { type NavInfo, updateNavInfoActionType } from './types/nav-types'; 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 BaseProps = { +location: { +pathname: string, ... }, }; type Props = { ...BaseProps, // Redux state +navInfo: NavInfo, +entriesLoadingStatus: LoadingStatus, +loggedIn: boolean, +activeThreadCurrentlyUnread: boolean, // Redux dispatch functions +dispatch: Dispatch, +modals: $ReadOnlyArray, }; class App extends React.PureComponent { componentDidMount() { const { navInfo, location: { pathname }, loggedIn, } = this.props; const newURL = canonicalURLFromReduxState(navInfo, pathname, loggedIn); if (pathname !== newURL) { history.replace(newURL); } } componentDidUpdate(prevProps: Props) { const { navInfo, location: { pathname }, loggedIn, } = this.props; if (!_isEqual(navInfo)(prevProps.navInfo)) { const newURL = canonicalURLFromReduxState(navInfo, pathname, loggedIn); if (newURL !== pathname) { history.push(newURL); } } else if (pathname !== prevProps.location.pathname) { const newNavInfo = navInfoFromURL(pathname, { navInfo }); if (!_isEqual(newNavInfo)(navInfo)) { this.props.dispatch({ type: updateNavInfoActionType, payload: newNavInfo, }); } } else if (loggedIn !== prevProps.loggedIn) { const newURL = canonicalURLFromReduxState(navInfo, pathname, loggedIn); if (newURL !== pathname) { history.replace(newURL); } } } onWordmarkClicked = () => { this.props.dispatch({ type: updateNavInfoActionType, payload: { tab: 'chat' }, }); }; render() { let content; if (this.props.loggedIn) { content = this.renderMainContent(); } else { content = ; } return ( + {content} {this.props.modals} ); } renderMainContent() { let mainContent; const { tab, settingsSection } = this.props.navInfo; if (tab === 'calendar') { mainContent = ; } else if (tab === 'chat') { mainContent = ; } else if (tab === 'apps') { mainContent = ; } else if (tab === 'settings') { if (settingsSection === 'account') { mainContent = ; } else if (settingsSection === 'danger-zone') { mainContent = ; } } const shouldShowNavigationArrows = false; let navigationArrows = null; if (shouldShowNavigationArrows) { navigationArrows = ; } return (

Comm

{navigationArrows}
{mainContent}
); } } const fetchEntriesLoadingStatusSelector = createLoadingStatusSelector( fetchEntriesActionTypes, ); const updateCalendarQueryLoadingStatusSelector = createLoadingStatusSelector( updateCalendarQueryActionTypes, ); const ConnectedApp: React.ComponentType = React.memo( function ConnectedApp(props) { const activeChatThreadID = useSelector( state => state.navInfo.activeChatThreadID, ); const navInfo = useSelector(state => state.navInfo); const fetchEntriesLoadingStatus = useSelector( fetchEntriesLoadingStatusSelector, ); const updateCalendarQueryLoadingStatus = useSelector( updateCalendarQueryLoadingStatusSelector, ); const entriesLoadingStatus = combineLoadingStatuses( fetchEntriesLoadingStatus, updateCalendarQueryLoadingStatus, ); const loggedIn = useSelector(isLoggedIn); const activeThreadCurrentlyUnread = useSelector( state => !activeChatThreadID || !!state.threadStore.threadInfos[activeChatThreadID]?.currentUser.unread, ); const boundUnreadCount = useSelector(unreadCount); React.useEffect(() => { document.title = getTitle(boundUnreadCount); }, [boundUnreadCount]); const dispatch = useDispatch(); const modalContext = useModalContext(); const modals = React.useMemo( () => modalContext.modals.map(([modal, key]) => ( {modal} )), [modalContext.modals], ); return ( ); }, ); function AppWithProvider(props: BaseProps): React.Node { return ( ); } export default AppWithProvider; diff --git a/web/redux/device-id-reducer.js b/web/redux/device-id-reducer.js index bef6b6279..8430446fe 100644 --- a/web/redux/device-id-reducer.js +++ b/web/redux/device-id-reducer.js @@ -1,15 +1,28 @@ // @flow +import { + deleteAccountActionTypes, + logOutActionTypes, +} from 'lib/actions/user-actions.js'; +import { setNewSessionActionType } from 'lib/utils/action-utils.js'; + import type { Action } from '../redux/redux-setup'; import { deviceIDFormatRegex } from '../utils/device-id'; import { setDeviceIDActionType } from './action-types'; export function reduceDeviceID(state: ?string, action: Action): ?string { if (action.type === setDeviceIDActionType) { if (action.payload?.match(deviceIDFormatRegex)) { return action.payload; } return null; + } else if ( + action.type === logOutActionTypes.success || + action.type === deleteAccountActionTypes.success || + (action.type === setNewSessionActionType && + action.payload.sessionChange.cookieInvalidated) + ) { + return null; } return state; } diff --git a/web/redux/device-id-updater.js b/web/redux/device-id-updater.js new file mode 100644 index 000000000..4cbf0e445 --- /dev/null +++ b/web/redux/device-id-updater.js @@ -0,0 +1,28 @@ +// @flow +import * as React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { generateDeviceID, deviceIDTypes } from '../utils/device-id'; +import { setDeviceIDActionType } from './action-types'; + +function DeviceIDUpdater(): null { + const dispatch = useDispatch(); + const deviceID = useSelector(state => state.deviceID); + const hasDeviceID = !!deviceID; + const hadDeviceIDRef = React.useRef(null); + + React.useEffect(() => { + if (hadDeviceIDRef.current !== false && !hasDeviceID) { + const newDeviceID = generateDeviceID(deviceIDTypes.WEB); + dispatch({ + type: setDeviceIDActionType, + payload: newDeviceID, + }); + } + hadDeviceIDRef.current = hasDeviceID; + }, [hasDeviceID, dispatch]); + + return null; +} + +export default DeviceIDUpdater;