diff options
Diffstat (limited to 'gui/src/renderer')
| -rw-r--r-- | gui/src/renderer/components/NotificationArea.tsx | 29 | ||||
| -rw-r--r-- | gui/src/renderer/components/NotificationBanner.tsx | 37 | ||||
| -rw-r--r-- | gui/src/renderer/redux/account/actions.ts | 10 | ||||
| -rw-r--r-- | gui/src/renderer/redux/account/reducers.ts | 18 |
4 files changed, 66 insertions, 28 deletions
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', diff --git a/gui/src/renderer/redux/account/actions.ts b/gui/src/renderer/redux/account/actions.ts index 61fc09d981..ece9719952 100644 --- a/gui/src/renderer/redux/account/actions.ts +++ b/gui/src/renderer/redux/account/actions.ts @@ -60,6 +60,10 @@ interface IAccountSetupFinished { type: 'ACCOUNT_SETUP_FINISHED'; } +interface IHideNewDeviceBanner { + type: 'HIDE_NEW_DEVICE_BANNER'; +} + interface IUpdateAccountTokenAction { type: 'UPDATE_ACCOUNT_TOKEN'; accountToken: AccountToken; @@ -94,6 +98,7 @@ export type AccountAction = | ICreateAccountFailed | IAccountCreated | IAccountSetupFinished + | IHideNewDeviceBanner | IUpdateAccountTokenAction | IUpdateAccountHistoryAction | IUpdateAccountExpiryAction @@ -187,6 +192,10 @@ function accountSetupFinished(): IAccountSetupFinished { return { type: 'ACCOUNT_SETUP_FINISHED' }; } +function hideNewDeviceBanner(): IHideNewDeviceBanner { + return { type: 'HIDE_NEW_DEVICE_BANNER' }; +} + function updateAccountToken(accountToken: AccountToken): IUpdateAccountTokenAction { return { type: 'UPDATE_ACCOUNT_TOKEN', @@ -229,6 +238,7 @@ export default { createAccountFailed, accountCreated, accountSetupFinished, + hideNewDeviceBanner, updateAccountToken, updateAccountHistory, updateAccountExpiry, diff --git a/gui/src/renderer/redux/account/reducers.ts b/gui/src/renderer/redux/account/reducers.ts index a78a777c0a..6f9e558b03 100644 --- a/gui/src/renderer/redux/account/reducers.ts +++ b/gui/src/renderer/redux/account/reducers.ts @@ -4,7 +4,8 @@ import { ReduxAction } from '../store'; type LoginMethod = 'existing_account' | 'new_account'; export type LoginState = | { type: 'none'; deviceRevoked: boolean } - | { type: 'logging in' | 'ok'; method: LoginMethod } + | { type: 'logging in'; method: LoginMethod } + | { type: 'ok'; method: LoginMethod; newDeviceBanner: boolean } | { type: 'too many devices'; method: LoginMethod } | { type: 'failed'; method: 'existing_account'; error: AccountDataError['error'] } | { type: 'failed'; method: 'new_account'; error: Error }; @@ -42,7 +43,7 @@ export default function ( case 'LOGGED_IN': return { ...state, - status: { type: 'ok', method: 'existing_account' }, + status: { type: 'ok', method: 'existing_account', newDeviceBanner: false }, accountToken: action.accountToken, deviceName: action.deviceName, }; @@ -97,7 +98,7 @@ export default function ( case 'ACCOUNT_CREATED': return { ...state, - status: { type: 'ok', method: 'new_account' }, + status: { type: 'ok', method: 'new_account', newDeviceBanner: true }, accountToken: action.accountToken, deviceName: action.deviceName, expiry: action.expiry, @@ -105,7 +106,16 @@ export default function ( case 'ACCOUNT_SETUP_FINISHED': return { ...state, - status: { type: 'ok', method: 'existing_account' }, + status: { type: 'ok', method: 'existing_account', newDeviceBanner: true }, + }; + case 'HIDE_NEW_DEVICE_BANNER': + if (state.status.type !== 'ok') { + return state; + } + + return { + ...state, + status: { ...state.status, newDeviceBanner: false }, }; case 'UPDATE_ACCOUNT_TOKEN': return { |
