add back link device in settings in a dialog

This commit is contained in:
Audric Ackermann 2020-01-17 17:49:21 +11:00
parent 29da7c2d53
commit 4d950f859b
14 changed files with 364 additions and 267 deletions

View File

@ -979,6 +979,9 @@
"allowPairing": {
"message": "Allow Pairing"
},
"allowPairingWithDevice": {
"message": "Allow pairing with this device?"
},
"provideDeviceAlias": {
"message": "Please provide an alias for this paired device"
},
@ -2601,7 +2604,7 @@
"message": "Devices"
},
"devicesSettingsDescription": {
"message": "Managed linked devices"
"message": "Manage linked devices"
},
"mnemonicEmpty": {
"message": "Seed is mandatory"
@ -2645,5 +2648,20 @@
},
"description": {
"message": "Description"
},
"filterReceivedRequests": {
"message": "Filter received requests"
},
"secretWords": {
"message": "Secret words:"
},
"pairingDevice": {
"message": "Pairing Device"
},
"gotPairingRequest": {
"message": "Got a pairing request"
},
"devicePairedSuccessfully": {
"message": "Device paired successfully"
}
}

View File

@ -216,28 +216,6 @@
showDevicePairingDialog() {
const dialog = new Whisper.DevicePairingDialogView();
dialog.on('startReceivingRequests', () => {
Whisper.events.on('devicePairingRequestReceived', pubKey =>
dialog.requestReceived(pubKey)
);
});
dialog.on('stopReceivingRequests', () => {
Whisper.events.off('devicePairingRequestReceived');
});
dialog.on('devicePairingRequestAccepted', (pubKey, cb) =>
Whisper.events.trigger('devicePairingRequestAccepted', pubKey, cb)
);
dialog.on('devicePairingRequestRejected', pubKey =>
Whisper.events.trigger('devicePairingRequestRejected', pubKey)
);
dialog.on('deviceUnpairingRequested', pubKey =>
Whisper.events.trigger('deviceUnpairingRequested', pubKey)
);
dialog.once('close', () => {
Whisper.events.off('devicePairingRequestReceived');
});
this.el.append(dialog.el);
},
showDevicePairingWordsDialog() {

View File

@ -576,14 +576,6 @@
h4 {
margin-top: 8px;
margin-bottom: 16px;
white-space: -moz-pre-wrap; /* Mozilla */
white-space: -hp-pre-wrap; /* HP printers */
white-space: -o-pre-wrap; /* Opera 7 */
white-space: -pre-wrap; /* Opera 4-6 */
white-space: pre-wrap; /* CSS 2.1 */
white-space: pre-line; /* CSS 3 (and 2.1 as well, actually) */
word-wrap: break-word; /* IE */
word-break: break-all;
}
}

View File

