diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 522bc76f4..b07b94851 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -862,11 +862,17 @@ "devicePairingReceived": { "message": "Device Linking Received" }, + "devicePairingRequestReceivedLimitTitle": { + "message": "Device linking limit reached." + }, + "devicePairingRequestReceivedLimitDescription": { + "message": "To change your linked devices, please unlink a device first." + }, "devicePairingRequestReceivedNoListenerTitle": { "message": "Device linking request received." }, "devicePairingRequestReceivedNoListenerDescription": { - "message": "Device linking request received but you are not on the device linking screen. \nFirst go to Settings -> Device -> Link New Device." + "message": "Device linking request received but you are not on the device linking screen. \nGo to Settings > Device > Link New Device." }, "waitingForDeviceToRegister": { "message": "Waiting for device to register..." @@ -2373,11 +2379,23 @@ "verifyPassword": { "message": "Verify Password" }, - "devicePairingHeader": { - "message": "Open Session on your other device and navigate to the Linked Devices section in your user account screen. Select Link a Device to prepare your other device for pairing, then enter your Session ID below to link this device to your Session ID." + "devicePairingHeaderReassure": { + "message": "Linking may take up to one minute to register on your primary device. Please be patient." + }, + "devicePairingHeader_Step1": { + "message": "Open Session on your other device." + }, + "devicePairingHeader_Step2": { + "message": "Navigate to the Devices section in your user account screen." + }, + "devicePairingHeader_Step3": { + "message": "Select Link New Device to prepare your other device for pairing." + }, + "devicePairingHeader_Step4": { + "message": "Enter your Session ID below to link this device to your Session ID." }, "enterSessionIDHere": { - "message": "Enter other device’s Session ID here" + "message": "Enter your Session ID here" }, "continueYourSession": { "message": "Continue Your Session" diff --git a/js/background.js b/js/background.js index 89f5d242f..f342aeab9 100644 --- a/js/background.js +++ b/js/background.js @@ -1371,13 +1371,31 @@ }); Whisper.events.on('devicePairingRequestReceivedNoListener', async () => { + // If linking limit has been reached, let master know. + const ourKey = textsecure.storage.user.getNumber(); + const ourPubKey = window.libsession.Types.PubKey.cast(ourKey); + const authorisations = await window.libsession.Protocols.MultiDeviceProtocol.fetchPairingAuthorisations( + ourPubKey + ); + + const title = authorisations.length + ? window.i18n('devicePairingRequestReceivedLimitTitle') + : window.i18n('devicePairingRequestReceivedNoListenerTitle'); + + const description = authorisations.length + ? window.i18n( + 'devicePairingRequestReceivedLimitDescription', + window.CONSTANTS.MAX_LINKED_DEVICES + ) + : window.i18n('devicePairingRequestReceivedNoListenerDescription'); + + const type = authorisations.length ? 'info' : 'warning'; + window.pushToast({ - title: window.i18n('devicePairingRequestReceivedNoListenerTitle'), - description: window.i18n( - 'devicePairingRequestReceivedNoListenerDescription' - ), - type: 'info', - id: 'pairingRequestNoListener', + title, + description, + type, + id: 'pairingRequestReceived', shouldFade: false, }); }); diff --git a/preload.js b/preload.js index efafb66fa..c9f918a8e 100644 --- a/preload.js +++ b/preload.js @@ -82,6 +82,7 @@ window.CONSTANTS = new (function() { this.MAX_USERNAME_LENGTH = 20; this.MAX_GROUP_NAME_LENGTH = 64; this.DEFAULT_PUBLIC_CHAT_URL = appConfig.get('defaultPublicChatServer'); + this.MAX_LINKED_DEVICES = 1; this.MAX_CONNECTION_DURATION = 5000; this.MAX_MESSAGE_BODY_LENGTH = 64 * 1024; // Limited due to the proof-of-work requirement diff --git a/stylesheets/_session_signin.scss b/stylesheets/_session_signin.scss index 65f0eed86..03ccccc3f 100644 --- a/stylesheets/_session_signin.scss +++ b/stylesheets/_session_signin.scss @@ -75,6 +75,20 @@ &__content { width: 100%; padding-top: 20px; + + &__secret-words { + display: flex; + flex-direction: column; + align-items: center; + background-color: $session-shade-6; + padding: $session-margin-sm $session-margin-lg; + border-radius: 8px; + margin-bottom: 0px; + + label { + margin-bottom: 5px; + } + } } &__sections { @@ -228,11 +242,17 @@ &-description-long, &-signin-device-pairing-header { padding-top: 10px; - padding-bottom: 10px; + padding-bottom: 20px; color: $session-color-light-grey; text-align: center; font-size: 12px; line-height: 20px; + + ol { + margin-left: 20px; + padding: 0px; + text-align: justify; + } } &-id-editable { diff --git a/ts/components/session/RegistrationTabs.tsx b/ts/components/session/RegistrationTabs.tsx index db7fdd8da..be7d733ea 100644 --- a/ts/components/session/RegistrationTabs.tsx +++ b/ts/components/session/RegistrationTabs.tsx @@ -34,6 +34,7 @@ interface State { selectedTab: TabType; signInMode: SignInMode; signUpMode: SignUpMode; + secretWords: string | undefined; displayName: string; password: string; validatePassword: string; @@ -109,6 +110,7 @@ export class RegistrationTabs extends React.Component<{}, State> { selectedTab: TabType.Create, signInMode: SignInMode.Default, signUpMode: SignUpMode.Default, + secretWords: undefined, displayName: '', password: '', validatePassword: '', @@ -418,10 +420,41 @@ export class RegistrationTabs extends React.Component<{}, State> { return (
- {window.i18n('devicePairingHeader')} + {!!this.state.secretWords ? ( +

{window.i18n('devicePairingHeaderReassure')}

+ ) : ( +
    +
  1. + +
  2. +
  3. + +
  4. +
  5. + +
  6. +
  7. + +
  8. +
+ )}
- {this.renderEnterSessionID(true)} - + {this.renderEnterSessionID(!this.state.secretWords)} + {this.state.secretWords && ( +
+ +
{this.state.secretWords}
+
+ )} +
); } @@ -534,10 +567,14 @@ export class RegistrationTabs extends React.Component<{}, State> { return (
{this.renderContinueYourSessionButton()} -

{or}

- {this.renderRestoreUsingSeedButton( - SessionButtonType.BrandOutline, - SessionButtonColor.White + {!this.state.secretWords && ( + <> +

{or}

+ {this.renderRestoreUsingSeedButton( + SessionButtonType.BrandOutline, + SessionButtonColor.White + )} + )}
); @@ -584,8 +621,8 @@ export class RegistrationTabs extends React.Component<{}, State> { let enableContinue = true; let text = window.i18n('continueYourSession'); - const displayNameOK = !displayNameError && !!displayName; //display name required - const mnemonicOK = !mnemonicError && !!mnemonicSeed; //Mnemonic required + const displayNameOK = !displayNameError && !!displayName; // Display name required + const mnemonicOK = !mnemonicError && !!mnemonicSeed; // Mnemonic required const passwordsOK = !password || (!passwordErrorString && passwordFieldsMatch); // password is valid if empty, or if no error and fields are matching if (signInMode === SignInMode.UsingSeed) { @@ -597,13 +634,28 @@ export class RegistrationTabs extends React.Component<{}, State> { enableContinue = displayNameOK && passwordsOK; } + const shouldRenderCancel = + this.state.signInMode === SignInMode.LinkingDevice && + !!this.state.secretWords; + + text = shouldRenderCancel ? window.i18n('cancel') : text; + const buttonColor = shouldRenderCancel + ? SessionButtonColor.White + : SessionButtonColor.Green; + const buttonType = shouldRenderCancel + ? SessionButtonType.BrandOutline + : SessionButtonType.Brand; + const onClick = () => { + shouldRenderCancel + ? this.cancelSecondaryDevice() + : this.handleContinueYourSessionClick(); + }; + return ( { - this.handleContinueYourSessionClick(); - }} - buttonType={SessionButtonType.Brand} - buttonColor={SessionButtonColor.Green} + onClick={onClick} + buttonType={buttonType} + buttonColor={buttonColor} text={text} disabled={!enableContinue} /> @@ -719,6 +771,11 @@ export class RegistrationTabs extends React.Component<{}, State> { window.ConversationController.reset(); await window.ConversationController.load(); window.Whisper.RotateSignedPreKeyListener.stop(window.Whisper.events); + + this.setState({ + loading: false, + secretWords: undefined, + }); } private async register(language: string) { @@ -892,14 +949,8 @@ 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(`Here is your secret:\n${words}`); - window.pushToast({ - title: `${window.i18n('secretPrompt')}`, - description: words, - id: 'yourSecret', - shouldFade: false, - }); + const secretWords = window.mnemonic.pubkey_to_secret_words(pubkey); + this.setState({ secretWords }); } catch (e) { window.console.log(e); await this.resetRegistration(); diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index de93f3d84..5b896ad6c 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -120,6 +120,11 @@ export const _getLeftPaneLists = ( }; } + // Remove all invalid conversations and conversatons of devices associated with cancelled attempted links + if (!conversation.timestamp) { + continue; + } + if (conversation.activeAt !== undefined) { allContacts.push(conversation); } diff --git a/ts/test/state/selectors/conversations_test.ts b/ts/test/state/selectors/conversations_test.ts index 6bc3f65ec..f771aebe4 100644 --- a/ts/test/state/selectors/conversations_test.ts +++ b/ts/test/state/selectors/conversations_test.ts @@ -110,7 +110,6 @@ describe('state/selectors/conversations', () => { assert.strictEqual(conversations[1].name, 'Á'); assert.strictEqual(conversations[2].name, 'B'); assert.strictEqual(conversations[3].name, 'C'); - assert.strictEqual(conversations[4].name, 'No timestamp'); }); }); });