summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2022-09-09 10:32:50 +0200
committerOskar Nyberg <oskar@mullvad.net>2022-09-09 10:32:50 +0200
commit7abc85f427c10d72e6ad3fd3a1d1369430acf570 (patch)
tree6c9cc0265866e85e5652f828fc9e35dc2ecdc064
parent65bc519d7271b3bef32dd50cf7b8d41d42f07389 (diff)
parent83d5938d6c228976b05f73f6b56c725e2069586f (diff)
downloadmullvadvpn-7abc85f427c10d72e6ad3fd3a1d1369430acf570.tar.xz
mullvadvpn-7abc85f427c10d72e6ad3fd3a1d1369430acf570.zip
Merge branch 'prevent-redux-updates-during-transition'
-rw-r--r--gui/src/renderer/app.tsx3
-rw-r--r--gui/src/renderer/components/Account.tsx45
-rw-r--r--gui/src/renderer/components/Modal.tsx17
-rw-r--r--gui/src/renderer/components/TransitionContainer.tsx36
-rw-r--r--gui/src/renderer/containers/AccountPage.tsx4
-rw-r--r--gui/src/renderer/lib/will-exit.tsx13
-rw-r--r--gui/src/renderer/redux/store.ts14
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;
}