summaryrefslogtreecommitdiffhomepage
path: root/gui/src
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2023-03-08 17:19:13 +0100
committerOskar Nyberg <oskar@mullvad.net>2023-03-10 10:29:51 +0100
commit25fa6c63766f6da254ebc6d252214244ca116e0e (patch)
treeaba4873436bc4cab25bf08125430ac98d96c02cc /gui/src
parentafe0095af4b931217d7add807dffcd7527a92627 (diff)
downloadmullvadvpn-25fa6c63766f6da254ebc6d252214244ca116e0e.tar.xz
mullvadvpn-25fa6c63766f6da254ebc6d252214244ca116e0e.zip
Make transition queue smarter to avoid unnecessary transitions
Diffstat (limited to 'gui/src')
-rw-r--r--gui/src/main/index.ts1
-rw-r--r--gui/src/renderer/components/AppRouter.tsx2
-rw-r--r--gui/src/renderer/components/TransitionContainer.tsx131
3 files changed, 90 insertions, 44 deletions
diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts
index 36cdcfe06c..1e8214a277 100644
--- a/gui/src/main/index.ts
+++ b/gui/src/main/index.ts
@@ -452,6 +452,7 @@ class ApplicationMain
}
const wasConnected = this.daemonRpc.isConnected;
+ IpcMainEventChannel.navigation.notifyReset?.();
this.daemonRpc.disconnect();
this.onDaemonDisconnected(wasConnected);
};
diff --git a/gui/src/renderer/components/AppRouter.tsx b/gui/src/renderer/components/AppRouter.tsx
index e31066337a..cd6e07fbbd 100644
--- a/gui/src/renderer/components/AppRouter.tsx
+++ b/gui/src/renderer/components/AppRouter.tsx
@@ -59,7 +59,7 @@ export default function AppRouter() {
return (
<Focus ref={focusRef}>
<TransitionContainer onTransitionEnd={onNavigation} {...transition}>
- <TransitionView viewId={currentLocation.key || ''} routePath={history.location.pathname}>
+ <TransitionView routePath={history.location.pathname}>
<Switch key={currentLocation.key} location={currentLocation}>
<Route exact path={RoutePath.launch} component={Launch} />
<Route exact path={RoutePath.login} component={LoginPage} />
diff --git a/gui/src/renderer/components/TransitionContainer.tsx b/gui/src/renderer/components/TransitionContainer.tsx
index d213371d39..ce9900927f 100644
--- a/gui/src/renderer/components/TransitionContainer.tsx
+++ b/gui/src/renderer/components/TransitionContainer.tsx
@@ -5,7 +5,6 @@ import { ITransitionSpecification } from '../lib/history';
import { WillExit } from '../lib/will-exit';
interface ITransitioningViewProps {
- viewId: string;
routePath: string;
children?: React.ReactNode;
}
@@ -95,50 +94,18 @@ export default class TransitionContainer extends React.Component<IProps, IState>
};
private isCycling = false;
+ private isTransitioning = false;
- private currentContentRef = React.createRef<HTMLDivElement>();
- private nextContentRef = React.createRef<HTMLDivElement>();
+ private currentContentRef: React.MutableRefObject<HTMLDivElement | null> = React.createRef<HTMLDivElement>();
+ private nextContentRef: React.MutableRefObject<HTMLDivElement | null> = React.createRef<HTMLDivElement>();
// The item that should trigger the cycle to finish in onTransitionEnd
private transitioningItemRef?: React.RefObject<HTMLDivElement>;
- public static getDerivedStateFromProps(props: IProps, state: IState) {
- const candidate = props.children;
-
- if (candidate && state.currentItem) {
- // Synchronize updates to the last added child. Although the queue doesn't change, the child
- // itself might need to change. That's why the queue-/next item is replaced by it again after
- // calling `makeItem`.
- if (state.queuedItem?.view.props.viewId === candidate.props.viewId) {
- // Child is last item in queue. No change to the queue needed.
- return {
- queuedItem: TransitionContainer.makeItem(props),
- };
- } else if (
- !state.queuedItem &&
- state.nextItem?.view.props.viewId === candidate.props.viewId
- ) {
- // Child is next item, no change to the queue needed.
- return { nextItem: TransitionContainer.makeItem(props) };
- } else if (
- !state.queuedItem &&
- !state.nextItem &&
- state.currentItem.view.props.viewId === candidate.props.viewId
- ) {
- // Child is current item and there's no new child, no change to the queue needed.
- return { currentItem: TransitionContainer.makeItem(props) };
- } else {
- // Child is a new item and is added to the queue.
- return { queuedItem: TransitionContainer.makeItem(props) };
- }
- } else if (candidate && !state.currentItem) {
- // Child is set as current item if there's no item already.
- return { currentItem: TransitionContainer.makeItem(props) };
- } else {
- return null;
+ public componentDidUpdate(prevProps: IProps) {
+ if (this.props.children !== prevProps.children) {
+ this.updateStateFromProps();
}
- }
- public componentDidUpdate() {
if (
this.state.currentItemStyle &&
this.state.currentItemTransition &&
@@ -167,9 +134,9 @@ export default class TransitionContainer extends React.Component<IProps, IState>
return (
<StyledTransitionContainer disableUserInteraction={willExit}>
{this.state.currentItem && (
- <WillExit key={this.state.currentItem.view.props.viewId} value={willExit}>
+ <WillExit key={this.state.currentItem.view.props.routePath} value={willExit}>
<StyledTransitionContent
- ref={this.currentContentRef}
+ ref={this.setCurrentContentRef}
transition={this.state.currentItemStyle}
onTransitionEnd={this.onTransitionEnd}>
{this.state.currentItem.view}
@@ -178,9 +145,9 @@ export default class TransitionContainer extends React.Component<IProps, IState>
)}
{this.state.nextItem && (
- <WillExit key={this.state.nextItem.view.props.viewId} value={false}>
+ <WillExit key={this.state.nextItem.view.props.routePath} value={false}>
<StyledTransitionContent
- ref={this.nextContentRef}
+ ref={this.setNextContentRef}
transition={this.state.nextItemStyle}
onTransitionEnd={this.onTransitionEnd}>
{this.state.nextItem.view}
@@ -191,8 +158,86 @@ export default class TransitionContainer extends React.Component<IProps, IState>
);
}
+ private setCurrentContentRef = (element: HTMLDivElement) => {
+ this.currentContentRef.current?.removeEventListener('transitionstart', this.onTransitionStart);
+ this.currentContentRef.current = element;
+ this.currentContentRef.current?.addEventListener('transitionstart', this.onTransitionStart);
+ };
+
+ private setNextContentRef = (element: HTMLDivElement) => {
+ this.nextContentRef.current?.removeEventListener('transitionstart', this.onTransitionStart);
+ this.nextContentRef.current = element;
+ this.nextContentRef.current?.addEventListener('transitionstart', this.onTransitionStart);
+ };
+
+ private updateStateFromProps() {
+ const candidate = this.props.children;
+
+ if (candidate && this.state.currentItem) {
+ // Update currentItem, nextItem, queuedItem depending on which the candidate matches.
+ if (
+ !this.isTransitioning &&
+ this.state.currentItem.view.props.routePath === candidate.props.routePath
+ ) {
+ // There's no transition in progress and the newest candidate has the same path as the
+ // current. In this sitation the app should just remain in the same view.
+ this.setState(
+ {
+ currentItem: TransitionContainer.makeItem(this.props),
+ nextItem: undefined,
+ queuedItem: undefined,
+ currentItemStyle: undefined,
+ nextItemStyle: undefined,
+ currentItemTransition: undefined,
+ nextItemTransition: undefined,
+ },
+ () => (this.isCycling = false),
+ );
+ } else if (!this.isTransitioning && this.state.nextItem) {
+ // There's no transition in progress but there is a next item. Abort the transition and add
+ // the candidate to the queue. The app shouldn't start a transition if there is another view
+ // to queue.
+ this.setState(
+ {
+ nextItem: undefined,
+ queuedItem: TransitionContainer.makeItem(this.props),
+ currentItemStyle: undefined,
+ nextItemStyle: undefined,
+ currentItemTransition: undefined,
+ nextItemTransition: undefined,
+ },
+ () => (this.isCycling = false),
+ );
+ } else if (this.state.nextItem?.view.props.routePath === candidate.props.routePath) {
+ // There's an update to the item that is currently being transitioned to. Update that item
+ // and continue the transition.
+ this.setState({
+ nextItem: TransitionContainer.makeItem(this.props),
+ queuedItem: undefined,
+ });
+ } else {
+ // If none of the above, initiate a transition to the new item.
+ this.setState({ queuedItem: TransitionContainer.makeItem(this.props) });
+ }
+ } else if (candidate) {
+ // Child is set as current item if there's no item already.
+ this.setState({ currentItem: TransitionContainer.makeItem(this.props) });
+ }
+ }
+
+ private onTransitionStart = (event: TransitionEvent) => {
+ if (
+ this.isCycling &&
+ !this.isTransitioning &&
+ event.target === this.transitioningItemRef?.current
+ ) {
+ this.isTransitioning = true;
+ }
+ };
+
private onTransitionEnd = (event: React.TransitionEvent<HTMLDivElement>) => {
if (this.isCycling && event.target === this.transitioningItemRef?.current) {
+ this.isTransitioning = false;
this.transitioningItemRef = undefined;
this.makeNextItemCurrent(() => {
this.onFinishCycle();