Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch to ViewTransition API #7720

Draft
wants to merge 21 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e421801
Fix always build `test-manager` binary
MarkusPettersson98 Mar 12, 2025
100586d
Implement transitions with ViewTransition API
raksooo Jan 30, 2025
346a274
Replace TransitionContainer with ViewTransition API
raksooo Jan 30, 2025
bb09820
Adjust components to new transition system
raksooo Jan 30, 2025
05441b7
Remove WillExit component and useWillExit hook
raksooo Oct 11, 2024
eed1dfc
Add max width to Lang and Layout
raksooo Feb 14, 2025
c162694
Add waitForRoute utility
tobias-jarvelov Feb 26, 2025
a3743b3
Refactor waitForNextRoute into a factory
tobias-jarvelov Feb 27, 2025
211a7e4
Add waitForNextRoute utility
tobias-jarvelov Feb 27, 2025
c6805dc
Remove waitForNoTransition
tobias-jarvelov Mar 4, 2025
98bee94
Replace waitForNavigation with waitForRoute in tests
tobias-jarvelov Mar 4, 2025
8ea08d6
Wait for initial route before starting each test suite
tobias-jarvelov Feb 27, 2025
3fb5209
Use narrower locators for buttons in installed settings test
tobias-jarvelov Feb 27, 2025
26a0113
Improve specificity of locators in tunnel-state expects
tobias-jarvelov Feb 27, 2025
2aa40e8
Ensure we only target one element with inIp locator
tobias-jarvelov Feb 27, 2025
04a9836
Remove test location.spec.ts
tobias-jarvelov Feb 27, 2025
ac7c756
FIXUP Narrower button
tobias-jarvelov Mar 12, 2025
a40e8a2
FIXME connection button locator
tobias-jarvelov Mar 12, 2025
4490747
FIXME remove testid check
tobias-jarvelov Mar 12, 2025
4382133
Ensure currentRoute only tries to execute javascript when window is f…
tobias-jarvelov Mar 12, 2025
9fedd57
FIXME FIXUP locator last()
tobias-jarvelov Mar 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions .github/workflows/desktop-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -184,15 +184,14 @@ jobs:
path: ./bin/mullvad-version
if-no-files-found: error

# This step should always be run because the `test-manager` binary is used to compile the
# result matrix at the end! If that functionality is ever split out from the `test-manager`,
# this step may be conditionally run.
build-test-manager-linux:
name: Build Test Manager
needs: prepare-linux
# Note: libssl-dev is installed on the test server, so build test-manager there for the sake of simplicity
runs-on: [self-hosted, desktop-test, Linux] # app-test-linux
if: |
!cancelled() &&
needs.prepare-matrices.outputs.linux_matrix != '[]' &&
needs.prepare-matrices.outputs.linux_matrix != ''
steps:
- name: Checkout repository
uses: actions/checkout@v4
Expand Down
11 changes: 11 additions & 0 deletions desktop/packages/mullvad-vpn/assets/css/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,14 @@ body {
transition-duration: 0ms !important;
}
}

::view-transition-image-pair(root) {
isolation: auto;
}

