diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2023-03-08 17:19:13 +0100 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2023-03-10 10:29:51 +0100 |
| commit | 25fa6c63766f6da254ebc6d252214244ca116e0e (patch) | |
| tree | aba4873436bc4cab25bf08125430ac98d96c02cc /gui/src | |
| parent | afe0095af4b931217d7add807dffcd7527a92627 (diff) | |
| download | mullvadvpn-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.ts | 1 | ||||
| -rw-r--r-- | gui/src/renderer/components/AppRouter.tsx | 2 | ||||
| -rw-r--r-- | gui/src/renderer/components/TransitionContainer.tsx | 131 |
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(); |
