diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2023-06-14 10:25:32 +0200 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2023-06-14 10:25:32 +0200 |
| commit | 28069e86a364aa7f8e7fad51924ad3e0345e13d3 (patch) | |
| tree | 95d4f7b21a659b93f34c45ef1718f81a7ab8a8e5 /gui/src/renderer/components | |
| parent | 499e7516c4537c4e8fa6983c1e6698df32f1e9de (diff) | |
| parent | 0aadb762da306adc7e305388496a384fdb2c4181 (diff) | |
| download | mullvadvpn-28069e86a364aa7f8e7fad51924ad3e0345e13d3.tar.xz mullvadvpn-28069e86a364aa7f8e7fad51924ad3e0345e13d3.zip | |
Merge branch 'device-improvements'
Diffstat (limited to 'gui/src/renderer/components')
| -rw-r--r-- | gui/src/renderer/components/Account.tsx | 9 | ||||
| -rw-r--r-- | gui/src/renderer/components/AccountStyles.tsx | 5 | ||||
| -rw-r--r-- | gui/src/renderer/components/DeviceInfoButton.tsx | 57 | ||||
| -rw-r--r-- | gui/src/renderer/components/ExpiredAccountErrorView.tsx | 19 | ||||
| -rw-r--r-- | gui/src/renderer/components/ExpiredAccountErrorViewStyles.tsx | 8 | ||||
| -rw-r--r-- | gui/src/renderer/components/InfoButton.tsx | 3 | ||||
| -rw-r--r-- | gui/src/renderer/components/NotificationArea.tsx | 29 | ||||
| -rw-r--r-- | gui/src/renderer/components/NotificationBanner.tsx | 37 |
8 files changed, 141 insertions, 26 deletions
diff --git a/gui/src/renderer/components/Account.tsx b/gui/src/renderer/components/Account.tsx index 2841e4811c..bd3bafbafc 100644 --- a/gui/src/renderer/components/Account.tsx +++ b/gui/src/renderer/components/Account.tsx @@ -25,11 +25,13 @@ import { AccountRows, AccountRowValue, DeviceRowValue, + StyledDeviceNameRow, StyledSpinnerContainer, } from './AccountStyles'; import AccountTokenLabel from './AccountTokenLabel'; import * as AppButton from './AppButton'; import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from './AriaGroup'; +import DeviceInfoButton from './DeviceInfoButton'; import ImageView from './ImageView'; import { BackAction } from './KeyboardNavigation'; import { Footer, Layout, SettingsContainer } from './Layout'; @@ -185,7 +187,12 @@ function LogoutDialog() { function DeviceNameRow() { const deviceName = useSelector((state) => state.account.deviceName); - return <DeviceRowValue>{deviceName}</DeviceRowValue>; + return ( + <StyledDeviceNameRow> + <DeviceRowValue>{deviceName}</DeviceRowValue> + <DeviceInfoButton /> + </StyledDeviceNameRow> + ); } function AccountNumberRow() { diff --git a/gui/src/renderer/components/AccountStyles.tsx b/gui/src/renderer/components/AccountStyles.tsx index 5cb934134a..706abb1bdf 100644 --- a/gui/src/renderer/components/AccountStyles.tsx +++ b/gui/src/renderer/components/AccountStyles.tsx @@ -50,3 +50,8 @@ export const StyledSpinnerContainer = styled.div({ justifyContent: 'center', padding: '8px 0', }); + +export const StyledDeviceNameRow = styled.div({ + display: 'flex', + flexDirection: 'row', +}); diff --git a/gui/src/renderer/components/DeviceInfoButton.tsx b/gui/src/renderer/components/DeviceInfoButton.tsx new file mode 100644 index 0000000000..4839866972 --- /dev/null +++ b/gui/src/renderer/components/DeviceInfoButton.tsx @@ -0,0 +1,57 @@ +import styled from 'styled-components'; + +import { messages } from '../../shared/gettext'; +import { useBoolean } from '../lib/utilityHooks'; +import * as AppButton from './AppButton'; +import { InfoIcon } from './InfoButton'; +import { ModalAlert, ModalAlertType, ModalMessage } from './Modal'; + +const StyledInfoButton = styled.button({ + margin: '0 0 0 10px', + borderWidth: 0, + padding: 0, + cursor: 'default', + backgroundColor: 'transparent', +}); + +export default function DeviceInfoButton() { + const [deviceHelpVisible, showDeviceHelp, hideDeviceHelp] = useBoolean(); + + return ( + <> + <StyledInfoButton + onClick={showDeviceHelp} + aria-label={messages.pgettext('accessibility', 'More information')}> + <InfoIcon size={16} /> + </StyledInfoButton> + <ModalAlert + isOpen={deviceHelpVisible} + type={ModalAlertType.info} + buttons={[ + <AppButton.BlueButton key="back" onClick={hideDeviceHelp}> + {messages.gettext('Got it!')} + </AppButton.BlueButton>, + ]} + close={hideDeviceHelp}> + <ModalMessage> + {messages.pgettext( + 'device-management', + 'This is the name assigned to the device. Each device logged in on a Mullvad account gets a unique name that helps you identify it when you manage your devices in the app or on the website.', + )} + </ModalMessage> + <ModalMessage> + {messages.pgettext( + 'device-management', + 'You can have up to 5 devices logged in on one Mullvad account.', + )} + </ModalMessage> + <ModalMessage> + {messages.pgettext( + 'device-management', + 'If you log out, the device and the device name is removed. When you log back in again, the device will get a new name.', + )} + </ModalMessage> + </ModalAlert> + </> + ); +} diff --git a/gui/src/renderer/components/ExpiredAccountErrorView.tsx b/gui/src/renderer/components/ExpiredAccountErrorView.tsx index d5af378cca..3049ba8075 100644 --- a/gui/src/renderer/components/ExpiredAccountErrorView.tsx +++ b/gui/src/renderer/components/ExpiredAccountErrorView.tsx @@ -4,6 +4,7 @@ import { sprintf } from 'sprintf-js'; import { links } from '../../config.json'; import { messages } from '../../shared/gettext'; import log from '../../shared/logging'; +import { capitalizeEveryWord } from '../../shared/string-helpers'; import { useAppContext } from '../context'; import { useHistory } from '../lib/history'; import { RoutePath } from '../lib/routes'; @@ -11,6 +12,7 @@ import { useSelector } from '../redux/store'; import * as AppButton from './AppButton'; import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from './AriaGroup'; import * as Cell from './cell'; +import DeviceInfoButton from './DeviceInfoButton'; import { StyledAccountTokenContainer, StyledAccountTokenLabel, @@ -18,6 +20,7 @@ import { StyledBody, StyledContainer, StyledCustomScrollbars, + StyledDeviceLabel, StyledHeader, StyledMessage, StyledModalCellContainer, @@ -117,6 +120,22 @@ function WelcomeView() { </StyledAccountTokenContainer> </StyledAccountTokenMessage> + <StyledDeviceLabel> + <span> + {sprintf( + // TRANSLATORS: A label that will display the newly created device name to inform the user + // TRANSLATORS: about it. + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(deviceName)s - The name of the current device + messages.pgettext('device-management', 'Device name: %(deviceName)s'), + { + deviceName: capitalizeEveryWord(account.deviceName ?? ''), + }, + )} + </span> + <DeviceInfoButton /> + </StyledDeviceLabel> + <StyledMessage> {sprintf('%(introduction)s %(recoveryMessage)s', { introduction: messages.pgettext( diff --git a/gui/src/renderer/components/ExpiredAccountErrorViewStyles.tsx b/gui/src/renderer/components/ExpiredAccountErrorViewStyles.tsx index 17c02cfc3c..753ecdacef 100644 --- a/gui/src/renderer/components/ExpiredAccountErrorViewStyles.tsx +++ b/gui/src/renderer/components/ExpiredAccountErrorViewStyles.tsx @@ -69,3 +69,11 @@ export const StyledAccountTokenContainer = styled.div({ height: '50px', alignItems: 'center', }); + +export const StyledDeviceLabel = styled.span(tinyText, { + display: 'flex', + alignItems: 'middle', + lineHeight: '20px', + marginBottom: '18px', + color: colors.white, +}); diff --git a/gui/src/renderer/components/InfoButton.tsx b/gui/src/renderer/components/InfoButton.tsx index 52d92e18d6..302b043ef4 100644 --- a/gui/src/renderer/components/InfoButton.tsx +++ b/gui/src/renderer/components/InfoButton.tsx @@ -17,13 +17,14 @@ const StyledInfoButton = styled.button({ interface IInfoIconProps { className?: string; + size?: number; } export function InfoIcon(props: IInfoIconProps) { return ( <ImageView source="icon-info" - width={18} + width={props.size ?? 18} tintColor={colors.white} tintHoverColor={colors.white80} className={props.className} diff --git a/gui/src/renderer/components/NotificationArea.tsx b/gui/src/renderer/components/NotificationArea.tsx index 4923bacc7c..79fb3ef17c 100644 --- a/gui/src/renderer/components/NotificationArea.tsx +++ b/gui/src/renderer/components/NotificationArea.tsx @@ -5,6 +5,7 @@ import styled from 'styled-components'; import { colors } from '../../config.json'; import { messages } from '../../shared/gettext'; import log from '../../shared/logging'; +import { NewDeviceNotificationProvider } from '../../shared/notifications/new-device'; import { BlockWhenDisconnectedNotificationProvider, CloseToAccountExpiryNotificationProvider, @@ -19,14 +20,18 @@ import { UpdateAvailableNotificationProvider, } from '../../shared/notifications/notification'; import { useAppContext } from '../context'; +import useActions from '../lib/actionsHook'; import { transitions, useHistory } from '../lib/history'; +import { formatHtml } from '../lib/html-formatter'; import { RoutePath } from '../lib/routes'; +import accountActions from '../redux/account/actions'; import { IReduxState } from '../redux/store'; import * as AppButton from './AppButton'; import { ModalAlert, ModalAlertType, ModalMessage } from './Modal'; import { NotificationActions, NotificationBanner, + NotificationCloseAction, NotificationContent, NotificationIndicator, NotificationOpenLinkAction, @@ -40,7 +45,7 @@ interface IProps { } export default function NotificationArea(props: IProps) { - const accountExpiry = useSelector((state: IReduxState) => state.account.expiry); + const account = useSelector((state: IReduxState) => state.account); const locale = useSelector((state: IReduxState) => state.userInterface.locale); const tunnelState = useSelector((state: IReduxState) => state.connection.status); const version = useSelector((state: IReduxState) => state.version); @@ -52,6 +57,8 @@ export default function NotificationArea(props: IProps) { state.settings.splitTunneling && state.settings.splitTunnelingApplications.length > 0, ); + const { hideNewDeviceBanner } = useActions(accountActions); + const notificationProviders: InAppNotificationProvider[] = [ new ConnectingNotificationProvider({ tunnelState }), new ReconnectingNotificationProvider(tunnelState), @@ -65,13 +72,20 @@ export default function NotificationArea(props: IProps) { new UnsupportedVersionNotificationProvider(version), ]; - if (accountExpiry) { + if (account.expiry) { notificationProviders.push( - new CloseToAccountExpiryNotificationProvider({ accountExpiry, locale }), + new CloseToAccountExpiryNotificationProvider({ accountExpiry: account.expiry, locale }), ); } - notificationProviders.push(new UpdateAvailableNotificationProvider(version)); + notificationProviders.push( + new NewDeviceNotificationProvider({ + shouldDisplay: account.status.type === 'ok' && account.status.newDeviceBanner, + deviceName: account.deviceName ?? '', + close: hideNewDeviceBanner, + }), + new UpdateAvailableNotificationProvider(version), + ); const notificationProvider = notificationProviders.find((notification) => notification.mayDisplay(), @@ -86,7 +100,7 @@ export default function NotificationArea(props: IProps) { <NotificationIndicator type={notification.indicator} /> <NotificationContent role="status" aria-live="polite"> <NotificationTitle>{notification.title}</NotificationTitle> - <NotificationSubtitle>{notification.subtitle}</NotificationSubtitle> + <NotificationSubtitle>{formatHtml(notification.subtitle ?? '')}</NotificationSubtitle> </NotificationContent> {notification.action && <NotificationActionWrapper action={notification.action} />} </NotificationBanner> @@ -128,6 +142,9 @@ function NotificationActionWrapper(props: INotificationActionWrapperProps) { case 'troubleshoot-dialog': setTroubleshootInfo(props.action.troubleshoot); break; + case 'close': + props.action.close(); + break; } } @@ -154,6 +171,8 @@ function NotificationActionWrapper(props: INotificationActionWrapperProps) { </> ); break; + case 'close': + actionComponent = <NotificationCloseAction onClick={handleClick} />; } } diff --git a/gui/src/renderer/components/NotificationBanner.tsx b/gui/src/renderer/components/NotificationBanner.tsx index 4f20d315c8..18e18ce4f5 100644 --- a/gui/src/renderer/components/NotificationBanner.tsx +++ b/gui/src/renderer/components/NotificationBanner.tsx @@ -35,29 +35,23 @@ export const NotificationActionButton = styled(AppButton.SimpleButton)({ border: 'none', }); -export const NotificationOpenLinkActionIcon = styled(ImageView)({ - [NotificationActionButton + ':hover &']: { +export const NotificationActionButtonInner = styled(ImageView)({ + [NotificationActionButton + ':hover &&']: { backgroundColor: colors.white80, }, }); -export const NotificationTroubleshootDialogActionIcon = styled(ImageView)({ - [NotificationActionButton + ':hover &']: { - backgroundColor: colors.white80, - }, -}); - -interface INotifcationOpenLinkActionProps { +interface NotificationActionProps { onClick: () => Promise<void>; } -export function NotificationOpenLinkAction(props: INotifcationOpenLinkActionProps) { +export function NotificationOpenLinkAction(props: NotificationActionProps) { return ( <AppButton.BlockingButton onClick={props.onClick}> <NotificationActionButton aria-describedby={NOTIFICATION_AREA_ID} aria-label={messages.gettext('Open URL')}> - <NotificationOpenLinkActionIcon + <NotificationActionButtonInner height={12} width={12} tintColor={colors.white60} @@ -68,19 +62,13 @@ export function NotificationOpenLinkAction(props: INotifcationOpenLinkActionProp ); } -interface INotifcationTroubleshootDialogActionProps { - onClick: () => Promise<void>; -} - -export function NotificationTroubleshootDialogAction( - props: INotifcationTroubleshootDialogActionProps, -) { +export function NotificationTroubleshootDialogAction(props: NotificationActionProps) { return ( <NotificationActionButton aria-describedby={NOTIFICATION_AREA_ID} aria-label={messages.gettext('Troubleshoot')} onClick={props.onClick}> - <NotificationOpenLinkActionIcon + <NotificationActionButtonInner height={12} width={12} tintColor={colors.white60} @@ -90,6 +78,17 @@ export function NotificationTroubleshootDialogAction( ); } +export function NotificationCloseAction(props: NotificationActionProps) { + return ( + <NotificationActionButton + aria-describedby={NOTIFICATION_AREA_ID} + aria-label={messages.pgettext('accessibility', 'Close notification')} + onClick={props.onClick}> + <NotificationActionButtonInner source="icon-close" width={16} tintColor={colors.white60} /> + </NotificationActionButton> + ); +} + export const NotificationContent = styled.div.attrs({ id: NOTIFICATION_AREA_ID })({ display: 'flex', flexDirection: 'column', |
