summaryrefslogtreecommitdiffhomepage
path: root/gui
diff options
context:
space:
mode:
Diffstat (limited to 'gui')
-rw-r--r--gui/src/main/daemon-rpc.ts16
-rw-r--r--gui/src/main/index.ts4
-rw-r--r--gui/src/renderer/app.tsx6
-rw-r--r--gui/src/renderer/components/Account.tsx92
-rw-r--r--gui/src/renderer/components/AccountStyles.tsx7
-rw-r--r--gui/src/renderer/components/TooManyDevices.tsx70
-rw-r--r--gui/src/renderer/containers/AccountPage.tsx9
-rw-r--r--gui/src/renderer/redux/account/actions.ts24
-rw-r--r--gui/src/renderer/redux/account/reducers.ts13
-rw-r--r--gui/src/shared/daemon-rpc-types.ts1
-rw-r--r--gui/src/shared/ipc-schema.ts1
-rw-r--r--gui/src/shared/string-helpers.ts4
12 files changed, 208 insertions, 39 deletions
diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts
index bb90d77d52..ff0c81a707 100644
--- a/gui/src/main/daemon-rpc.ts
+++ b/gui/src/main/daemon-rpc.ts
@@ -516,7 +516,7 @@ export class DaemonRpc {
accountToken,
);
- return response.toObject().devicesList;
+ return response.getDevicesList().map(convertFromDevice);
}
public async removeDevice(deviceRemoval: IDeviceRemoval): Promise<void> {
@@ -1376,16 +1376,26 @@ function convertFromDeviceEvent(deviceEvent: grpcTypes.DeviceEvent): IDeviceEven
}
function convertFromDeviceConfig(deviceConfig?: grpcTypes.DeviceConfig): DeviceConfig {
+ const device = deviceConfig?.getDevice();
return (
deviceConfig && {
accountToken: deviceConfig.getAccountToken(),
- device: deviceConfig.getDevice()?.toObject(),
+ device: device ? convertFromDevice(device) : undefined,
}
);
}
function convertFromDeviceRemoval(deviceRemoval: grpcTypes.RemoveDeviceEvent): Array<IDevice> {
- return deviceRemoval.getNewDeviceListList().map((device) => device.toObject());
+ return deviceRemoval.getNewDeviceListList().map(convertFromDevice);
+}
+
+function convertFromDevice(device: grpcTypes.Device): IDevice {
+ const asObject = device.toObject();
+
+ return {
+ ...asObject,
+ ports: asObject.portsList.map((port) => port.id),
+ };
}
function ensureExists<T>(value: T | undefined, errorMessage: string): T {
diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts
index 9bb988a199..b1f7f44b8a 100644
--- a/gui/src/main/index.ts
+++ b/gui/src/main/index.ts
@@ -1312,6 +1312,10 @@ class ApplicationMain {
});
IpcMainEventChannel.account.handleUpdateData(() => this.updateAccountData());
+ IpcMainEventChannel.account.handleGetDevice(async () => {
+ const deviceConfig = await this.daemonRpc.getDevice();
+ return deviceConfig?.device;
+ });
IpcMainEventChannel.account.handleListDevices((accountToken: AccountToken) => {
return this.daemonRpc.listDevices(accountToken);
});
diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx
index 47a58fc4dd..9f3bb70a04 100644
--- a/gui/src/renderer/app.tsx
+++ b/gui/src/renderer/app.tsx
@@ -334,6 +334,10 @@ export default class AppRenderer {
IpcRendererEventChannel.account.updateData();
}
+ public getDevice = (): Promise<IDevice | undefined> => {
+ return IpcRendererEventChannel.account.getDevice();
+ };
+
public fetchDevices = async (accountToken: AccountToken): Promise<Array<IDevice>> => {
const devices = await IpcRendererEventChannel.account.listDevices(accountToken);
this.reduxActions.account.updateDevices(devices);
@@ -783,7 +787,7 @@ export default class AppRenderer {
if (oldAccount && !newAccount) {
this.loginScheduler.cancel();
- if (newDeviceEvent.remote) {
+ if (!this.reduxStore.getState().account.loggingOut && newDeviceEvent.remote) {
reduxAccount.deviceRevoked();
} else {
reduxAccount.loggedOut();
diff --git a/gui/src/renderer/components/Account.tsx b/gui/src/renderer/components/Account.tsx
index a612f2a07e..4a84e3c110 100644
--- a/gui/src/renderer/components/Account.tsx
+++ b/gui/src/renderer/components/Account.tsx
@@ -10,6 +10,7 @@ import {
AccountRows,
AccountRowValue,
DeviceRowValue,
+ StyledSpinnerContainer,
StyledBuyCreditButton,
StyledContainer,
StyledRedeemVoucherButton,
@@ -18,10 +19,12 @@ import AccountTokenLabel from './AccountTokenLabel';
import * as AppButton from './AppButton';
import { AriaDescribed, AriaDescription, AriaDescriptionGroup } from './AriaGroup';
import { Layout } from './Layout';
+import { ModalAlert, ModalAlertType, ModalMessage } from './Modal';
import { NavigationBar, NavigationItems, TitleBarItem } from './NavigationBar';
import SettingsHeader, { HeaderTitle } from './SettingsHeader';
-import { AccountToken } from '../../shared/daemon-rpc-types';
+import { AccountToken, IDevice } from '../../shared/daemon-rpc-types';
+import ImageView from './ImageView';
import { BackAction } from './KeyboardNavigation';
interface IProps {
@@ -30,13 +33,22 @@ interface IProps {
accountExpiry?: string;
expiryLocale: string;
isOffline: boolean;
+ prepareLogout: () => void;
+ cancelLogout: () => void;
onLogout: () => void;
onClose: () => void;
onBuyMore: () => Promise<void>;
updateAccountData: () => void;
+ getDevice: () => Promise<IDevice | undefined>;
}
-export default class Account extends React.Component<IProps> {
+interface IState {
+ logoutDialogState: 'hidden' | 'checking-ports' | 'confirm';
+}
+
+export default class Account extends React.Component<IProps, IState> {
+ public state: IState = { logoutDialogState: 'hidden' };
+
public componentDidMount() {
this.props.updateAccountData();
}
@@ -65,7 +77,7 @@ export default class Account extends React.Component<IProps> {
<AccountRows>
<AccountRow>
<AccountRowLabel>
- {messages.pgettext('account-view', 'Device name')}
+ {messages.pgettext('device-management', 'Device name')}
</AccountRowLabel>
<DeviceRowValue>{this.props.deviceName}</DeviceRowValue>
</AccountRow>
@@ -114,16 +126,88 @@ export default class Account extends React.Component<IProps> {
<StyledRedeemVoucherButton />
- <AppButton.RedButton onClick={this.props.onLogout}>
+ <AppButton.RedButton onClick={this.onTryLogout}>
{messages.pgettext('account-view', 'Log out')}
</AppButton.RedButton>
</AccountFooter>
</AccountContainer>
</StyledContainer>
+
+ {this.renderLogoutDialog()}
</Layout>
</BackAction>
);
}
+
+ private renderLogoutDialog() {
+ const modalType =
+ this.state.logoutDialogState === 'checking-ports' ? undefined : ModalAlertType.warning;
+
+ const message =
+ this.state.logoutDialogState === 'checking-ports' ? (
+ <StyledSpinnerContainer>
+ <ImageView source="icon-spinner" width={60} height={60} />
+ </StyledSpinnerContainer>
+ ) : (
+ <ModalMessage>
+ {
+ // TRANSLATORS: This is is a further explanation of what happens when logging out.
+ messages.pgettext(
+ 'device-management',
+ 'The ports forwarded to this device will be deleted if you log out.',
+ )
+ }
+ </ModalMessage>
+ );
+
+ const buttons =
+ this.state.logoutDialogState === 'checking-ports'
+ ? []
+ : [
+ <AppButton.RedButton key="logout" onClick={this.props.onLogout}>
+ {
+ // TRANSLATORS: Confirmation button when logging out
+ messages.pgettext('device-management', 'Log out anyway')
+ }
+ </AppButton.RedButton>,
+ <AppButton.BlueButton key="back" onClick={this.cancelLogout}>
+ {messages.gettext('Back')}
+ </AppButton.BlueButton>,
+ ];
+
+ return (
+ <ModalAlert
+ isOpen={this.state.logoutDialogState !== 'hidden'}
+ type={modalType}
+ buttons={buttons}>
+ {message}
+ </ModalAlert>
+ );
+ }
+
+ private onTryLogout = async () => {
+ this.setState({ logoutDialogState: 'checking-ports' });
+ this.props.prepareLogout();
+
+ const device = await this.props.getDevice();
+ if (device === undefined) {
+ this.onHideLogoutConfirmationDialog();
+ } else if (device.ports !== undefined && device.ports.length > 0) {
+ this.setState({ logoutDialogState: 'confirm' });
+ } else {
+ this.props.onLogout();
+ this.onHideLogoutConfirmationDialog();
+ }
+ };
+
+ private cancelLogout = () => {
+ this.props.cancelLogout();
+ this.onHideLogoutConfirmationDialog();
+ };
+
+ private onHideLogoutConfirmationDialog = () => {
+ this.setState({ logoutDialogState: 'hidden' });
+ };
}
function FormattedAccountExpiry(props: { expiry?: string; locale: string }) {
diff --git a/gui/src/renderer/components/AccountStyles.tsx b/gui/src/renderer/components/AccountStyles.tsx
index 34ce600fde..549b0cda7d 100644
--- a/gui/src/renderer/components/AccountStyles.tsx
+++ b/gui/src/renderer/components/AccountStyles.tsx
@@ -52,6 +52,13 @@ export const AccountOutOfTime = styled(AccountRowValue)({
color: colors.red,
});
+export const StyledSpinnerContainer = styled.div({
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: '8px 0',
+});
+
export const AccountFooter = styled.div({
display: 'flex',
flexDirection: 'column',
diff --git a/gui/src/renderer/components/TooManyDevices.tsx b/gui/src/renderer/components/TooManyDevices.tsx
index 56ecbd99c1..b743dd2aec 100644
--- a/gui/src/renderer/components/TooManyDevices.tsx
+++ b/gui/src/renderer/components/TooManyDevices.tsx
@@ -4,10 +4,12 @@ import styled from 'styled-components';
import { colors } from '../../config.json';
import { IDevice } from '../../shared/daemon-rpc-types';
import { messages } from '../../shared/gettext';
+import { capitalizeEveryWord } from '../../shared/string-helpers';
import { useAppContext } from '../context';
import { transitions, useHistory } from '../lib/history';
import { RoutePath } from '../lib/routes';
import { useBoolean } from '../lib/utilityHooks';
+import { formatMarkdown } from '../markdown-formatter';
import { useSelector } from '../redux/store';
import * as AppButton from './AppButton';
import * as Cell from './cell';
@@ -192,6 +194,8 @@ function Device(props: IDeviceProps) {
setDeleting();
}, [props.onRemove, props.device.id, hideConfirmation, setDeleting]);
+ const capitalizedDeviceName = capitalizeEveryWord(props.device.name);
+
return (
<>
<StyledCellContainer>
@@ -213,36 +217,42 @@ function Device(props: IDeviceProps) {
/>
</StyledRemoveDeviceButton>
</StyledCellContainer>
- {confirmationVisible && (
- <ModalAlert
- type={ModalAlertType.warning}
- iconColor={colors.red}
- buttons={[
- <AppButton.RedButton key="remove" onClick={onRemove} disabled={deleting}>
- {
- // TRANSLATORS: Confirmation button when logging out other device.
- messages.pgettext('device-management', 'Yes, log out device')
- }
- </AppButton.RedButton>,
- <AppButton.BlueButton key="back" onClick={hideConfirmation} disabled={deleting}>
- {messages.gettext('Back')}
- </AppButton.BlueButton>,
- ]}
- close={hideConfirmation}>
- {deleting ? (
- <ImageView source="icon-spinner" />
- ) : (
- <>
- <ModalMessage>
- {sprintf(
+ <ModalAlert
+ isOpen={confirmationVisible}
+ type={ModalAlertType.warning}
+ iconColor={colors.red}
+ buttons={[
+ <AppButton.RedButton key="remove" onClick={onRemove} disabled={deleting}>
+ {
+ // TRANSLATORS: Confirmation button when logging out other device.
+ messages.pgettext('device-management', 'Yes, log out device')
+ }
+ </AppButton.RedButton>,
+ <AppButton.BlueButton key="back" onClick={hideConfirmation} disabled={deleting}>
+ {messages.gettext('Back')}
+ </AppButton.BlueButton>,
+ ]}
+ close={hideConfirmation}>
+ {deleting ? (
+ <ImageView source="icon-spinner" />
+ ) : (
+ <>
+ <ModalMessage>
+ {formatMarkdown(
+ sprintf(
// TRANSLATORS: Text displayed above button which logs out another device.
+ // TRANSLATORS: The text enclosed in "**" will appear bold.
+ // TRANSLATORS: Available placeholders:
+ // TRANSLATORS: %(deviceName)s - The name of the device to log out.
messages.pgettext(
'device-management',
- 'Are you sure you want to log out of %(deviceName)s?',
+ 'Are you sure you want to log out of **%(deviceName)s**?',
),
- { deviceName: props.device.name },
- )}
- </ModalMessage>
+ { deviceName: capitalizedDeviceName },
+ ),
+ )}
+ </ModalMessage>
+ {props.device.ports && props.device.ports.length > 0 && (
<ModalMessage>
{
// TRANSLATORS: Further information about consequences of logging out device.
@@ -252,10 +262,10 @@ function Device(props: IDeviceProps) {
)
}
</ModalMessage>
- </>
- )}
- </ModalAlert>
- )}
+ )}
+ </>
+ )}
+ </ModalAlert>
</>
);
}
diff --git a/gui/src/renderer/containers/AccountPage.tsx b/gui/src/renderer/containers/AccountPage.tsx
index a5251500d0..2309c614fb 100644
--- a/gui/src/renderer/containers/AccountPage.tsx
+++ b/gui/src/renderer/containers/AccountPage.tsx
@@ -1,10 +1,12 @@
import { connect } from 'react-redux';
+import { bindActionCreators } from 'redux';
import { links } from '../../config.json';
import Account from '../components/Account';
import withAppContext, { IAppContext } from '../context';
import { IHistoryProps, withHistory } from '../lib/history';
import { IReduxState, ReduxDispatch } from '../redux/store';
+import accountActions from '../redux/account/actions';
const mapStateToProps = (state: IReduxState) => ({
deviceName: state.account.deviceName,
@@ -13,8 +15,12 @@ const mapStateToProps = (state: IReduxState) => ({
expiryLocale: state.userInterface.locale,
isOffline: state.connection.isBlocked,
});
-const mapDispatchToProps = (_dispatch: ReduxDispatch, props: IHistoryProps & IAppContext) => {
+const mapDispatchToProps = (dispatch: ReduxDispatch, props: IHistoryProps & IAppContext) => {
+ const account = bindActionCreators(accountActions, dispatch);
+
return {
+ prepareLogout: () => account.prepareLogout(),
+ cancelLogout: () => account.cancelLogout(),
onLogout: () => {
void props.app.logout();
},
@@ -23,6 +29,7 @@ const mapDispatchToProps = (_dispatch: ReduxDispatch, props: IHistoryProps & IAp
},
onBuyMore: () => props.app.openLinkWithAuth(links.purchase),
updateAccountData: () => props.app.updateAccountData(),
+ getDevice: () => props.app.getDevice(),
};
};
diff --git a/gui/src/renderer/redux/account/actions.ts b/gui/src/renderer/redux/account/actions.ts
index 5bc3e84aa9..355acc4a35 100644
--- a/gui/src/renderer/redux/account/actions.ts
+++ b/gui/src/renderer/redux/account/actions.ts
@@ -21,6 +21,14 @@ interface ILoginTooManyDevicesAction {
error: Error;
}
+interface IPrepareLogoutAction {
+ type: 'PREPARE_LOG_OUT';
+}
+
+interface ICancelLogoutAction {
+ type: 'CANCEL_LOGOUT';
+}
+
interface ILoggedOutAction {
type: 'LOGGED_OUT';
}
@@ -78,6 +86,8 @@ export type AccountAction =
| ILoggedInAction
| ILoginFailedAction
| ILoginTooManyDevicesAction
+ | IPrepareLogoutAction
+ | ICancelLogoutAction
| ILoggedOutAction
| IResetLoginErrorAction
| IDeviceRevokedAction
@@ -119,6 +129,18 @@ function loginTooManyDevices(error: Error): ILoginTooManyDevicesAction {
};
}
+function prepareLogout(): IPrepareLogoutAction {
+ return {
+ type: 'PREPARE_LOG_OUT',
+ };
+}
+
+function cancelLogout(): ICancelLogoutAction {
+ return {
+ type: 'CANCEL_LOGOUT',
+ };
+}
+
function loggedOut(): ILoggedOutAction {
return {
type: 'LOGGED_OUT',
@@ -196,6 +218,8 @@ export default {
loggedIn,
loginFailed,
loginTooManyDevices,
+ prepareLogout,
+ cancelLogout,
loggedOut,
resetLoginError,
deviceRevoked,
diff --git a/gui/src/renderer/redux/account/reducers.ts b/gui/src/renderer/redux/account/reducers.ts
index 5be640e98b..00f2ef7bb5 100644
--- a/gui/src/renderer/redux/account/reducers.ts
+++ b/gui/src/renderer/redux/account/reducers.ts
@@ -13,6 +13,7 @@ export interface IAccountReduxState {
accountHistory?: AccountToken;
expiry?: string; // ISO8601
status: LoginState;
+ loggingOut: boolean;
}
const initialState: IAccountReduxState = {
@@ -22,6 +23,7 @@ const initialState: IAccountReduxState = {
accountHistory: undefined,
expiry: undefined,
status: { type: 'none', deviceRevoked: false },
+ loggingOut: false,
};
export default function (
@@ -53,12 +55,23 @@ export default function (
...state,
status: { type: 'too many devices', method: 'existing_account', error: action.error },
};
+ case 'PREPARE_LOG_OUT':
+ return {
+ ...state,
+ loggingOut: true,
+ };
+ case 'CANCEL_LOGOUT':
+ return {
+ ...state,
+ loggingOut: false,
+ };
case 'LOGGED_OUT':
return {
...state,
status: { type: 'none', deviceRevoked: false },
accountToken: undefined,
expiry: undefined,
+ loggingOut: false,
};
case 'RESET_LOGIN_ERROR':
return {
diff --git a/gui/src/shared/daemon-rpc-types.ts b/gui/src/shared/daemon-rpc-types.ts
index 7840a68d26..9f4e02baa0 100644
--- a/gui/src/shared/daemon-rpc-types.ts
+++ b/gui/src/shared/daemon-rpc-types.ts
@@ -337,6 +337,7 @@ export type DeviceConfig =
export interface IDevice {
id: string;
name: string;
+ ports?: Array<string>;
}
export interface IDeviceRemoval {
diff --git a/gui/src/shared/ipc-schema.ts b/gui/src/shared/ipc-schema.ts
index 62dd206c4c..89dd0b37b6 100644
--- a/gui/src/shared/ipc-schema.ts
+++ b/gui/src/shared/ipc-schema.ts
@@ -176,6 +176,7 @@ export const ipcSchema = {
getWwwAuthToken: invoke<void, string>(),
submitVoucher: invoke<string, VoucherResponse>(),
updateData: send<void>(),
+ getDevice: invoke<void, IDevice | undefined>(),
listDevices: invoke<AccountToken, Array<IDevice>>(),
removeDevice: invoke<IDeviceRemoval, void>(),
},
diff --git a/gui/src/shared/string-helpers.ts b/gui/src/shared/string-helpers.ts
index 30a2ac9d58..c69ebddfcf 100644
--- a/gui/src/shared/string-helpers.ts
+++ b/gui/src/shared/string-helpers.ts
@@ -1,3 +1,7 @@
export function capitalize(inputString: string): string {
return inputString.charAt(0).toUpperCase() + inputString.slice(1);
}
+
+export function capitalizeEveryWord(inputString: string): string {
+ return inputString.split(' ').map(capitalize).join(' ');
+}