::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
display: block;
}
48 changes: 24 additions & 24 deletions desktop/packages/mullvad-vpn/src/renderer/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import MacOsScrollbarDetection from './components/MacOsScrollbarDetection';
import { ModalContainer } from './components/Modal';
import { AppContext } from './context';
import { Theme } from './lib/components';
import History, { ITransitionSpecification, transitions } from './lib/history';
import History, { TransitionType } from './lib/history';
import { loadTranslations } from './lib/load-translations';
import IpcOutput from './lib/logging';
import { RoutePath } from './lib/routes';
Expand Down Expand Up @@ -409,7 +409,7 @@ export default class AppRenderer {
actions.account.loginTooManyDevices();
this.loginState = 'too many devices';

this.history.reset(RoutePath.tooManyDevices, { transition: transitions.push });
this.history.reset(RoutePath.tooManyDevices, { transition: TransitionType.push });
} catch {
log.error('Failed to fetch device list');
actions.account.loginFailed('list-devices');
Expand All @@ -426,7 +426,7 @@ export default class AppRenderer {
this.loginState = 'none';
};

public logout = async (transition = transitions.dismiss) => {
public logout = async (transition = TransitionType.dismiss) => {
try {
this.history.reset(RoutePath.login, { transition });
await IpcRendererEventChannel.account.logout();
Expand All @@ -437,7 +437,7 @@ export default class AppRenderer {
};

public leaveRevokedDevice = async () => {
await this.logout(transitions.pop);
await this.logout(TransitionType.pop);
await this.disconnectTunnel();
};

Expand Down Expand Up @@ -718,38 +718,38 @@ export default class AppRenderer {
// First level contains the possible next locations and the second level contains the
// possible current locations.
const navigationTransitions: Partial<
Record<RoutePath, Partial<Record<RoutePath | '*', ITransitionSpecification>>>
Record<RoutePath, Partial<Record<RoutePath | '*', TransitionType>>>
> = {
[RoutePath.launch]: {
[RoutePath.login]: transitions.pop,
[RoutePath.main]: transitions.pop,
'*': transitions.dismiss,
[RoutePath.login]: TransitionType.pop,
[RoutePath.main]: TransitionType.pop,
'*': TransitionType.dismiss,
},
[RoutePath.login]: {
[RoutePath.launch]: transitions.push,
[RoutePath.main]: transitions.pop,
[RoutePath.deviceRevoked]: transitions.pop,
'*': transitions.dismiss,
[RoutePath.launch]: TransitionType.push,
[RoutePath.main]: TransitionType.pop,
[RoutePath.deviceRevoked]: TransitionType.pop,
'*': TransitionType.dismiss,
},
[RoutePath.main]: {
[RoutePath.launch]: transitions.push,
[RoutePath.login]: transitions.push,
[RoutePath.tooManyDevices]: transitions.push,
'*': transitions.dismiss,
[RoutePath.launch]: TransitionType.push,
[RoutePath.login]: TransitionType.push,
[RoutePath.tooManyDevices]: TransitionType.push,
'*': TransitionType.dismiss,
},
[RoutePath.expired]: {
[RoutePath.launch]: transitions.push,
[RoutePath.login]: transitions.push,
[RoutePath.tooManyDevices]: transitions.push,
'*': transitions.dismiss,
[RoutePath.launch]: TransitionType.push,
[RoutePath.login]: TransitionType.push,
[RoutePath.tooManyDevices]: TransitionType.push,
'*': TransitionType.dismiss,
},
[RoutePath.timeAdded]: {
[RoutePath.expired]: transitions.push,
[RoutePath.redeemVoucher]: transitions.push,
'*': transitions.dismiss,
[RoutePath.expired]: TransitionType.push,
[RoutePath.redeemVoucher]: TransitionType.push,
'*': TransitionType.dismiss,
},
[RoutePath.deviceRevoked]: {
'*': transitions.pop,
'*': TransitionType.pop,
},
};

Expand Down
105 changes: 41 additions & 64 deletions desktop/packages/mullvad-vpn/src/renderer/components/AppRouter.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { createRef, useCallback, useEffect, useState } from 'react';
import { useCallback, useRef } from 'react';
import { Route, Switch } from 'react-router';

import LoginPage from '../components/Login';
import SelectLocation from '../components/select-location/SelectLocationContainer';
import { useAppContext } from '../context';
import { ITransitionSpecification, transitions, useHistory } from '../lib/history';
import { RoutePath } from '../lib/routes';
import { useViewTransitions } from '../lib/transition-hooks';
import Account from './Account';
import ApiAccessMethods from './ApiAccessMethods';
import DaitaSettings from './DaitaSettings';
Expand Down Expand Up @@ -35,80 +34,58 @@ import Shadowsocks from './Shadowsocks';
import SplitTunnelingSettings from './SplitTunnelingSettings';
import Support from './Support';
import TooManyDevices from './TooManyDevices';
import TransitionContainer, { TransitionView } from './TransitionContainer';
import UdpOverTcp from './UdpOverTcp';
import UserInterfaceSettings from './UserInterfaceSettings';
import { AppInfoView, ChangelogView } from './views';
import VpnSettings from './VpnSettings';
import WireguardSettings from './WireguardSettings';

export default function AppRouter() {
const history = useHistory();
const [currentLocation, setCurrentLocation] = useState(history.location);
const [transition, setTransition] = useState<ITransitionSpecification>(transitions.none);
const { setNavigationHistory } = useAppContext();
const focusRef = createRef<IFocusHandle>();

useEffect(() => {
// React throttles updates, so it's impossible to capture the intermediate navigation without
// listening to the history directly.
const unobserveHistory = history.listen((location, _, transition) => {
setNavigationHistory(history.asObject);
setCurrentLocation(location);
setTransition(transition);
});

return () => {
unobserveHistory?.();
};
}, [history, setNavigationHistory]);

const focusRef = useRef<IFocusHandle>(null);
const onNavigation = useCallback(() => {
focusRef.current?.resetFocus();
}, [focusRef]);

const currentLocation = useViewTransitions(onNavigation);

return (
<Focus ref={focusRef}>
<TransitionContainer onTransitionEnd={onNavigation} {...transition}>
<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} />
<Route exact path={RoutePath.tooManyDevices} component={TooManyDevices} />
<Route exact path={RoutePath.deviceRevoked} component={DeviceRevokedView} />
<Route exact path={RoutePath.main} component={MainView} />
<Route exact path={RoutePath.expired} component={ExpiredAccountErrorView} />
<Route exact path={RoutePath.redeemVoucher} component={VoucherInput} />
<Route exact path={RoutePath.voucherSuccess} component={VoucherVerificationSuccess} />
<Route exact path={RoutePath.timeAdded} component={TimeAdded} />
<Route exact path={RoutePath.setupFinished} component={SetupFinished} />
<Route exact path={RoutePath.account} component={Account} />
<Route exact path={RoutePath.settings} component={Settings} />
<Route exact path={RoutePath.selectLanguage} component={SelectLanguage} />
<Route exact path={RoutePath.userInterfaceSettings} component={UserInterfaceSettings} />
<Route exact path={RoutePath.multihopSettings} component={MultihopSettings} />
<Route exact path={RoutePath.vpnSettings} component={VpnSettings} />
<Route exact path={RoutePath.wireguardSettings} component={WireguardSettings} />
<Route exact path={RoutePath.daitaSettings} component={DaitaSettings} />
<Route exact path={RoutePath.udpOverTcp} component={UdpOverTcp} />
<Route exact path={RoutePath.shadowsocks} component={Shadowsocks} />
<Route exact path={RoutePath.openVpnSettings} component={OpenVpnSettings} />
<Route exact path={RoutePath.splitTunneling} component={SplitTunnelingSettings} />
<Route exact path={RoutePath.apiAccessMethods} component={ApiAccessMethods} />
<Route exact path={RoutePath.settingsImport} component={SettingsImport} />
<Route exact path={RoutePath.settingsTextImport} component={SettingsTextImport} />
<Route exact path={RoutePath.editApiAccessMethods} component={EditApiAccessMethod} />
<Route exact path={RoutePath.support} component={Support} />
<Route exact path={RoutePath.problemReport} component={ProblemReport} />
<Route exact path={RoutePath.debug} component={Debug} />
<Route exact path={RoutePath.selectLocation} component={SelectLocation} />
<Route exact path={RoutePath.editCustomBridge} component={EditCustomBridge} />
<Route exact path={RoutePath.filter} component={Filter} />
<Route exact path={RoutePath.appInfo} component={AppInfoView} />
<Route exact path={RoutePath.changelog} component={ChangelogView} />
</Switch>
</TransitionView>
</TransitionContainer>
<Switch key={currentLocation.key} location={currentLocation}>
<Route exact path={RoutePath.launch} component={Launch} />
<Route exact path={RoutePath.login} component={LoginPage} />
<Route exact path={RoutePath.tooManyDevices} component={TooManyDevices} />
<Route exact path={RoutePath.deviceRevoked} component={DeviceRevokedView} />
<Route exact path={RoutePath.main} component={MainView} />
<Route exact path={RoutePath.expired} component={ExpiredAccountErrorView} />
<Route exact path={RoutePath.redeemVoucher} component={VoucherInput} />
<Route exact path={RoutePath.voucherSuccess} component={VoucherVerificationSuccess} />
<Route exact path={RoutePath.timeAdded} component={TimeAdded} />
<Route exact path={RoutePath.setupFinished} component={SetupFinished} />
<Route exact path={RoutePath.account} component={Account} />
<Route exact path={RoutePath.settings} component={Settings} />
<Route exact path={RoutePath.selectLanguage} component={SelectLanguage} />
<Route exact path={RoutePath.userInterfaceSettings} component={UserInterfaceSettings} />
<Route exact path={RoutePath.multihopSettings} component={MultihopSettings} />
<Route exact path={RoutePath.vpnSettings} component={VpnSettings} />
<Route exact path={RoutePath.wireguardSettings} component={WireguardSettings} />
<Route exact path={RoutePath.daitaSettings} component={DaitaSettings} />
<Route exact path={RoutePath.udpOverTcp} component={UdpOverTcp} />
<Route exact path={RoutePath.shadowsocks} component={Shadowsocks} />
<Route exact path={RoutePath.openVpnSettings} component={OpenVpnSettings} />
<Route exact path={RoutePath.splitTunneling} component={SplitTunnelingSettings} />
<Route exact path={RoutePath.apiAccessMethods} component={ApiAccessMethods} />
<Route exact path={RoutePath.settingsImport} component={SettingsImport} />
<Route exact path={RoutePath.settingsTextImport} component={SettingsTextImport} />
<Route exact path={RoutePath.editApiAccessMethods} component={EditApiAccessMethod} />
<Route exact path={RoutePath.support} component={Support} />
<Route exact path={RoutePath.problemReport} component={ProblemReport} />
<Route exact path={RoutePath.debug} component={Debug} />
<Route exact path={RoutePath.selectLocation} component={SelectLocation} />
<Route exact path={RoutePath.editCustomBridge} component={EditCustomBridge} />
<Route exact path={RoutePath.filter} component={Filter} />
<Route exact path={RoutePath.appInfo} component={AppInfoView} />
<Route exact path={RoutePath.changelog} component={ChangelogView} />
</Switch>
</Focus>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { useAppContext } from '../context';
import useActions from '../lib/actionsHook';
import { Flex, Icon } from '../lib/components';
import { Colors } from '../lib/foundations';
import { transitions, useHistory } from '../lib/history';
import { TransitionType, useHistory } from '../lib/history';
import { IconBadge } from '../lib/icon-badge';
import { generateRoutePath } from '../lib/routeHelpers';
import { RoutePath } from '../lib/routes';
Expand Down Expand Up @@ -282,7 +282,7 @@ function useFinishedCallback() {
accountSetupFinished();
}

history.reset(RoutePath.main, { transition: transitions.push });
history.reset(RoutePath.main, { transition: TransitionType.push });
}, [isNewAccount, accountSetupFinished, history]);

return callback;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useSelector } from '../redux/store';
const StyledLang = styled.div({
display: 'flex',
flex: '1',
maxWidth: '100%',
});

export default function Lang(props: PropsWithChildren) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import styled from 'styled-components';
import { messages } from '../../shared/gettext';
import { useAppContext } from '../context';
import { Colors } from '../lib/foundations';
import { transitions, useHistory } from '../lib/history';
import { TransitionType, useHistory } from '../lib/history';
import { RoutePath } from '../lib/routes';
import { useBoolean } from '../lib/utility-hooks';
import { useSelector } from '../redux/store';
Expand Down Expand Up @@ -75,7 +75,7 @@ function DefaultFooter() {

const openSendProblemReport = useCallback(() => {
hideDialog();
push(RoutePath.problemReport, { transition: transitions.show });
push(RoutePath.problemReport, { transition: TransitionType.show });
}, [hideDialog, push]);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const Layout = styled.div({
flexDirection: 'column',
flex: 1,
height: '100vh',
maxWidth: '100%',
});

export const SettingsContainer = styled(Container)({
Expand Down
3 changes: 2 additions & 1 deletion desktop/packages/mullvad-vpn/src/renderer/components/Map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { TunnelState } from '../../shared/daemon-rpc-types';
import log from '../../shared/logging';
import { useAppContext } from '../context';
import GlMap, { ConnectionState, Coordinate } from '../lib/3dmap';
import { getReduceMotion } from '../lib/functions';
import {
useCombinedRefs,
useEffectEvent,
Expand Down Expand Up @@ -44,7 +45,7 @@ export default function Map() {

const connectionState = getConnectionState(hasLocationValue, connection.status.state);

const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const reduceMotion = getReduceMotion();
const animate = !reduceMotion && animateMap;

return (
Expand Down
11 changes: 3 additions & 8 deletions desktop/packages/mullvad-vpn/src/renderer/components/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { Icon, IconProps, Spinner } from '../lib/components';
import { Colors } from '../lib/foundations';
import { IconBadge } from '../lib/icon-badge';
import { useEffectEvent } from '../lib/utility-hooks';
import { useWillExit } from '../lib/will-exit';
import * as AppButton from './AppButton';
import { measurements, normalText, tinyText } from './common-styles';
import CustomScrollbars from './CustomScrollbars';
Expand Down Expand Up @@ -185,20 +184,16 @@ export function ModalAlert(props: IModalAlertProps & { isOpen: boolean }) {
const activeModalContext = useContext(ActiveModalContext);
const [openState, setOpenState] = useState<OpenState>({ isClosing: false, wasOpen: isOpen });

const willExit = useWillExit();

// Modal shouldn't prepare for being opened again while view is disappearing.
const onTransitionEnd = useCallback(() => {
if (!willExit) {
setOpenState({ isClosing: false, wasOpen: isOpen });
}
}, [willExit, isOpen]);
setOpenState({ isClosing: false, wasOpen: isOpen });
}, [isOpen]);

const onOpenStateChange = useEffectEvent((isOpen: boolean) => {
setOpenState(({ isClosing, wasOpen }) => ({
isClosing: isClosing || (wasOpen && !isOpen),
// Unmounting the Modal during view transitions result in a visual glitch.
wasOpen: willExit ? wasOpen : isOpen,
wasOpen: isOpen,
}));
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { useAppContext } from '../context';
import useActions from '../lib/actionsHook';
import { Link } from '../lib/components';
import { Colors } from '../lib/foundations';
import { transitions, useHistory } from '../lib/history';
import { TransitionType, useHistory } from '../lib/history';
import { formatHtml } from '../lib/html-formatter';
import {
NewDeviceNotificationProvider,
Expand Down Expand Up @@ -211,7 +211,7 @@ function NotificationActionWrapper({

const goToProblemReport = useCallback(() => {
closeTroubleshootModal();
push(RoutePath.problemReport, { transition: transitions.show });
push(RoutePath.problemReport, { transition: TransitionType.show });
}, [closeTroubleshootModal, push]);

let actionComponent: React.ReactElement | undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useAppContext } from '../context';
import useActions from '../lib/actionsHook';
import { Flex, Icon, IconProps } from '../lib/components';
import { Colors, spacings } from '../lib/foundations';
import { transitions, useHistory } from '../lib/history';
import { TransitionType, useHistory } from '../lib/history';
import { RoutePath } from '../lib/routes';
import { useBoolean, useEffectEvent } from '../lib/utility-hooks';
import settingsImportActions from '../redux/settings-import/actions';
Expand Down Expand Up @@ -72,7 +72,7 @@ export default function SettingsImport() {
}, [clearAllRelayOverrides, hideClearDialog, setImportStatus]);

const navigateTextImport = useCallback(() => {
history.push(RoutePath.settingsTextImport, { transition: transitions.show });
history.push(RoutePath.settingsTextImport, { transition: TransitionType.show });
}, [history]);

const importFile = useCallback(async () => {
Expand Down
Loading
Loading