@ -677,17 +677,13 @@ label {
justify-content: flex-end;
.session-button {
margin-left: $session-margin-sm;
margin: $session-margin-xs;
}
&__center {
display: flex;
justify-content: center;
}
.session-button {
margin: 0 $session-margin-xs;
}
}
&__text-highlight {
@ -915,20 +911,23 @@ label {
&-header {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
background-color: $session-shade-6;
height: $main-view-header-height;
line-height: $main-view-header-height;
font-weight: bold;
font-size: 18px;
&-title {
line-height: $main-view-header-height;
font-weight: bold;
font-size: 18px;
text-align: center;
flex-grow: 1;
}
.session-button,
.session-icon-button {
display: flex;
justify-content: center;
position: absolute;
right: $session-margin-lg;
align-items: center;
height: $main-view-header-height;
margin-right: $session-margin-lg;
}
}

View File

@ -248,3 +248,7 @@
color: $session-color-light-grey;
font-size: 13px;
}
.registration-content-centered {
text-align: center;
}

View File

@ -1,30 +1,22 @@
import React from 'react';
import React, { ChangeEvent } from 'react';
import { QRCode } from 'react-qr-svg';
import { SessionModal } from './session/SessionModal';
import { SessionButton } from './session/SessionButton';
import { SessionSpinner } from './session/SessionSpinner';
interface Props {
i18n: any;
onClose: any;
pubKeyToUnpair: string | null;
pubKey: string | null;
}
interface State {
currentPubKey: string | null;
accepted: boolean;
isListening: boolean;
success: boolean;
loading: boolean;
view:
| 'default'
| 'waitingForRequest'
| 'requestReceived'
| 'requestAccepted'
| 'confirmUnpair';
pubKeyRequests: Array<any>;
data: Array<any>;
currentView: 'filterRequestView' | 'qrcodeView';
errors: any;
loading: boolean;
deviceAlias: string | null;
}
export class DevicePairingDialog extends React.Component<Props, State> {
@ -33,156 +25,149 @@ export class DevicePairingDialog extends React.Component<Props, State> {
this.closeDialog = this.closeDialog.bind(this);
this.onKeyUp = this.onKeyUp.bind(this);
this.startReceivingRequests = this.startReceivingRequests.bind(this);
this.stopReceivingRequests = this.stopReceivingRequests.bind(this);
this.startReceivingRequests = this.startReceivingRequests.bind(this);
this.getPubkeyName = this.getPubkeyName.bind(this);
this.skipDevice = this.skipDevice.bind(this);
this.allowDevice = this.allowDevice.bind(this);
this.validateSecondaryDevice = this.validateSecondaryDevice.bind(this);
this.handleUpdateDeviceAlias = this.handleUpdateDeviceAlias.bind(this);
this.state = {
currentPubKey: this.props.pubKey,
currentPubKey: null,
accepted: false,
isListening: false,
success: false,
loading: true,
view: 'default',
pubKeyRequests: [],
data: [],
pubKeyRequests: Array(),
currentView: 'qrcodeView',
loading: false,
errors: undefined,
deviceAlias: null,
};
}
public componentDidMount() {
this.getSecondaryDevices();
public componentWillMount() {
this.startReceivingRequests();
}
public render() {
const { i18n } = this.props;
public componentWillUnmount() {
this.closeDialog();
}
const waitingForRequest = this.state.view === 'waitingForRequest';
const nothingPaired = this.state.data.length === 0;
/*
dialog.on('deviceUnpairingRequested', pubKey =>
Whisper.events.trigger('deviceUnpairingRequested', pubKey)
);*/
public renderFilterRequestsView() {
const { currentPubKey, accepted, deviceAlias } = this.state;
const secretWords = window.mnemonic.pubkey_to_secret_words(currentPubKey);
const deviceAliasPlaceholder = this.getPubkeyName(currentPubKey);
const deviceName = deviceAliasPlaceholder.deviceAlias;
if (accepted) {
return (
<SessionModal
title={window.i18n('provideDeviceAlias')}
onOk={() => null}
onClose={this.closeDialog}
>
<div className="session-modal__centered">
<input onChange={this.handleUpdateDeviceAlias}>{deviceName}</input>
<div className="session-modal__button-group">
<SessionButton
text={window.i18n('ok')}
onClick={this.validateSecondaryDevice}
disabled={!deviceAlias}
/>
</div>
<SessionSpinner loading={this.state.loading} />
</div>
</SessionModal>
);
}
return (
<SessionModal
title={window.i18n('allowPairingWithDevice')}
onOk={() => null}
onClose={this.closeDialog}
>
<div className="session-modal__centered">
<label>{window.i18n('secretWords')}</label>
<div className="text-subtle">{secretWords}</div>
<div className="session-modal__button-group">
<SessionButton
text={window.i18n('skip')}
onClick={this.skipDevice}
/>
<SessionButton
text={window.i18n('allowPairing')}
onClick={this.allowDevice}
/>
</div>
</div>
</SessionModal>
);
}
public renderQrCodeView() {
const theme = window.Events.getThemeSetting();
const requestReceived = this.hasReceivedRequests();
const title = window.i18n('pairingDevice');
// Foreground equivalent to .session-modal background color
const bgColor = 'rgba(0, 0, 0, 0)';
const fgColor = theme === 'dark' ? '#FFFFFF' : '#1B1B1B';
// const renderPairedDevices = this.state.data.map((pubKey: any) => {
// const pubKeyInfo = this.getPubkeyName(pubKey);
// const isFinalItem =
// this.state.data[this.state.data.length - 1] === pubKey;
// return (
// <div key={pubKey}>
// <p>
// {pubKeyInfo.deviceAlias}
// <br />
// <span className="text-subtle">Pairing Secret:</span>{' '}
// {pubKeyInfo.secretWords}
// </p>
// {!isFinalItem ? <hr className="text-soft fullwidth" /> : null}
// </div>
// );
// });
return (
<>
{!this.state.loading && (
<SessionModal
title={i18n('pairedDevices')}
onOk={() => null}
onClose={this.closeDialog}
>
{waitingForRequest ? (
<div className="session-modal__centered">
<h3>{i18n('waitingForDeviceToRegister')}</h3>
<small className="text-subtle">
{i18n('pairNewDevicePrompt')}
</small>
<div className="spacer-lg" />
<SessionModal title={title} onOk={() => null} onClose={this.closeDialog}>
<div className="session-modal__centered">
<h4>{window.i18n('waitingForDeviceToRegister')}</h4>
<small className="text-subtle">
{window.i18n('pairNewDevicePrompt')}
</small>
<div className="spacer-lg" />
<div id="qr">
<QRCode
value={window.textsecure.storage.user.getNumber()}
bgColor={bgColor}
fgColor={fgColor}
level="L"
/>
</div>
<div id="qr">
<QRCode
value={window.textsecure.storage.user.getNumber()}
bgColor={bgColor}
fgColor={fgColor}
level="L"
/>
</div>
<div className="spacer-lg" />
<div className="session-modal__button-group__center">
<SessionButton
text={i18n('cancel')}
onClick={this.stopReceivingRequests}
/>
</div>
</div>
<div className="spacer-lg" />
<div className="session-modal__button-group__center">
{!requestReceived ? (
<SessionButton
text={window.i18n('cancel')}
onClick={this.closeDialog}
/>
) : (
<>
{nothingPaired ? (
<div className="session-modal__centered">
<div>{i18n('noPairedDevices')}</div>
</div>
) : (
<div className="session-modal__centered">
{'renderPairedDevices'}
</div>
)}
<div className="spacer-lg" />
<div className="session-modal__button-group__center">
<SessionButton
text={i18n('pairNewDevice')}
onClick={this.startReceivingRequests}
/>
</div>
</>
<div className="session-modal__button-group">
<SessionButton
text={window.i18n('filterReceivedRequests')}
onClick={this.stopReceivingRequests}
/>
</div>
)}
</SessionModal>
)}
</>
</div>
</div>
</SessionModal>
);
}
private showView(
view?:
| 'default'
| 'waitingForRequest'
| 'requestReceived'
| 'requestAccepted'
| 'confirmUnpair'
) {
if (!view) {
this.setState({
view: 'default',
});
public render() {
const { currentView } = this.state;
const renderQrCodeView = currentView === 'qrcodeView';
const renderFilterRequestView = currentView === 'filterRequestView';
return;
}
if (view === 'waitingForRequest') {
this.setState({
view,
isListening: true,
});
return;
}
this.setState({ view });
}
private getSecondaryDevices() {
const secondaryDevices = window.libloki.storage
.getSecondaryDevicesFor(this.state.currentPubKey)
.then(() => {
this.setState({
data: secondaryDevices,
loading: false,
});
});
}
private startReceivingRequests() {
this.showView('waitingForRequest');
return (
<>
{renderQrCodeView && this.renderQrCodeView()}
{renderFilterRequestView && this.renderFilterRequestsView()}
</>
);
}
private getPubkeyName(pubKey: string | null) {
@ -197,74 +182,104 @@ export class DevicePairingDialog extends React.Component<Props, State> {
return { deviceAlias, secretWords };
}
private stopReceivingRequests() {
if (this.state.success) {
const aliasKey = 'deviceAlias';
const deviceAlias = this.getPubkeyName(this.state.currentPubKey)[
aliasKey
];
const conv = window.ConversationController.get(this.state.currentPubKey);
if (conv) {
conv.setNickname(deviceAlias);
}
}
this.showView();
private reset() {
this.setState({
currentPubKey: null,
accepted: false,
pubKeyRequests: Array(),
currentView: 'filterRequestView',
deviceAlias: null,
});
}
// private requestReceived(secondaryDevicePubKey: string | EventHandlerNonNull) {
// // FIFO: push at the front of the array with unshift()
// this.state.pubKeyRequests.unshift(secondaryDevicePubKey);
// if (!this.state.currentPubKey) {
// this.nextPubKey();
private startReceivingRequests() {
this.reset();
window.Whisper.events.on(
'devicePairingRequestReceived',
(pubKey: string) => {
this.requestReceived(pubKey);
}
);
this.setState({ currentView: 'qrcodeView' });
}
// this.showView('requestReceived');
// }
// }
private stopReceivingRequests() {
this.setState({ currentView: 'filterRequestView' });
window.Whisper.events.off('devicePairingRequestReceived');
}
// private allowDevice() {
// this.setState({
// accepted: true,
// });
// window.Whisper.trigger(
// 'devicePairingRequestAccepted',
// this.state.currentPubKey,
// (errors: any) => {
// this.transmisssionCB(errors);
private requestReceived(secondaryDevicePubKey: string | EventHandlerNonNull) {
// FIFO: push at the front of the array with unshift()
this.state.pubKeyRequests.unshift(secondaryDevicePubKey);
window.pushToast({
title: window.i18n('gotPairingRequest'),
description: `${window.shortenPubkey(
secondaryDevicePubKey
)} ${window.i18n(
'showPairingWordsTitle'
)}: ${window.mnemonic.pubkey_to_secret_words(secondaryDevicePubKey)}`,
});
if (!this.state.currentPubKey) {
this.nextPubKey();
}
}
// return true;
// }
// );
// this.showView();
// }
private allowDevice() {
this.setState({
accepted: true,
});
}
// private transmisssionCB(errors: any) {
// if (!errors) {
// this.setState({
// success: true,
// });
// } else {
// return;
// }
// }
private transmissionCB(errors: any) {
if (!errors) {
this.setState({
errors: null,
});
this.closeDialog();
window.pushToast({
title: window.i18n('devicePairedSuccessfully'),
});
const conv = window.ConversationController.get(this.state.currentPubKey);
if (conv) {
conv.setNickname(this.state.deviceAlias);
}
// private skipDevice() {
// window.Whisper.trigger(
// 'devicePairingRequestRejected',
// this.state.currentPubKey
// );
// this.nextPubKey();
// this.showView();
// }
// FIXME display error somewhere
// FIXME display list of linked device
// FIXME do not show linked device in list of contacts
// private nextPubKey() {
// // FIFO: pop at the back of the array using pop()
// const pubKeyRequests = this.state.pubKeyRequests;
// this.setState({
// currentPubKey: pubKeyRequests.pop(),
// });
// }
return;
}
/* this.$('.transmissionStatus').text(errors);
this.$('.requestAcceptedView .ok').show();*/
this.setState({
errors: errors,
});
}
private skipDevice() {
window.Whisper.events.trigger(
'devicePairingRequestRejected',
this.state.currentPubKey
);
const hasNext = this.state.pubKeyRequests.length > 0;
this.nextPubKey();
if (!hasNext) {
this.startReceivingRequests();
}
this.setState({
currentView: hasNext ? 'filterRequestView' : 'qrcodeView',
});
}
private nextPubKey() {
// FIFO: pop at the back of the array using pop()
this.setState({
currentPubKey: this.state.pubKeyRequests.pop(),
});
}
private onKeyUp(event: any) {
switch (event.key) {
@ -276,9 +291,42 @@ export class DevicePairingDialog extends React.Component<Props, State> {
}
}
private validateSecondaryDevice() {
this.setState({ loading: true });
window.Whisper.events.trigger(
'devicePairingRequestAccepted',
this.state.currentPubKey,
(errors: any) => {
this.transmissionCB(errors);
return true;
}
);
}
private hasReceivedRequests() {
return this.state.currentPubKey || this.state.pubKeyRequests.length > 0;
}
private closeDialog() {
window.removeEventListener('keyup', this.onKeyUp);
this.stopReceivingRequests();
window.Whisper.events.off('devicePairingRequestReceived');
if (this.state.currentPubKey && !this.state.accepted) {
window.Whisper.events.trigger(
'devicePairingRequestRejected',
this.state.currentPubKey
);
}
this.props.onClose();
}
private handleUpdateDeviceAlias(value: ChangeEvent<HTMLInputElement>) {
const trimmed = value.target.value.trim();
if (!!trimmed) {
this.setState({ deviceAlias: trimmed });
} else {
this.setState({ deviceAlias: null });
}
}
}

View File

@ -14,10 +14,12 @@ export const MainViewController = {
},
renderSettingsView: (category: SessionSettingCategory) => {
ReactDOM.render(
<SettingsView category={category} />,
document.getElementById('main-view')
);
if (document.getElementById('main-view')) {
ReactDOM.render(
<SettingsView category={category} />,
document.getElementById('main-view')
);
}
},
};

View File

@ -274,11 +274,13 @@ export class LeftPaneChannelSection extends React.Component<Props, State> {
return (
<div className="left-pane-contact-bottom-buttons">
{showEditButton && <SessionButton
text={edit}
buttonType={SessionButtonType.SquareOutline}
buttonColor={SessionButtonColor.White}
/>}
{showEditButton && (
<SessionButton
text={edit}
buttonType={SessionButtonType.SquareOutline}
buttonColor={SessionButtonColor.White}
/>
)}
<SessionButton
text={addChannel}
buttonType={SessionButtonType.SquareOutline}

View File

@ -280,11 +280,13 @@ export class LeftPaneContactSection extends React.Component<Props, State> {
return (
<div className="left-pane-contact-bottom-buttons">
{showEditButton && <SessionButton
text={edit}
buttonType={SessionButtonType.SquareOutline}
buttonColor={SessionButtonColor.White}
/>}
{showEditButton && (
<SessionButton
text={edit}
buttonType={SessionButtonType.SquareOutline}
buttonColor={SessionButtonColor.White}
/>
)}
{selectedTab === 0 ? (
<SessionButton
text={addContact}

View File

@ -132,7 +132,7 @@ export class LeftPaneSettingSection extends React.Component<any, State> {
</div>
</div>
</div>
</div>
);
}
@ -208,6 +208,11 @@ export class LeftPaneSettingSection extends React.Component<any, State> {
description: window.i18n('notificationSettingsDescription'),
hidden: false,
},
{
id: SessionSettingCategory.Devices,
title: window.i18n('devicesSettingsTitle'),
description: window.i18n('devicesSettingsDescription'),
},
];
}

View File

@ -10,6 +10,7 @@ import {
import { trigger } from '../../shims/events';
import { SessionHtmlRenderer } from './SessionHTMLRenderer';
import { SessionIdEditable } from './SessionIdEditable';
import { SessionSpinner } from './SessionSpinner';
enum SignInMode {
Default,
@ -42,6 +43,7 @@ interface State {
primaryDevicePubKey: string;
mnemonicError: string | undefined;
displayNameError: string | undefined;
loading: boolean;
}
const Tab = ({
@ -115,6 +117,7 @@ export class RegistrationTabs extends React.Component<{}, State> {
primaryDevicePubKey: '',
mnemonicError: undefined,
displayNameError: undefined,
loading: false,
};
this.accountManager = window.getAccountManager();
@ -413,11 +416,12 @@ export class RegistrationTabs extends React.Component<{}, State> {
}
if (signInMode === SignInMode.LinkingDevice) {
return (
<div className="">
<div className="registration-content-centered">
<div className="session-signin-device-pairing-header">
{window.i18n('devicePairingHeader')}
</div>
{this.renderEnterSessionID(true)}
<SessionSpinner loading={this.state.loading} />
</div>
);
}
@ -734,7 +738,7 @@ export class RegistrationTabs extends React.Component<{}, State> {
if (passwordErrorString || passwordFieldsMatch) {
window.pushToast({
title: window.i18n('invalidPassword'),
type: 'success',
type: 'error',
id: 'invalidPassword',
});
@ -782,6 +786,9 @@ export class RegistrationTabs extends React.Component<{}, State> {
if (window.textsecure.storage.get('secondaryDeviceStatus') === 'ongoing') {
return;
}
this.setState({
loading: true,
});
await this.resetRegistration();
window.textsecure.storage.put('secondaryDeviceStatus', 'ongoing');
@ -798,7 +805,7 @@ export class RegistrationTabs extends React.Component<{}, State> {
);
const onError = async (error: any) => {
window.log.error.error(error);
window.log.error(error);
await this.resetRegistration();
};
@ -826,16 +833,26 @@ export class RegistrationTabs extends React.Component<{}, State> {
await this.accountManager.requestPairing(primaryPubKey);
const pubkey = window.textsecure.storage.user.getNumber();
const words = window.mnemonic.pubkey_to_secret_words(pubkey);
window.console.log('pubkey_to_secret_words');
window.console.log(`Here is your secret:\n${words}`);
window.pushToast({
title: `Here is your secret: "${words}"`,
id: 'yourSecret',
shouldFade: false,
});
} catch (e) {
window.console.log(e);
//onError(e);
this.setState({
loading: false,
});
}
}
private async onSecondaryDeviceRegistered() {
// Ensure the left menu is updated
this.setState({
loading: false,
});
trigger('userChanged', { isSecondaryDevice: true });
// will re-run the background initialisation
trigger('registration_done');

View File

@ -6,6 +6,7 @@ import { SessionIconButton, SessionIconSize, SessionIconType } from './icon';
export const SessionRegistrationView: React.FC = () => (
<div className="session-content">
<div id="session-toast-container" />
<div id="error" className="collapse" />
<div className="session-content-close-button">
<SessionIconButton

View File

@ -45,7 +45,14 @@ export class SettingsView extends React.Component<SettingsViewProps, State> {
}
/* tslint:disable-next-line:max-func-body-length */
public renderSettingInCategory() {
public renderSettingInCategory(): JSX.Element {
const { category } = this.props;
if (category === SessionSettingCategory.Devices) {
// special case for linked devices
return this.renderLinkedDevicesCategory();
}
const { Settings } = window.Signal.Types;
// Grab initial values from database on startup
@ -230,6 +237,7 @@ export class SettingsView extends React.Component<SettingsViewProps, State> {
this.updateSetting(setting);
});
return (
<div key={setting.id}>
{shouldRenderSettings &&
@ -307,4 +315,9 @@ export class SettingsView extends React.Component<SettingsViewProps, State> {
});
}
}
private renderLinkedDevicesCategory(): JSX.Element {
return <div />;
}
}

View File

@ -1,35 +1,51 @@
import React from 'react';
import { SessionIconButton, SessionIconSize, SessionIconType } from '../icon';
import { SettingsViewProps } from './SessionSettings';
import { SessionSettingCategory, SettingsViewProps } from './SessionSettings';
import { SessionButton } from '../SessionButton';
export class SettingsHeader extends React.Component<SettingsViewProps> {
public constructor(props: any) {
super(props);
this.showAddLinkedDeviceModal = this.showAddLinkedDeviceModal.bind(this);
}
public focusSearch() {
$('.left-pane-setting-section .session-search-input input').focus();
}
public showAddLinkedDeviceModal() {
window.Whisper.events.trigger('showDevicePairingDialog');
}
public render() {
const category = String(this.props.category);
const categoryTitlePrefix = category[0].toUpperCase() + category.substr(1);
const { category } = this.props;
const categoryString = String(category);
const categoryTitlePrefix =
categoryString[0].toUpperCase() + categoryString.substr(1);
// Remove 's' on the end to keep words in singular form
const categoryTitle =
categoryTitlePrefix[categoryTitlePrefix.length - 1] === 's'
? `${categoryTitlePrefix.slice(0, -1)} Settings`
: `${categoryTitlePrefix} Settings`;
const showSearch = false;
const showAddDevice = category === SessionSettingCategory.Devices;
return (
<div className="session-settings-header">
{categoryTitle}
<div className="session-settings-header-title">{categoryTitle}</div>
{showSearch && <SessionIconButton
iconType={SessionIconType.Search}
iconSize={SessionIconSize.Huge}
onClick={this.focusSearch}
/>}
/>
}
{showAddDevice && (
<SessionButton
text={window.i18n('linkNewDevice')}
onClick={this.showAddLinkedDeviceModal}
/>
)}
</div>
);
}