diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 7f090909e..06f944bbb 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -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" } } diff --git a/js/views/app_view.js b/js/views/app_view.js index 7c1b59d52..3893d053c 100644 --- a/js/views/app_view.js +++ b/js/views/app_view.js @@ -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() { diff --git a/stylesheets/_conversation.scss b/stylesheets/_conversation.scss index 1b2bb2144..9faf2e72f 100644 --- a/stylesheets/_conversation.scss +++ b/stylesheets/_conversation.scss @@ -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; } } diff --git a/stylesheets/_session.scss b/stylesheets/_session.scss index ac30ec90c..e94cc6069 100644 --- a/stylesheets/_session.scss +++ b/stylesheets/_session.scss @@ -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; } } diff --git a/stylesheets/_session_signin.scss b/stylesheets/_session_signin.scss index f8a026587..d74ac66d6 100644 --- a/stylesheets/_session_signin.scss +++ b/stylesheets/_session_signin.scss @@ -248,3 +248,7 @@ color: $session-color-light-grey; font-size: 13px; } + +.registration-content-centered { + text-align: center; +} diff --git a/ts/components/DevicePairingDialog.tsx b/ts/components/DevicePairingDialog.tsx index 1a30e4153..a22a0ede9 100644 --- a/ts/components/DevicePairingDialog.tsx +++ b/ts/components/DevicePairingDialog.tsx @@ -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; - data: Array; + currentView: 'filterRequestView' | 'qrcodeView'; + errors: any; + loading: boolean; + deviceAlias: string | null; } export class DevicePairingDialog extends React.Component { @@ -33,156 +25,149 @@ export class DevicePairingDialog extends React.Component { 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 ( + null} + onClose={this.closeDialog} + > +
+ {deviceName} +
+ +
+ +
+
+ ); + } + return ( + null} + onClose={this.closeDialog} + > +
+ +
{secretWords}
+
+ + +
+
+
+ ); + } + + 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 ( - //
- //

- // {pubKeyInfo.deviceAlias} - //
- // Pairing Secret:{' '} - // {pubKeyInfo.secretWords} - //

- // {!isFinalItem ?
: null} - //
- // ); - // }); - return ( - <> - {!this.state.loading && ( - null} - onClose={this.closeDialog} - > - {waitingForRequest ? ( -
-

{i18n('waitingForDeviceToRegister')}

- - {i18n('pairNewDevicePrompt')} - -
- -
- -
- -
-
- -
-
+ null} onClose={this.closeDialog}> +
+

{window.i18n('waitingForDeviceToRegister')}

