diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2022-09-09 10:32:50 +0200 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2022-09-09 10:32:50 +0200 |
| commit | 7abc85f427c10d72e6ad3fd3a1d1369430acf570 (patch) | |
| tree | 6c9cc0265866e85e5652f828fc9e35dc2ecdc064 | |
| parent | 65bc519d7271b3bef32dd50cf7b8d41d42f07389 (diff) | |
| parent | 83d5938d6c228976b05f73f6b56c725e2069586f (diff) | |
| download | mullvadvpn-7abc85f427c10d72e6ad3fd3a1d1369430acf570.tar.xz mullvadvpn-7abc85f427c10d72e6ad3fd3a1d1369430acf570.zip | |
Merge branch 'prevent-redux-updates-during-transition'
| -rw-r--r-- | gui/src/renderer/app.tsx | 3 | ||||
| -rw-r--r-- | gui/src/renderer/components/Account.tsx | 45 | ||||
| -rw-r--r-- | gui/src/renderer/components/Modal.tsx | 17 | ||||
| -rw-r--r-- | gui/src/renderer/components/TransitionContainer.tsx | 36 | ||||
| -rw-r--r-- | gui/src/renderer/containers/AccountPage.tsx | 4 | ||||
| -rw-r--r-- | gui/src/renderer/lib/will-exit.tsx | 13 | ||||
| -rw-r--r-- | gui/src/renderer/redux/store.ts | 14 |
7 files changed, 90 insertions, 42 deletions
diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx index faaf784aed..a3c5407eaa 100644 --- a/gui/src/renderer/app.tsx +++ b/gui/src/renderer/app.tsx @@ -369,6 +369,7 @@ export default class AppRenderer { public async logout() { try { + this.history.reset(RoutePath.login, transitions.dismiss); await IpcRendererEventChannel.account.logout(); } catch (e) { const error = e as Error; @@ -647,7 +648,7 @@ export default class AppRenderer { [RoutePath.launch]: transitions.push, [RoutePath.main]: transitions.pop, [RoutePath.deviceRevoked]: transitions.pop, - '*': transitions.none, + '*': transitions.dismiss, }, [RoutePath.main]: { [RoutePath.launch]: transitions.push, diff --git a/gui/src/renderer/components/Account.tsx b/gui/src/renderer/components/Account.tsx index 2ab21f7d30..b5bbfd773c 100644 --- a/gui/src/renderer/components/Account.tsx +++ b/gui/src/renderer/components/Account.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; import { formatDate, hasExpired } from '../../shared/account-expiry'; -import { AccountToken, DeviceState } from '../../shared/daemon-rpc-types'; +import { DeviceState } from '../../shared/daemon-rpc-types'; import { messages } from '../../shared/gettext'; +import { useSelector } from '../redux/store'; import { AccountContainer, AccountFooter, @@ -27,10 +28,6 @@ import { NavigationBar, NavigationItems, TitleBarItem } from './NavigationBar'; import SettingsHeader, { HeaderTitle } from './SettingsHeader'; interface IProps { - deviceName?: string; - accountToken?: AccountToken; - accountExpiry?: string; - expiryLocale: string; isOffline: boolean; prepareLogout: () => void; cancelLogout: () => void; @@ -81,27 +78,21 @@ export default class Account extends React.Component<IProps, IState> { <AccountRowLabel> {messages.pgettext('device-management', 'Device name')} </AccountRowLabel> - <DeviceRowValue>{this.props.deviceName}</DeviceRowValue> + <DeviceNameRow /> </AccountRow> <AccountRow> <AccountRowLabel> {messages.pgettext('account-view', 'Account number')} </AccountRowLabel> - <AccountRowValue - as={AccountTokenLabel} - accountToken={this.props.accountToken || ''} - /> + <AccountNumberRow /> </AccountRow> <AccountRow> <AccountRowLabel> {messages.pgettext('account-view', 'Paid until')} </AccountRowLabel> - <FormattedAccountExpiry - expiry={this.props.accountExpiry} - locale={this.props.expiryLocale} - /> + <AccountExpiryRow /> </AccountRow> </AccountRows> @@ -166,7 +157,7 @@ export default class Account extends React.Component<IProps, IState> { this.state.logoutDialogStage === 'checking-ports' ? [] : [ - <AppButton.RedButton key="logout" onClick={this.props.onLogout}> + <AppButton.RedButton key="logout" onClick={this.confirmLogout}> { // TRANSLATORS: Confirmation button when logging out messages.pgettext('device-management', 'Log out anyway') @@ -196,11 +187,15 @@ export default class Account extends React.Component<IProps, IState> { ) { this.setState({ logoutDialogStage: 'confirm' }); } else { - this.props.onLogout(); - this.onHideLogoutConfirmationDialog(); + this.confirmLogout(); } }; + private confirmLogout = () => { + this.onHideLogoutConfirmationDialog(); + this.props.onLogout(); + }; + private cancelLogout = () => { this.props.cancelLogout(); this.onHideLogoutConfirmationDialog(); @@ -211,6 +206,22 @@ export default class Account extends React.Component<IProps, IState> { }; } +function AccountNumberRow() { + const accountToken = useSelector((state) => state.account.accountToken); + return <AccountRowValue as={AccountTokenLabel} accountToken={accountToken || ''} />; +} + +function AccountExpiryRow() { + const accountExpiry = useSelector((state) => state.account.expiry); + const expiryLocale = useSelector((state) => state.userInterface.locale); + return <FormattedAccountExpiry expiry={accountExpiry} locale={expiryLocale} />; +} + +function DeviceNameRow() { + const deviceName = useSelector((state) => state.account.deviceName); + return <DeviceRowValue>{deviceName}</DeviceRowValue>; +} + function FormattedAccountExpiry(props: { expiry?: string; locale: string }) { if (props.expiry) { if (hasExpired(props.expiry)) { diff --git a/gui/src/renderer/components/Modal.tsx b/gui/src/renderer/components/Modal.tsx index f6fa9f25b4..92e794342e 100644 --- a/gui/src/renderer/components/Modal.tsx +++ b/gui/src/renderer/components/Modal.tsx @@ -4,6 +4,7 @@ import styled from 'styled-components'; import { colors } from '../../config.json'; import log from '../../shared/logging'; +import { useWillExit } from '../lib/will-exit'; import { tinyText } from './common-styles'; import CustomScrollbars from './CustomScrollbars'; import ImageView from './ImageView'; @@ -162,10 +163,22 @@ export function ModalAlert(props: IModalAlertProps & { isOpen: boolean }) { const [closing, setClosing] = useState(false); const prevIsOpen = useRef(isOpen); - const onTransitionEnd = useCallback(() => setClosing(false), []); + const willExit = useWillExit(); + + // Modal shouldn't prepare for being opened again while view is disappearing. + const onTransitionEnd = useCallback(() => { + if (!willExit) { + setClosing(false); + } + }, [willExit]); + useEffect(() => { setClosing((closing) => closing || (prevIsOpen.current && !isOpen)); - prevIsOpen.current = isOpen; + + // Unmounting the Modal during view transitions result in a visual glitch. + if (!willExit) { + prevIsOpen.current = isOpen; + } }, [isOpen]); if (!prevIsOpen.current && !isOpen && !closing) { diff --git a/gui/src/renderer/components/TransitionContainer.tsx b/gui/src/renderer/components/TransitionContainer.tsx index e360e6838e..6deeac5cd0 100644 --- a/gui/src/renderer/components/TransitionContainer.tsx +++ b/gui/src/renderer/components/TransitionContainer.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import styled from 'styled-components'; import { ITransitionSpecification } from '../lib/history'; +import { WillExit } from '../lib/will-exit'; interface ITransitioningViewProps { viewId: string; @@ -154,29 +155,30 @@ export default class TransitionContainer extends React.Component<IProps, IState> } public render() { - const disableUserInteraction = - this.state.itemQueue.length > 0 || this.state.nextItem ? true : false; + const willExit = this.state.itemQueue.length > 0 || this.state.nextItem !== undefined; return ( - <StyledTransitionContainer disableUserInteraction={disableUserInteraction}> + <StyledTransitionContainer disableUserInteraction={willExit}> {this.state.currentItem && ( - <StyledTransitionContent - key={this.state.currentItem.view.props.viewId} - ref={this.currentContentRef} - transition={this.state.currentItemStyle} - onTransitionEnd={this.onTransitionEnd}> - {this.state.currentItem.view} - </StyledTransitionContent> + <WillExit key={this.state.currentItem.view.props.viewId} value={willExit}> + <StyledTransitionContent + ref={this.currentContentRef} + transition={this.state.currentItemStyle} + onTransitionEnd={this.onTransitionEnd}> + {this.state.currentItem.view} + </StyledTransitionContent> + </WillExit> )} {this.state.nextItem && ( - <StyledTransitionContent - key={this.state.nextItem.view.props.viewId} - ref={this.nextContentRef} - transition={this.state.nextItemStyle} - onTransitionEnd={this.onTransitionEnd}> - {this.state.nextItem.view} - </StyledTransitionContent> + <WillExit key={this.state.nextItem.view.props.viewId} value={false}> + <StyledTransitionContent + ref={this.nextContentRef} + transition={this.state.nextItemStyle} + onTransitionEnd={this.onTransitionEnd}> + {this.state.nextItem.view} + </StyledTransitionContent> + </WillExit> )} </StyledTransitionContainer> ); diff --git a/gui/src/renderer/containers/AccountPage.tsx b/gui/src/renderer/containers/AccountPage.tsx index a81676c48b..359d33b5c0 100644 --- a/gui/src/renderer/containers/AccountPage.tsx +++ b/gui/src/renderer/containers/AccountPage.tsx @@ -9,10 +9,6 @@ import accountActions from '../redux/account/actions'; import { IReduxState, ReduxDispatch } from '../redux/store'; const mapStateToProps = (state: IReduxState) => ({ - deviceName: state.account.deviceName, - accountToken: state.account.accountToken, - accountExpiry: state.account.expiry, - expiryLocale: state.userInterface.locale, isOffline: state.connection.isBlocked, }); const mapDispatchToProps = (dispatch: ReduxDispatch, props: IHistoryProps & IAppContext) => { diff --git a/gui/src/renderer/lib/will-exit.tsx b/gui/src/renderer/lib/will-exit.tsx new file mode 100644 index 0000000000..67ce4c5549 --- /dev/null +++ b/gui/src/renderer/lib/will-exit.tsx @@ -0,0 +1,13 @@ +import React, { useContext } from 'react'; + +// This context tells its subtree if it should stop rendering or not. This is useful during +// transitions, e.g. on log out, since data might be updated which makes the disappearing view +// update a lot during the transition. There's currently no support for unpausing, which can be +// added later if needed. +const willExitContext = React.createContext<boolean>(false); + +export const WillExit = willExitContext.Provider; + +export function useWillExit() { + return useContext(willExitContext); +} diff --git a/gui/src/renderer/redux/store.ts b/gui/src/renderer/redux/store.ts index 79700497c6..d0969dbf85 100644 --- a/gui/src/renderer/redux/store.ts +++ b/gui/src/renderer/redux/store.ts @@ -1,6 +1,8 @@ +import { useRef } from 'react'; import { useSelector as useReduxSelector } from 'react-redux'; import { combineReducers, compose, createStore, Dispatch } from 'redux'; +import { useWillExit } from '../lib/will-exit'; import accountActions, { AccountAction } from './account/actions'; import accountReducer, { IAccountReduxState } from './account/reducers'; import connectionActions, { ConnectionAction } from './connection/actions'; @@ -64,6 +66,16 @@ function composeEnhancers(): typeof compose { : compose(); } +// This hook adds type to state to make use simpler. It also prevents the state from update if the +// WillExit context value is true. export function useSelector<R>(fn: (state: IReduxState) => R): R { - return useReduxSelector(fn); + const value = useReduxSelector(fn); + const valueBeforeExit = useRef(value); + const willExit = useWillExit(); + + if (!willExit) { + valueBeforeExit.current = value; + } + + return valueBeforeExit.current; } |