+ + {window.i18n('pairNewDevicePrompt')} + +
+ +
+ +
+ +
+
+ {!requestReceived ? ( + ) : ( - <> - {nothingPaired ? ( -
-
{i18n('noPairedDevices')}
-
- ) : ( -
- {'renderPairedDevices'} -
- )} - -
-
- -
- +
+ +
)} - - )} - +
+
+ ); } - private showView( - view?: - | 'default' - | 'waitingForRequest' - | 'requestReceived' - | 'requestAccepted' - | 'confirmUnpair' - ) { - if (!view) { - this.setState({ - view: 'default', - }); - - 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, - }); - }); - } + public render() { + const { currentView } = this.state; + const renderQrCodeView = currentView === 'qrcodeView'; + const renderFilterRequestView = currentView === 'filterRequestView'; - 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 { return { deviceAlias, secretWords }; } + private reset() { + this.setState({ + currentPubKey: null, + accepted: false, + pubKeyRequests: Array(), + currentView: 'filterRequestView', + deviceAlias: null, + }); + } + + private startReceivingRequests() { + this.reset(); + window.Whisper.events.on( + 'devicePairingRequestReceived', + (pubKey: string) => { + this.requestReceived(pubKey); + } + ); + this.setState({ currentView: 'qrcodeView' }); + } + private stopReceivingRequests() { - if (this.state.success) { - const aliasKey = 'deviceAlias'; - const deviceAlias = this.getPubkeyName(this.state.currentPubKey)[ - aliasKey - ]; + this.setState({ currentView: 'filterRequestView' }); + window.Whisper.events.off('devicePairingRequestReceived'); + } + 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(); + } + } + + private allowDevice() { + this.setState({ + accepted: true, + }); + } + + 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(deviceAlias); + conv.setNickname(this.state.deviceAlias); } + + // FIXME display error somewhere + // FIXME display list of linked device + // FIXME do not show linked device in list of contacts + + return; } + /* this.$('.transmissionStatus').text(errors); + this.$('.requestAcceptedView .ok').show();*/ - this.showView(); + this.setState({ + errors: errors, + }); } - // 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(); - - // this.showView('requestReceived'); - // } - // } - - // private allowDevice() { - // this.setState({ - // accepted: true, - // }); - // window.Whisper.trigger( - // 'devicePairingRequestAccepted', - // this.state.currentPubKey, - // (errors: any) => { - // this.transmisssionCB(errors); - - // return true; - // } - // ); - // this.showView(); - // } - - // private transmisssionCB(errors: any) { - // if (!errors) { - // this.setState({ - // success: true, - // }); - // } else { - // return; - // } - // } - - // private skipDevice() { - // window.Whisper.trigger( - // 'devicePairingRequestRejected', - // this.state.currentPubKey - // ); - // this.nextPubKey(); - // this.showView(); - // } - - // private nextPubKey() { - // // FIFO: pop at the back of the array using pop() - // const pubKeyRequests = this.state.pubKeyRequests; - // this.setState({ - // currentPubKey: pubKeyRequests.pop(), - // }); - // } + 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 { } } + 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) { + const trimmed = value.target.value.trim(); + if (!!trimmed) { + this.setState({ deviceAlias: trimmed }); + } else { + this.setState({ deviceAlias: null }); + } + } } diff --git a/ts/components/MainViewController.tsx b/ts/components/MainViewController.tsx index 8bc446c54..2641081fa 100644 --- a/ts/components/MainViewController.tsx +++ b/ts/components/MainViewController.tsx @@ -14,10 +14,12 @@ export const MainViewController = { }, renderSettingsView: (category: SessionSettingCategory) => { - ReactDOM.render( - , - document.getElementById('main-view') - ); + if (document.getElementById('main-view')) { + ReactDOM.render( + , + document.getElementById('main-view') + ); + } }, }; diff --git a/ts/components/session/LeftPaneChannelSection.tsx b/ts/components/session/LeftPaneChannelSection.tsx index de9909efa..c9e2a1802 100644 --- a/ts/components/session/LeftPaneChannelSection.tsx +++ b/ts/components/session/LeftPaneChannelSection.tsx @@ -274,11 +274,13 @@ export class LeftPaneChannelSection extends React.Component { return (
- {showEditButton && } + {showEditButton && ( + + )} { return (
- {showEditButton && } + {showEditButton && ( + + )} {selectedTab === 0 ? ( {
-
+
); } @@ -208,6 +208,11 @@ export class LeftPaneSettingSection extends React.Component { description: window.i18n('notificationSettingsDescription'), hidden: false, }, + { + id: SessionSettingCategory.Devices, + title: window.i18n('devicesSettingsTitle'), + description: window.i18n('devicesSettingsDescription'), + }, ]; } diff --git a/ts/components/session/RegistrationTabs.tsx b/ts/components/session/RegistrationTabs.tsx index 6eac6ce4f..f398ba33d 100644 --- a/ts/components/session/RegistrationTabs.tsx +++ b/ts/components/session/RegistrationTabs.tsx @@ -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 ( -
+
{window.i18n('devicePairingHeader')}
{this.renderEnterSessionID(true)} +
); } @@ -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'); diff --git a/ts/components/session/SessionRegistrationView.tsx b/ts/components/session/SessionRegistrationView.tsx index 3da95f6b5..3ab0d345e 100644 --- a/ts/components/session/SessionRegistrationView.tsx +++ b/ts/components/session/SessionRegistrationView.tsx @@ -6,6 +6,7 @@ import { SessionIconButton, SessionIconSize, SessionIconType } from './icon'; export const SessionRegistrationView: React.FC = () => (
+
{ } /* 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 { this.updateSetting(setting); }); + return (
{shouldRenderSettings && @@ -307,4 +315,9 @@ export class SettingsView extends React.Component { }); } } + + + private renderLinkedDevicesCategory(): JSX.Element { + return
; + } } diff --git a/ts/components/session/settings/SessionSettingsHeader.tsx b/ts/components/session/settings/SessionSettingsHeader.tsx index 39864e439..8feb3b9dd 100644 --- a/ts/components/session/settings/SessionSettingsHeader.tsx +++ b/ts/components/session/settings/SessionSettingsHeader.tsx @@ -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 { 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 (
- {categoryTitle} +
{categoryTitle}
{showSearch && } + /> + } + {showAddDevice && ( + + )}
); }