Redesign device link screens
This commit is contained in:
parent
a023fc1bb0
commit
364f00f37a
36 changed files with 1358 additions and 803 deletions
|
@ -3,6 +3,7 @@
|
|||
|
||||
<!-- prettier-ignore -->
|
||||
<link rel="stylesheet" href="../stylesheets/manifest.css" />
|
||||
<script src="../components/qrcode/qrcode.js"></script>
|
||||
<script>
|
||||
window.SignalWindow = window.SignalWindow || {};
|
||||
window.SignalWindow.log = {
|
||||
|
|
|
@ -1306,29 +1306,75 @@
|
|||
"message": "Privacy is possible. Signal makes it easy.",
|
||||
"description": "Tagline displayed under 'installWelcome' string on the install page"
|
||||
},
|
||||
"linkYourPhone": {
|
||||
"message": "Link your phone to Signal Desktop",
|
||||
"description": "Shown on the front page when the application first starts, above the QR code"
|
||||
},
|
||||
"signalSettings": {
|
||||
"message": "Signal Settings",
|
||||
"description": "Used in the guidance to help people find the 'link new device' area of their Signal mobile app"
|
||||
},
|
||||
"linkedDevices": {
|
||||
"message": "Linked Devices",
|
||||
"description": "Used in the guidance to help people find the 'link new device' area of their Signal mobile app"
|
||||
},
|
||||
"plusButton": {
|
||||
"message": "'+' Button",
|
||||
"description": "The button used in Signal Android to add a new linked device"
|
||||
},
|
||||
"linkNewDevice": {
|
||||
"message": "Link New Device",
|
||||
"description": "The menu option shown in Signal iOS to add a new linked device"
|
||||
},
|
||||
"LinkScreen__scan-this-code": {
|
||||
"Install__scan-this-code": {
|
||||
"message": "Scan this code in the Signal app on your phone",
|
||||
"description": "Alt text for the QR code on the device link screen"
|
||||
"description": "Title of the device link screen. Also used as alt text for the QR code on the device link screen"
|
||||
},
|
||||
"Install__instructions__1": {
|
||||
"message": "Open Signal on your phone",
|
||||
"description": "Instructions on the device link screen"
|
||||
},
|
||||
"Install__instructions__2": {
|
||||
"message": "Tap into $settings$, then tap $linkedDevices$",
|
||||
"description": "Instructions on the device link screen",
|
||||
"placeholders": {
|
||||
"settings": {
|
||||
"content": "$1",
|
||||
"example": "<strong>Settings</strong>"
|
||||
},
|
||||
"linkedDevices": {
|
||||
"content": "$2",
|
||||
"example": "<strong>Linked Devices</strong>"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Install__instructions__2__settings": {
|
||||
"message": "Settings",
|
||||
"description": "Part of the 2nd instruction on the device link screen"
|
||||
},
|
||||
"Install__instructions__3": {
|
||||
"message": "Tap $plusButton$ (Android) or $linkNewDevice$ (iPhone)",
|
||||
"description": "Instructions on the device link screen",
|
||||
"placeholders": {
|
||||
"plusButton": {
|
||||
"content": "$1",
|
||||
"example": "<PlusButton />"
|
||||
},
|
||||
"linkNewDevice": {
|
||||
"content": "$2",
|
||||
"example": "<strong>Link New Device</strong>"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Install__qr-failed": {
|
||||
"message": "The QR code couldn't load. Check your internet and try again. $learnMore$",
|
||||
"description": "Shown on the install screen if the QR code fails to load",
|
||||
"placeholders": {
|
||||
"learnMore": {
|
||||
"content": "$1",
|
||||
"example": "<a>Learn more</a>"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Install__qr-failed__learn-more": {
|
||||
"message": "Learn more",
|
||||
"description": "Shown on the install screen if the QR code fails to load"
|
||||
},
|
||||
"Install__choose-device-name__description": {
|
||||
"message": "You'll see this name under \"Linked Devices\" on your phone",
|
||||
"description": "The subheader shown on the 'choose device name' screen in the device linking process"
|
||||
},
|
||||
"Install__choose-device-name__placeholder": {
|
||||
"message": "My Computer",
|
||||
"description": "The placeholder for the 'choose device name' input"
|
||||
},
|
||||
"Preferences--device-name": {
|
||||
"message": "Device name",
|
||||
|
@ -1346,6 +1392,10 @@
|
|||
"message": "Syncing contacts and groups",
|
||||
"description": "Shown during initial link while contacts and groups are being pulled from mobile device"
|
||||
},
|
||||
"initialSync__subtitle": {
|
||||
"message": "Note: Your chat history will not be synced to this device",
|
||||
"description": "Shown during initial link while contacts and groups are being pulled from mobile device"
|
||||
},
|
||||
"installConnectionFailed": {
|
||||
"message": "Failed to connect to server.",
|
||||
"description": "Displayed when we can't connect to the server."
|
||||
|
@ -1359,6 +1409,9 @@
|
|||
"installErrorHeader": {
|
||||
"message": "Something went wrong!"
|
||||
},
|
||||
"installUnknownError": {
|
||||
"message": "An unexpected error occurred. Please try again."
|
||||
},
|
||||
"installTryAgain": {
|
||||
"message": "Try again"
|
||||
},
|
||||
|
|
|
@ -91,105 +91,6 @@
|
|||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-tmpl-mustache" id="link-flow-template">
|
||||
<div class='module-title-bar-drag-area'></div>
|
||||
|
||||
{{#isStep3}}
|
||||
<div id='step3' class='step'>
|
||||
<div class='inner'>
|
||||
<div class='step-body'>
|
||||
<div class='header'>{{ linkYourPhone }}</div>
|
||||
<div id="qr">
|
||||
<div class='container'>
|
||||
<span class='dot'></span>
|
||||
<span class='dot'></span>
|
||||
<span class='dot'></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='nav'>
|
||||
<div class='instructions'>
|
||||
<div class='android'>
|
||||
<div class='label'>
|
||||
<span class='os-icon android'></span>
|
||||
</div>
|
||||
<div class='body'>
|
||||
<div>→ {{ signalSettings }}</div>
|
||||
<div>→ {{ linkedDevices }}</div>
|
||||
<div>→ {{ androidFinalStep }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='apple'>
|
||||
<div class='label'>
|
||||
<span class='os-icon apple'></span>
|
||||
</div>
|
||||
<div class='body'>
|
||||
<div>→ {{ signalSettings }}</div>
|
||||
<div>→ {{ linkedDevices }}</div>
|
||||
<div>→ {{ appleFinalStep }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/isStep3}}
|
||||
{{#isStep4}}
|
||||
<form id='link-phone'>
|
||||
<div id='step4' class='step'>
|
||||
<div class='inner'>
|
||||
<div class='step-body'>
|
||||
<span class='banner-icon lead-pencil'></span>
|
||||
<div class='header'>{{ chooseName }}</div>
|
||||
<div>
|
||||
<input type='text' class='device-name' spellcheck='false' maxlength='50' tabIndex="0" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='nav'>
|
||||
<div>
|
||||
<button class="button finish" type="submit" tabIndex="0">{{ finishLinkingPhoneButton }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{{/isStep4}}
|
||||
{{#isStep5}}
|
||||
<div id='step5' class='step'>
|
||||
<div class='inner'>
|
||||
<div class='step-body'>
|
||||
<span class='banner-icon sync'></span>
|
||||
<div class='header'>{{ syncing }}</div>
|
||||
</div>
|
||||
<div class='progress'>
|
||||
<div class='bar-container'>
|
||||
<div class='bar progress-bar progress-bar-striped active'></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/isStep5}}
|
||||
|
||||
{{#isError}}
|
||||
<div id='error' class='step'>
|
||||
<div class='inner'>
|
||||
<div class='step-body'>
|
||||
<span class='banner-icon alert-outline'></span>
|
||||
<div class='header'>{{ errorHeader }}</div>
|
||||
<div class='body'>{{ errorMessage }}</div>
|
||||
</div>
|
||||
<div class='nav'>
|
||||
<a class='button try-again'>{{ errorButton }}</a>
|
||||
{{#errorSecondButton}}
|
||||
<a class='button second'>{{ errorSecondButton }}</a>
|
||||
{{/errorSecondButton}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/isError}}
|
||||
</script>
|
||||
|
||||
<script type="text/javascript" src="js/components.js"></script>
|
||||
<script type="text/javascript" src="ts/set_os_class.js"></script>
|
||||
<script
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m15 5h-1v-1h1m-5 1h-1v-1h1m5.53-1.84 1.31-1.31c.19-.19.19-.51 0-.71-.2-.19-.52-.19-.71 0l-1.48 1.48c-.8-.39-1.7-.62-2.65-.62-.96 0-1.86.23-2.66.63l-1.49-1.49c-.19-.19-.51-.19-.7 0-.2.2-.2.52 0 .71l1.31 1.31c-1.49 1.1-2.46 2.84-2.46 4.84h12c0-2-1-3.75-2.47-4.84m4.97 5.84a1.5 1.5 0 0 0 -1.5 1.5v7a1.5 1.5 0 0 0 1.5 1.5 1.5 1.5 0 0 0 1.5-1.5v-7a1.5 1.5 0 0 0 -1.5-1.5m-17 0a1.5 1.5 0 0 0 -1.5 1.5v7a1.5 1.5 0 0 0 1.5 1.5 1.5 1.5 0 0 0 1.5-1.5v-7a1.5 1.5 0 0 0 -1.5-1.5m2.5 10a1 1 0 0 0 1 1h1v3.5a1.5 1.5 0 0 0 1.5 1.5 1.5 1.5 0 0 0 1.5-1.5v-3.5h2v3.5a1.5 1.5 0 0 0 1.5 1.5 1.5 1.5 0 0 0 1.5-1.5v-3.5h1a1 1 0 0 0 1-1v-10h-12z"/></svg>
|
Before Width: | Height: | Size: 723 B |
|
@ -1 +0,0 @@
|
|||
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53-1.71-2.47-3.02-7.02-1.26-10.08.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83m-5.71-16c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>
|
Before Width: | Height: | Size: 546 B |
|
@ -458,7 +458,6 @@ try {
|
|||
require('./ts/backbone/views/whisper_view');
|
||||
require('./ts/views/conversation_view');
|
||||
require('./ts/views/inbox_view');
|
||||
require('./ts/views/install_view');
|
||||
require('./ts/SignalProtocolStore');
|
||||
require('./ts/background');
|
||||
|
||||
|
|
|
@ -323,64 +323,6 @@ $loading-height: 16px;
|
|||
}
|
||||
}
|
||||
|
||||
#qr {
|
||||
display: inline-block;
|
||||
|
||||
&.ready {
|
||||
border: 5px solid $color-ultramarine;
|
||||
box-shadow: 2px 2px 4px $color-black-alpha-40;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 20em;
|
||||
border: 5px solid $color-white;
|
||||
}
|
||||
|
||||
@media (max-height: 475px) {
|
||||
img {
|
||||
width: 8em;
|
||||
height: 8em;
|
||||
}
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 3px solid $color-ultramarine;
|
||||
border-radius: 50%;
|
||||
float: left;
|
||||
margin: 0 6px;
|
||||
transform: scale(0);
|
||||
|
||||
animation: loading 1500ms ease infinite 0ms;
|
||||
&:nth-child(2) {
|
||||
animation: loading 1500ms ease infinite 333ms;
|
||||
}
|
||||
&:nth-child(3) {
|
||||
animation: loading 1500ms ease infinite 666ms;
|
||||
}
|
||||
}
|
||||
|
||||
canvas {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.os-icon {
|
||||
height: 3em;
|
||||
width: 3em;
|
||||
vertical-align: text-bottom;
|
||||
display: inline-block;
|
||||
margin: 0.5em;
|
||||
|
||||
&.apple {
|
||||
@include color-svg('../images/full-screen-flow/apple.svg', black);
|
||||
}
|
||||
&.android {
|
||||
@include color-svg('../images/full-screen-flow/android.svg', black);
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
font-weight: normal;
|
||||
line-height: 1em;
|
||||
|
|
|
@ -613,3 +613,76 @@
|
|||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin normal-input {
|
||||
@include font-body-1;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
border-width: 2px;
|
||||
border-style: solid;
|
||||
width: 100%;
|
||||
|
||||
@include light-theme {
|
||||
background: $color-white;
|
||||
color: $color-black;
|
||||
border-color: $color-gray-15;
|
||||
|
||||
&:disabled {
|
||||
background: $color-gray-02;
|
||||
border-color: $color-gray-05;
|
||||
color: $color-gray-90;
|
||||
}
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
background: $color-gray-80;
|
||||
color: $color-gray-05;
|
||||
border-color: $color-gray-45;
|
||||
|
||||
&:disabled {
|
||||
background: $color-gray-95;
|
||||
border-color: $color-gray-60;
|
||||
color: $color-gray-20;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
|
||||
@include light-theme {
|
||||
border-color: $color-ultramarine;
|
||||
}
|
||||
@include dark-theme {
|
||||
border-color: $color-ultramarine-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin install-screen {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
justify-content: center;
|
||||
line-height: 30px;
|
||||
user-select: none;
|
||||
width: 100vw;
|
||||
|
||||
@include light-theme {
|
||||
background: $color-gray-02;
|
||||
color: $color-black;
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
background: $color-gray-95;
|
||||
color: $color-white;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@include font-title-2;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@include font-body-1;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,47 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.module-GroupInput {
|
||||
@include font-body-1;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
border-width: 2px;
|
||||
border-style: solid;
|
||||
width: 100%;
|
||||
|
||||
@include light-theme {
|
||||
background: $color-white;
|
||||
color: $color-black;
|
||||
border-color: $color-gray-15;
|
||||
|
||||
&:disabled {
|
||||
background: $color-gray-02;
|
||||
border-color: $color-gray-05;
|
||||
color: $color-gray-90;
|
||||
}
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
background: $color-gray-80;
|
||||
color: $color-gray-05;
|
||||
border-color: $color-gray-45;
|
||||
|
||||
&:disabled {
|
||||
background: $color-gray-95;
|
||||
border-color: $color-gray-60;
|
||||
color: $color-gray-20;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
|
||||
@include light-theme {
|
||||
border-color: $color-ultramarine;
|
||||
}
|
||||
@include dark-theme {
|
||||
border-color: $color-ultramarine-light;
|
||||
}
|
||||
}
|
||||
@include normal-input;
|
||||
|
||||
&__description {
|
||||
resize: none;
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.module-InstallScreenChoosingDeviceNameStep {
|
||||
@include install-screen;
|
||||
|
||||
text-align: center;
|
||||
|
||||
&__inputs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 32px 0 16px 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__input {
|
||||
@include normal-input;
|
||||
width: 90%;
|
||||
max-width: 300px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
}
|
21
stylesheets/components/InstallScreenErrorStep.scss
Normal file
21
stylesheets/components/InstallScreenErrorStep.scss
Normal file
|
@ -0,0 +1,21 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.module-InstallScreenErrorStep {
|
||||
@include install-screen;
|
||||
flex-direction: column;
|
||||
padding-left: 2rem;
|
||||
padding-right: 2rem;
|
||||
text-align: center;
|
||||
|
||||
&__buttons {
|
||||
margin-top: 1rem;
|
||||
|
||||
.module-Button {
|
||||
margin-left: 1rem;
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
26
stylesheets/components/InstallScreenLinkInProgressStep.scss
Normal file
26
stylesheets/components/InstallScreenLinkInProgressStep.scss
Normal file
|
@ -0,0 +1,26 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.module-InstallScreenLinkInProgressStep {
|
||||
@include install-screen;
|
||||
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
|
||||
h1 {
|
||||
@include font-body-1-bold;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@include font-subtitle;
|
||||
font-weight: normal;
|
||||
|
||||
@include light-theme {
|
||||
color: $color-gray-60;
|
||||
}
|
||||
@include dark-theme {
|
||||
color: $color-gray-25;
|
||||
}
|
||||
}
|
||||
}
|
136
stylesheets/components/InstallScreenQrCodeNotScannedStep.scss
Normal file
136
stylesheets/components/InstallScreenQrCodeNotScannedStep.scss
Normal file
|
@ -0,0 +1,136 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.module-InstallScreenQrCodeNotScannedStep {
|
||||
@include install-screen;
|
||||
|
||||
&__contents {
|
||||
align-items: center;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
max-width: 760px;
|
||||
padding: 22px;
|
||||
margin: 20px;
|
||||
animation: 500ms module-InstallScreenQrCodeNotScannedStep__slide-in;
|
||||
position: relative;
|
||||
|
||||
@include light-theme {
|
||||
background: $color-white;
|
||||
}
|
||||
@include dark-theme {
|
||||
background: $color-gray-80;
|
||||
}
|
||||
}
|
||||
|
||||
&__qr-code {
|
||||
// This should match the size defined in the JavaScript.
|
||||
$size: 256px;
|
||||
|
||||
align-items: center;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 4px;
|
||||
box-sizing: content-box;
|
||||
display: flex;
|
||||
padding: 8px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin-right: 38px;
|
||||
min-height: $size;
|
||||
min-width: $size;
|
||||
width: $size;
|
||||
|
||||
&--loaded {
|
||||
background: $color-white;
|
||||
}
|
||||
|
||||
&--load-failed {
|
||||
@include font-subtitle;
|
||||
|
||||
@include light-theme {
|
||||
color: $color-gray-60;
|
||||
border-color: $color-gray-05;
|
||||
}
|
||||
@include dark-theme {
|
||||
color: $color-gray-25;
|
||||
border-color: $color-gray-60;
|
||||
}
|
||||
}
|
||||
|
||||
&__code {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: $size;
|
||||
width: $size;
|
||||
animation: 1s module-InstallScreenQrCodeNotScannedStep__slide-in;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__error-message {
|
||||
text-align: center;
|
||||
|
||||
&::before {
|
||||
@include color-svg(
|
||||
'../images/icons/v2/error-outline-24.svg',
|
||||
$color-accent-red
|
||||
);
|
||||
content: '';
|
||||
display: block;
|
||||
height: 22px;
|
||||
margin: 8px auto 0 auto;
|
||||
width: 22px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $color-ultramarine;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
@include font-body-1;
|
||||
line-height: 26px;
|
||||
list-style-position: inside;
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
|
||||
&__android-plus {
|
||||
background: $color-gray-25;
|
||||
border-radius: 100%;
|
||||
display: inline-block;
|
||||
padding: 5px;
|
||||
vertical-align: middle;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
@include light-theme {
|
||||
&::before {
|
||||
@include color-svg('../images/icons/v2/plus-24.svg', $color-white);
|
||||
}
|
||||
}
|
||||
|
||||
@include dark-theme {
|
||||
&::before {
|
||||
@include color-svg('../images/icons/v2/plus-24.svg', $color-gray-80);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes module-InstallScreenQrCodeNotScannedStep__slide-in {
|
||||
from {
|
||||
top: -8px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
top: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
21
stylesheets/components/InstallScreenSignalLogo.scss
Normal file
21
stylesheets/components/InstallScreenSignalLogo.scss
Normal file
|
@ -0,0 +1,21 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.InstallScreenSignalLogo {
|
||||
@include font-title-1;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-weight: bold;
|
||||
position: absolute;
|
||||
top: calc(35px + var(--title-bar-drag-area-height));
|
||||
left: 32px;
|
||||
|
||||
&::before {
|
||||
@include color-svg('../images/signal-logo.svg', $color-ultramarine);
|
||||
content: '';
|
||||
display: block;
|
||||
height: 32px;
|
||||
margin-right: 6px;
|
||||
width: 32px;
|
||||
}
|
||||
}
|
|
@ -70,6 +70,11 @@
|
|||
@import './components/Inbox.scss';
|
||||
@import './components/IncomingCallBar.scss';
|
||||
@import './components/Input.scss';
|
||||
@import './components/InstallScreenChoosingDeviceNameStep.scss';
|
||||
@import './components/InstallScreenErrorStep.scss';
|
||||
@import './components/InstallScreenLinkInProgressStep.scss';
|
||||
@import './components/InstallScreenQrCodeNotScannedStep.scss';
|
||||
@import './components/InstallScreenSignalLogo.scss';
|
||||
@import './components/LeftPaneDialog.scss';
|
||||
@import './components/LeftPaneSearchInput.scss';
|
||||
@import './components/Lightbox.scss';
|
||||
|
|
|
@ -65,102 +65,6 @@
|
|||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-tmpl-mustache" id="link-flow-template">
|
||||
<div class='module-title-bar-drag-area'></div>
|
||||
|
||||
{{#isStep3}}
|
||||
<div id='step3' class='step'>
|
||||
<div class='inner'>
|
||||
<div class='step-body'>
|
||||
<div class='header'>{{ linkYourPhone }}</div>
|
||||
<div id="qr">
|
||||
<div class='container'>
|
||||
<span class='dot'></span>
|
||||
<span class='dot'></span>
|
||||
<span class='dot'></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='nav'>
|
||||
<div class='instructions'>
|
||||
<div class='android'>
|
||||
<div class='label'>
|
||||
<span class='os-icon android'></span>
|
||||
</div>
|
||||
<div class='body'>
|
||||
<div>→ {{ signalSettings }}</div>
|
||||
<div>→ {{ linkedDevices }}</div>
|
||||
<div>→ {{ androidFinalStep }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='apple'>
|
||||
<div class='label'>
|
||||
<span class='os-icon apple'></span>
|
||||
</div>
|
||||
<div class='body'>
|
||||
<div>→ {{ signalSettings }}</div>
|
||||
<div>→ {{ linkedDevices }}</div>
|
||||
<div>→ {{ appleFinalStep }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/isStep3}}
|
||||
{{#isStep4}}
|
||||
<form id='link-phone'>
|
||||
<div id='step4' class='step'>
|
||||
<div class='inner'>
|
||||
<div class='step-body'>
|
||||
<span class='banner-icon lead-pencil'></span>
|
||||
<div class='header'>{{ chooseName }}</div>
|
||||
<div>
|
||||
<input type='text' class='device-name' spellcheck='false' maxlength='50' tabIndex="0" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='nav'>
|
||||
<div>
|
||||
<button class="button finish" type="submit" tabIndex="0">{{ finishLinkingPhoneButton }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{{/isStep4}}
|
||||
{{#isStep5}}
|
||||
<div id='step5' class='step'>
|
||||
<div class='inner'>
|
||||
<div class='step-body'>
|
||||
<span class='banner-icon sync'></span>
|
||||
<div class='header'>{{ syncing }}</div>
|
||||
</div>
|
||||
<div class='progress'>
|
||||
<div class='bar-container'>
|
||||
<div class='bar progress-bar progress-bar-striped active'></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/isStep5}}
|
||||
|
||||
{{#isError}}
|
||||
<div id='error' class='step'>
|
||||
<div class='inner'>
|
||||
<div class='step-body'>
|
||||
<span class='banner-icon alert-outline'></span>
|
||||
<div class='header'>{{ errorHeader }}</div>
|
||||
<div class='body'>{{ errorMessage }}</div>
|
||||
</div>
|
||||
<div class='nav'>
|
||||
<a class='button try-again'>{{ errorButton }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/isError}}
|
||||
</script>
|
||||
|
||||
<script type="text/javascript" src="../js/components.js"></script>
|
||||
<script type="text/javascript" src="../ts/backboneJquery.js"></script>
|
||||
<script
|
||||
|
|
|
@ -8,7 +8,7 @@ import classNames from 'classnames';
|
|||
|
||||
import { AppViewType } from '../state/ducks/app';
|
||||
import { Inbox } from './Inbox';
|
||||
import { Install } from './Install';
|
||||
import { SmartInstallScreen } from '../state/smart/InstallScreen';
|
||||
import { StandaloneRegistration } from './StandaloneRegistration';
|
||||
import { ThemeType } from '../types/Util';
|
||||
import { usePageVisibility } from '../hooks/usePageVisibility';
|
||||
|
@ -50,7 +50,7 @@ export const App = ({
|
|||
let contents;
|
||||
|
||||
if (appView === AppViewType.Installer) {
|
||||
contents = <Install />;
|
||||
contents = <SmartInstallScreen />;
|
||||
} else if (appView === AppViewType.Standalone) {
|
||||
const onComplete = () => {
|
||||
window.removeSetupMenuItems();
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { BackboneHost } from './BackboneHost';
|
||||
|
||||
export const Install = (): JSX.Element => {
|
||||
return (
|
||||
<BackboneHost
|
||||
className="full-screen-flow"
|
||||
View={window.Whisper.InstallView}
|
||||
/>
|
||||
);
|
||||
};
|
64
ts/components/InstallScreen.tsx
Normal file
64
ts/components/InstallScreen.tsx
Normal file
|
@ -0,0 +1,64 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ComponentProps, ReactElement } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
import { InstallScreenErrorStep } from './installScreen/InstallScreenErrorStep';
|
||||
import { InstallScreenChoosingDeviceNameStep } from './installScreen/InstallScreenChoosingDeviceNameStep';
|
||||
import { InstallScreenLinkInProgressStep } from './installScreen/InstallScreenLinkInProgressStep';
|
||||
import { InstallScreenQrCodeNotScannedStep } from './installScreen/InstallScreenQrCodeNotScannedStep';
|
||||
|
||||
export enum InstallScreenStep {
|
||||
Error,
|
||||
QrCodeNotScanned,
|
||||
ChoosingDeviceName,
|
||||
LinkInProgress,
|
||||
}
|
||||
|
||||
// We can't always use destructuring assignment because of the complexity of this props
|
||||
// type.
|
||||
/* eslint-disable react/destructuring-assignment */
|
||||
type PropsType =
|
||||
| {
|
||||
step: InstallScreenStep.Error;
|
||||
screenSpecificProps: ComponentProps<typeof InstallScreenErrorStep>;
|
||||
}
|
||||
| {
|
||||
step: InstallScreenStep.QrCodeNotScanned;
|
||||
screenSpecificProps: ComponentProps<
|
||||
typeof InstallScreenQrCodeNotScannedStep
|
||||
>;
|
||||
}
|
||||
| {
|
||||
step: InstallScreenStep.ChoosingDeviceName;
|
||||
screenSpecificProps: ComponentProps<
|
||||
typeof InstallScreenChoosingDeviceNameStep
|
||||
>;
|
||||
}
|
||||
| {
|
||||
step: InstallScreenStep.LinkInProgress;
|
||||
screenSpecificProps: ComponentProps<
|
||||
typeof InstallScreenLinkInProgressStep
|
||||
>;
|
||||
};
|
||||
|
||||
export function InstallScreen(props: Readonly<PropsType>): ReactElement {
|
||||
switch (props.step) {
|
||||
case InstallScreenStep.Error:
|
||||
return <InstallScreenErrorStep {...props.screenSpecificProps} />;
|
||||
case InstallScreenStep.QrCodeNotScanned:
|
||||
return (
|
||||
<InstallScreenQrCodeNotScannedStep {...props.screenSpecificProps} />
|
||||
);
|
||||
case InstallScreenStep.ChoosingDeviceName:
|
||||
return (
|
||||
<InstallScreenChoosingDeviceNameStep {...props.screenSpecificProps} />
|
||||
);
|
||||
case InstallScreenStep.LinkInProgress:
|
||||
return <InstallScreenLinkInProgressStep {...props.screenSpecificProps} />;
|
||||
default:
|
||||
throw missingCaseError(props);
|
||||
}
|
||||
}
|
9
ts/components/TitlebarDragArea.tsx
Normal file
9
ts/components/TitlebarDragArea.tsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ReactElement } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
export const TitlebarDragArea = (): ReactElement => (
|
||||
<div className="module-title-bar-drag-area" />
|
||||
);
|
|
@ -0,0 +1,36 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { setupI18n } from '../../util/setupI18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
|
||||
import { InstallScreenChoosingDeviceNameStep } from './InstallScreenChoosingDeviceNameStep';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const story = storiesOf(
|
||||
'Components/InstallScreen/InstallScreenChoosingDeviceNameStep',
|
||||
module
|
||||
);
|
||||
|
||||
story.add('Default', () => {
|
||||
const Wrapper = () => {
|
||||
const [deviceName, setDeviceName] = useState<string>('Default value');
|
||||
|
||||
return (
|
||||
<InstallScreenChoosingDeviceNameStep
|
||||
i18n={i18n}
|
||||
deviceName={deviceName}
|
||||
setDeviceName={setDeviceName}
|
||||
onSubmit={action('onSubmit')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return <Wrapper />;
|
||||
});
|
|
@ -0,0 +1,85 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ReactElement } from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import { normalizeDeviceName } from '../../util/normalizeDeviceName';
|
||||
|
||||
import { Button, ButtonVariant } from '../Button';
|
||||
import { TitlebarDragArea } from '../TitlebarDragArea';
|
||||
import { InstallScreenSignalLogo } from './InstallScreenSignalLogo';
|
||||
|
||||
// This is the string's `.length`, which is the number of UTF-16 code points. Instead, we
|
||||
// want this to be either 50 graphemes or 256 encrypted bytes, whichever is smaller. See
|
||||
// DESKTOP-2844.
|
||||
export const MAX_DEVICE_NAME_LENGTH = 50;
|
||||
|
||||
type PropsType = {
|
||||
deviceName: string;
|
||||
i18n: LocalizerType;
|
||||
onSubmit: () => void;
|
||||
setDeviceName: (value: string) => void;
|
||||
};
|
||||
|
||||
export function InstallScreenChoosingDeviceNameStep({
|
||||
deviceName,
|
||||
i18n,
|
||||
onSubmit,
|
||||
setDeviceName,
|
||||
}: Readonly<PropsType>): ReactElement {
|
||||
const hasFocusedRef = useRef<boolean>(false);
|
||||
const focusRef = (el: null | HTMLElement) => {
|
||||
if (el) {
|
||||
el.focus();
|
||||
hasFocusedRef.current = true;
|
||||
}
|
||||
};
|
||||
|
||||
const normalizedName = normalizeDeviceName(deviceName);
|
||||
const canSubmit =
|
||||
normalizedName.length > 0 &&
|
||||
normalizedName.length <= MAX_DEVICE_NAME_LENGTH;
|
||||
|
||||
return (
|
||||
<form
|
||||
className="module-InstallScreenChoosingDeviceNameStep"
|
||||
onSubmit={event => {
|
||||
event.preventDefault();
|
||||
onSubmit();
|
||||
}}
|
||||
>
|
||||
<TitlebarDragArea />
|
||||
|
||||
<InstallScreenSignalLogo />
|
||||
|
||||
<div className="module-InstallScreenChoosingDeviceNameStep__contents">
|
||||
<div className="module-InstallScreenChoosingDeviceNameStep__header">
|
||||
<h1>{i18n('chooseDeviceName')}</h1>
|
||||
<h2>{i18n('Install__choose-device-name__description')}</h2>
|
||||
</div>
|
||||
<div className="module-InstallScreenChoosingDeviceNameStep__inputs">
|
||||
<input
|
||||
className="module-InstallScreenChoosingDeviceNameStep__input"
|
||||
maxLength={MAX_DEVICE_NAME_LENGTH}
|
||||
onChange={event => {
|
||||
setDeviceName(event.target.value);
|
||||
}}
|
||||
placeholder={i18n('Install__choose-device-name__placeholder')}
|
||||
ref={focusRef}
|
||||
spellCheck={false}
|
||||
value={deviceName}
|
||||
/>
|
||||
<Button
|
||||
disabled={!canSubmit}
|
||||
variant={ButtonVariant.Primary}
|
||||
type="submit"
|
||||
>
|
||||
{i18n('finishLinkingPhone')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { setupI18n } from '../../util/setupI18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
|
||||
import { InstallScreenErrorStep, InstallError } from './InstallScreenErrorStep';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const story = storiesOf(
|
||||
'Components/InstallScreen/InstallScreenErrorStep',
|
||||
module
|
||||
);
|
||||
|
||||
const defaultProps = {
|
||||
i18n,
|
||||
quit: action('quit'),
|
||||
tryAgain: action('tryAgain'),
|
||||
};
|
||||
|
||||
story.add('Too many devices', () => (
|
||||
<InstallScreenErrorStep
|
||||
{...defaultProps}
|
||||
error={InstallError.TooManyDevices}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Too old', () => (
|
||||
<InstallScreenErrorStep {...defaultProps} error={InstallError.TooOld} />
|
||||
));
|
||||
|
||||
story.add('Too old', () => (
|
||||
<InstallScreenErrorStep {...defaultProps} error={InstallError.TooOld} />
|
||||
));
|
||||
|
||||
story.add('Connection failed', () => (
|
||||
<InstallScreenErrorStep
|
||||
{...defaultProps}
|
||||
error={InstallError.ConnectionFailed}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Unknown error', () => (
|
||||
<InstallScreenErrorStep {...defaultProps} error={InstallError.UnknownError} />
|
||||
));
|
77
ts/components/installScreen/InstallScreenErrorStep.tsx
Normal file
77
ts/components/installScreen/InstallScreenErrorStep.tsx
Normal file
|
@ -0,0 +1,77 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ReactElement } from 'react';
|
||||
import React from 'react';
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import { openLinkInWebBrowser } from '../../util/openLinkInWebBrowser';
|
||||
import { Button, ButtonVariant } from '../Button';
|
||||
import { TitlebarDragArea } from '../TitlebarDragArea';
|
||||
import { InstallScreenSignalLogo } from './InstallScreenSignalLogo';
|
||||
|
||||
export enum InstallError {
|
||||
TooManyDevices,
|
||||
TooOld,
|
||||
ConnectionFailed,
|
||||
UnknownError,
|
||||
}
|
||||
|
||||
export function InstallScreenErrorStep({
|
||||
error,
|
||||
i18n,
|
||||
quit,
|
||||
tryAgain,
|
||||
}: Readonly<{
|
||||
error: InstallError;
|
||||
i18n: LocalizerType;
|
||||
quit: () => unknown;
|
||||
tryAgain: () => unknown;
|
||||
}>): ReactElement {
|
||||
let errorMessage: string;
|
||||
let buttonText = i18n('installTryAgain');
|
||||
let onClickButton = () => tryAgain();
|
||||
let shouldShowQuitButton = false;
|
||||
|
||||
switch (error) {
|
||||
case InstallError.TooManyDevices:
|
||||
errorMessage = i18n('installTooManyDevices');
|
||||
break;
|
||||
case InstallError.TooOld:
|
||||
errorMessage = i18n('installTooOld');
|
||||
buttonText = i18n('upgrade');
|
||||
onClickButton = () => {
|
||||
openLinkInWebBrowser('https://signal.org/download');
|
||||
};
|
||||
shouldShowQuitButton = true;
|
||||
break;
|
||||
case InstallError.ConnectionFailed:
|
||||
errorMessage = i18n('installConnectionFailed');
|
||||
break;
|
||||
case InstallError.UnknownError:
|
||||
errorMessage = i18n('installUnknownError');
|
||||
break;
|
||||
default:
|
||||
throw missingCaseError(error);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="module-InstallScreenErrorStep">
|
||||
<TitlebarDragArea />
|
||||
|
||||
<InstallScreenSignalLogo />
|
||||
|
||||
<h1>{i18n('installErrorHeader')}</h1>
|
||||
<h2>{errorMessage}</h2>
|
||||
|
||||
<div className="module-InstallScreenErrorStep__buttons">
|
||||
<Button onClick={onClickButton}>{buttonText}</Button>
|
||||
{shouldShowQuitButton && (
|
||||
<Button onClick={() => quit()} variant={ButtonVariant.Secondary}>
|
||||
{i18n('quit')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { storiesOf } from '@storybook/react';
|
||||
|
||||
import { setupI18n } from '../../util/setupI18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
|
||||
import { InstallScreenLinkInProgressStep } from './InstallScreenLinkInProgressStep';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const story = storiesOf(
|
||||
'Components/InstallScreen/InstallScreenLinkInProgressStep',
|
||||
module
|
||||
);
|
||||
|
||||
story.add('Default', () => <InstallScreenLinkInProgressStep i18n={i18n} />);
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ReactElement } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
|
||||
import { Spinner } from '../Spinner';
|
||||
import { TitlebarDragArea } from '../TitlebarDragArea';
|
||||
import { InstallScreenSignalLogo } from './InstallScreenSignalLogo';
|
||||
|
||||
export const InstallScreenLinkInProgressStep = ({
|
||||
i18n,
|
||||
}: Readonly<{ i18n: LocalizerType }>): ReactElement => (
|
||||
<div className="module-InstallScreenLinkInProgressStep">
|
||||
<TitlebarDragArea />
|
||||
|
||||
<InstallScreenSignalLogo />
|
||||
|
||||
<Spinner size="50px" svgSize="normal" />
|
||||
<h1>{i18n('initialSync')}</h1>
|
||||
<h2>{i18n('initialSync__subtitle')}</h2>
|
||||
</div>
|
||||
);
|
|
@ -0,0 +1,91 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { storiesOf } from '@storybook/react';
|
||||
|
||||
import { setupI18n } from '../../util/setupI18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
|
||||
import type { Loadable } from '../../util/loadable';
|
||||
import { LoadingState } from '../../util/loadable';
|
||||
import { InstallScreenQrCodeNotScannedStep } from './InstallScreenQrCodeNotScannedStep';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const story = storiesOf(
|
||||
'Components/InstallScreen/InstallScreenQrCodeNotScannedStep',
|
||||
module
|
||||
);
|
||||
|
||||
const Simulation = ({ finalResult }: { finalResult: Loadable<string> }) => {
|
||||
const [provisioningUrl, setProvisioningUrl] = useState<Loadable<string>>({
|
||||
loadingState: LoadingState.Loading,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
setProvisioningUrl(finalResult);
|
||||
}, 2000);
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [finalResult]);
|
||||
|
||||
return (
|
||||
<InstallScreenQrCodeNotScannedStep
|
||||
i18n={i18n}
|
||||
provisioningUrl={provisioningUrl}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
story.add('QR code loading', () => (
|
||||
<InstallScreenQrCodeNotScannedStep
|
||||
i18n={i18n}
|
||||
provisioningUrl={{
|
||||
loadingState: LoadingState.Loading,
|
||||
}}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('QR code failed to load', () => (
|
||||
<InstallScreenQrCodeNotScannedStep
|
||||
i18n={i18n}
|
||||
provisioningUrl={{
|
||||
loadingState: LoadingState.LoadFailed,
|
||||
error: new Error('uh oh'),
|
||||
}}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('QR code loaded', () => (
|
||||
<InstallScreenQrCodeNotScannedStep
|
||||
i18n={i18n}
|
||||
provisioningUrl={{
|
||||
loadingState: LoadingState.Loaded,
|
||||
value:
|
||||
'https://example.com/fake-signal-link?uuid=56cdd548-e595-4962-9a27-3f1e8210a959&pub_key=SW4gdGhlIHZhc3QsIGRlZXAgZm9yZXN0IG9mIEh5cnVsZS4uLg%3D%3D',
|
||||
}}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Simulated loading', () => (
|
||||
<Simulation
|
||||
finalResult={{
|
||||
loadingState: LoadingState.Loaded,
|
||||
value:
|
||||
'https://example.com/fake-signal-link?uuid=56cdd548-e595-4962-9a27-3f1e8210a959&pub_key=SW4gdGhlIHZhc3QsIGRlZXAgZm9yZXN0IG9mIEh5cnVsZS4uLg%3D%3D',
|
||||
}}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Simulated failure', () => (
|
||||
<Simulation
|
||||
finalResult={{
|
||||
loadingState: LoadingState.LoadFailed,
|
||||
error: new Error('uh oh'),
|
||||
}}
|
||||
/>
|
||||
));
|
|
@ -0,0 +1,152 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ReactElement, ReactNode } from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import type { Loadable } from '../../util/loadable';
|
||||
import { LoadingState } from '../../util/loadable';
|
||||
|
||||
import { Intl } from '../Intl';
|
||||
import { Spinner } from '../Spinner';
|
||||
import { TitlebarDragArea } from '../TitlebarDragArea';
|
||||
import { InstallScreenSignalLogo } from './InstallScreenSignalLogo';
|
||||
import { getClassNamesFor } from '../../util/getClassNamesFor';
|
||||
|
||||
// We can't always use destructuring assignment because of the complexity of this props
|
||||
// type.
|
||||
/* eslint-disable react/destructuring-assignment */
|
||||
type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
provisioningUrl: Loadable<string>;
|
||||
};
|
||||
|
||||
// This should match the size in the CSS.
|
||||
const QR_CODE_SIZE = 256;
|
||||
const QR_CODE_FAILED_LINK =
|
||||
'https://support.signal.org/hc/articles/360007320451#desktop_multiple_device';
|
||||
|
||||
const getQrCodeClassName = getClassNamesFor(
|
||||
'module-InstallScreenQrCodeNotScannedStep__qr-code'
|
||||
);
|
||||
|
||||
export const InstallScreenQrCodeNotScannedStep = ({
|
||||
i18n,
|
||||
provisioningUrl,
|
||||
}: Readonly<PropsType>): ReactElement => (
|
||||
<div className="module-InstallScreenQrCodeNotScannedStep">
|
||||
<TitlebarDragArea />
|
||||
|
||||
<InstallScreenSignalLogo />
|
||||
|
||||
<div className="module-InstallScreenQrCodeNotScannedStep__contents">
|
||||
<QrCode i18n={i18n} {...provisioningUrl} />
|
||||
<div className="module-InstallScreenQrCodeNotScannedStep__instructions">
|
||||
<h1>{i18n('Install__scan-this-code')}</h1>
|
||||
<ol>
|
||||
<li>{i18n('Install__instructions__1')}</li>
|
||||
<li>
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="Install__instructions__2"
|
||||
components={{
|
||||
settings: (
|
||||
<strong>{i18n('Install__instructions__2__settings')}</strong>
|
||||
),
|
||||
linkedDevices: <strong>{i18n('linkedDevices')}</strong>,
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="Install__instructions__3"
|
||||
components={{
|
||||
plusButton: (
|
||||
<div
|
||||
className="module-InstallScreenQrCodeNotScannedStep__android-plus"
|
||||
aria-label="+"
|
||||
/>
|
||||
),
|
||||
linkNewDevice: <strong>{i18n('linkNewDevice')}</strong>,
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
function QrCode(
|
||||
props: Loadable<string> & { i18n: LocalizerType }
|
||||
): ReactElement {
|
||||
const { i18n } = props;
|
||||
|
||||
const qrCodeElRef = useRef<null | HTMLDivElement>(null);
|
||||
|
||||
const valueToRender =
|
||||
props.loadingState === LoadingState.Loaded ? props.value : undefined;
|
||||
|
||||
useEffect(() => {
|
||||
const qrCodeEl = qrCodeElRef.current;
|
||||
if (!qrCodeEl || !valueToRender) {
|
||||
return noop;
|
||||
}
|
||||
|
||||
const qrCode = new window.QRCode(qrCodeEl, {
|
||||
text: valueToRender,
|
||||
width: QR_CODE_SIZE * window.devicePixelRatio,
|
||||
height: QR_CODE_SIZE * window.devicePixelRatio,
|
||||
});
|
||||
|
||||
return qrCode.clear.bind(qrCode);
|
||||
}, [valueToRender]);
|
||||
|
||||
let contents: ReactNode;
|
||||
switch (props.loadingState) {
|
||||
case LoadingState.Loading:
|
||||
contents = <Spinner size="24px" svgSize="small" />;
|
||||
break;
|
||||
case LoadingState.LoadFailed:
|
||||
contents = (
|
||||
<span className={classNames(getQrCodeClassName('__error-message'))}>
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="Install__qr-failed"
|
||||
components={[
|
||||
<a href={QR_CODE_FAILED_LINK}>
|
||||
{i18n('Install__qr-failed__learn-more')}
|
||||
</a>,
|
||||
]}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
break;
|
||||
case LoadingState.Loaded:
|
||||
contents = (
|
||||
<div className={getQrCodeClassName('__code')} ref={qrCodeElRef} />
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw missingCaseError(props);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
getQrCodeClassName(''),
|
||||
props.loadingState === LoadingState.Loaded &&
|
||||
getQrCodeClassName('--loaded'),
|
||||
props.loadingState === LoadingState.LoadFailed &&
|
||||
getQrCodeClassName('--load-failed')
|
||||
)}
|
||||
>
|
||||
{contents}
|
||||
</div>
|
||||
);
|
||||
}
|
10
ts/components/installScreen/InstallScreenSignalLogo.tsx
Normal file
10
ts/components/installScreen/InstallScreenSignalLogo.tsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ReactElement } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
export const InstallScreenSignalLogo = (): ReactElement => (
|
||||
// Because "Signal" should be the same in every language, this is not localized.
|
||||
<div className="InstallScreenSignalLogo">Signal</div>
|
||||
);
|
267
ts/state/smart/InstallScreen.tsx
Normal file
267
ts/state/smart/InstallScreen.tsx
Normal file
|
@ -0,0 +1,267 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ComponentProps, ReactElement } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { getIntl } from '../selectors/user';
|
||||
|
||||
import * as log from '../../logging/log';
|
||||
import type { Loadable } from '../../util/loadable';
|
||||
import { LoadingState } from '../../util/loadable';
|
||||
import { assert } from '../../util/assert';
|
||||
import { explodePromise } from '../../util/explodePromise';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import {
|
||||
InstallScreen,
|
||||
InstallScreenStep,
|
||||
} from '../../components/InstallScreen';
|
||||
import { InstallError } from '../../components/installScreen/InstallScreenErrorStep';
|
||||
import { MAX_DEVICE_NAME_LENGTH } from '../../components/installScreen/InstallScreenChoosingDeviceNameStep';
|
||||
import { HTTPError } from '../../textsecure/Errors';
|
||||
import { isRecord } from '../../util/isRecord';
|
||||
import { normalizeDeviceName } from '../../util/normalizeDeviceName';
|
||||
|
||||
type PropsType = ComponentProps<typeof InstallScreen>;
|
||||
|
||||
type StateType =
|
||||
| {
|
||||
step: InstallScreenStep.Error;
|
||||
error: InstallError;
|
||||
}
|
||||
| {
|
||||
step: InstallScreenStep.QrCodeNotScanned;
|
||||
provisioningUrl: Loadable<string>;
|
||||
}
|
||||
| {
|
||||
step: InstallScreenStep.ChoosingDeviceName;
|
||||
deviceName: string;
|
||||
}
|
||||
| {
|
||||
step: InstallScreenStep.LinkInProgress;
|
||||
};
|
||||
|
||||
const INITIAL_STATE: StateType = {
|
||||
step: InstallScreenStep.QrCodeNotScanned,
|
||||
provisioningUrl: { loadingState: LoadingState.Loading },
|
||||
};
|
||||
|
||||
function getInstallError(err: unknown): InstallError {
|
||||
if (err instanceof HTTPError) {
|
||||
switch (err.code) {
|
||||
case -1:
|
||||
return InstallError.ConnectionFailed;
|
||||
case 409:
|
||||
return InstallError.TooOld;
|
||||
case 411:
|
||||
return InstallError.TooManyDevices;
|
||||
default:
|
||||
return InstallError.UnknownError;
|
||||
}
|
||||
}
|
||||
// AccountManager.registerSecondDevice uses this specific "websocket closed" error
|
||||
// message.
|
||||
if (isRecord(err) && err.message === 'websocket closed') {
|
||||
return InstallError.ConnectionFailed;
|
||||
}
|
||||
return InstallError.UnknownError;
|
||||
}
|
||||
|
||||
export function SmartInstallScreen(): ReactElement {
|
||||
const i18n = useSelector(getIntl);
|
||||
|
||||
const chooseDeviceNamePromiseWrapperRef = useRef(explodePromise<string>());
|
||||
|
||||
const [state, setState] = useState<StateType>(INITIAL_STATE);
|
||||
|
||||
const setProvisioningUrl = useCallback(
|
||||
(value: string) => {
|
||||
setState(currentState => {
|
||||
if (currentState.step !== InstallScreenStep.QrCodeNotScanned) {
|
||||
return currentState;
|
||||
}
|
||||
return {
|
||||
...currentState,
|
||||
provisioningUrl: {
|
||||
loadingState: LoadingState.Loaded,
|
||||
value,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
[setState]
|
||||
);
|
||||
|
||||
const onQrCodeScanned = useCallback(() => {
|
||||
setState(currentState => {
|
||||
if (currentState.step !== InstallScreenStep.QrCodeNotScanned) {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
return {
|
||||
step: InstallScreenStep.ChoosingDeviceName,
|
||||
deviceName: normalizeDeviceName(
|
||||
window.textsecure.storage.user.getDeviceName() ||
|
||||
window.getHostName() ||
|
||||
''
|
||||
).slice(0, MAX_DEVICE_NAME_LENGTH),
|
||||
};
|
||||
});
|
||||
}, [setState]);
|
||||
|
||||
const setDeviceName = useCallback(
|
||||
(deviceName: string) => {
|
||||
setState(currentState => {
|
||||
if (currentState.step !== InstallScreenStep.ChoosingDeviceName) {
|
||||
return currentState;
|
||||
}
|
||||
return {
|
||||
...currentState,
|
||||
deviceName,
|
||||
};
|
||||
});
|
||||
},
|
||||
[setState]
|
||||
);
|
||||
|
||||
const onSubmitDeviceName = useCallback(() => {
|
||||
if (state.step !== InstallScreenStep.ChoosingDeviceName) {
|
||||
return;
|
||||
}
|
||||
|
||||
let deviceName: string = normalizeDeviceName(state.deviceName);
|
||||
if (!deviceName.length) {
|
||||
// This should be impossible, but we have it here just in case.
|
||||
assert(
|
||||
false,
|
||||
'Unexpected empty device name. Falling back to placeholder value'
|
||||
);
|
||||
deviceName = i18n('Install__choose-device-name__placeholder');
|
||||
}
|
||||
chooseDeviceNamePromiseWrapperRef.current.resolve(deviceName);
|
||||
|
||||
setState({ step: InstallScreenStep.LinkInProgress });
|
||||
}, [state, i18n]);
|
||||
|
||||
useEffect(() => {
|
||||
let hasCleanedUp = false;
|
||||
|
||||
const accountManager = window.getAccountManager();
|
||||
assert(accountManager, 'Expected an account manager');
|
||||
|
||||
const updateProvisioningUrl = (value: string): void => {
|
||||
if (hasCleanedUp) {
|
||||
return;
|
||||
}
|
||||
setProvisioningUrl(value);
|
||||
};
|
||||
|
||||
const confirmNumber = async (): Promise<string> => {
|
||||
if (hasCleanedUp) {
|
||||
throw new Error('Cannot confirm number; the component was unmounted');
|
||||
}
|
||||
onQrCodeScanned();
|
||||
|
||||
if (window.CI) {
|
||||
chooseDeviceNamePromiseWrapperRef.current.resolve(window.CI.deviceName);
|
||||
}
|
||||
|
||||
const result = await chooseDeviceNamePromiseWrapperRef.current.promise;
|
||||
|
||||
if (hasCleanedUp) {
|
||||
throw new Error('Cannot confirm number; the component was unmounted');
|
||||
}
|
||||
|
||||
// Delete all data from the database unless we're in the middle of a re-link.
|
||||
// Without this, the app restarts at certain times and can cause weird things to
|
||||
// happen, like data from a previous light import showing up after a new install.
|
||||
const shouldRetainData = window.Signal.Util.Registration.everDone();
|
||||
if (!shouldRetainData) {
|
||||
try {
|
||||
await window.textsecure.storage.protocol.removeAllData();
|
||||
} catch (error) {
|
||||
log.error(
|
||||
'confirmNumber: error clearing database',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasCleanedUp) {
|
||||
throw new Error('Cannot confirm number; the component was unmounted');
|
||||
}
|
||||
|
||||
window.Signal.Util.postLinkExperience.start();
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await accountManager.registerSecondDevice(
|
||||
updateProvisioningUrl,
|
||||
confirmNumber
|
||||
);
|
||||
} catch (err: unknown) {
|
||||
if (hasCleanedUp) {
|
||||
return;
|
||||
}
|
||||
setState({
|
||||
step: InstallScreenStep.Error,
|
||||
error: getInstallError(err),
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
hasCleanedUp = true;
|
||||
};
|
||||
}, [setProvisioningUrl, onQrCodeScanned]);
|
||||
|
||||
let props: PropsType;
|
||||
|
||||
switch (state.step) {
|
||||
case InstallScreenStep.Error:
|
||||
props = {
|
||||
step: InstallScreenStep.Error,
|
||||
screenSpecificProps: {
|
||||
i18n,
|
||||
error: state.error,
|
||||
quit: () => window.shutdown(),
|
||||
tryAgain: () => setState(INITIAL_STATE),
|
||||
},
|
||||
};
|
||||
break;
|
||||
case InstallScreenStep.QrCodeNotScanned:
|
||||
props = {
|
||||
step: InstallScreenStep.QrCodeNotScanned,
|
||||
screenSpecificProps: {
|
||||
i18n,
|
||||
provisioningUrl: state.provisioningUrl,
|
||||
},
|
||||
};
|
||||
break;
|
||||
case InstallScreenStep.ChoosingDeviceName:
|
||||
props = {
|
||||
step: InstallScreenStep.ChoosingDeviceName,
|
||||
screenSpecificProps: {
|
||||
i18n,
|
||||
deviceName: state.deviceName,
|
||||
setDeviceName,
|
||||
onSubmit: onSubmitDeviceName,
|
||||
},
|
||||
};
|
||||
break;
|
||||
case InstallScreenStep.LinkInProgress:
|
||||
props = {
|
||||
step: InstallScreenStep.LinkInProgress,
|
||||
screenSpecificProps: { i18n },
|
||||
};
|
||||
break;
|
||||
default:
|
||||
throw missingCaseError(state);
|
||||
}
|
||||
|
||||
return <InstallScreen {...props} />;
|
||||
}
|
22
ts/test-both/util/normalizeDeviceName_test.ts
Normal file
22
ts/test-both/util/normalizeDeviceName_test.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { normalizeDeviceName } from '../../util/normalizeDeviceName';
|
||||
|
||||
describe('normalizeDeviceName', () => {
|
||||
it('leaves normal device names untouched', () => {
|
||||
for (const name of ['foo', 'bar Baz', '💅💅💅']) {
|
||||
assert.strictEqual(normalizeDeviceName(name), name);
|
||||
}
|
||||
});
|
||||
|
||||
it('trims device names', () => {
|
||||
assert.strictEqual(normalizeDeviceName(' foo\t'), 'foo');
|
||||
});
|
||||
|
||||
it('removes null characters', () => {
|
||||
assert.strictEqual(normalizeDeviceName('\0foo\0bar'), 'foobar');
|
||||
});
|
||||
});
|
|
@ -7756,6 +7756,22 @@
|
|||
"updated": "2019-11-01T22:46:33.013Z",
|
||||
"reasonDetail": "Used for setting focus only"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/installScreen/InstallScreenChoosingDeviceNameStep.tsx",
|
||||
"line": " const hasFocusedRef = useRef<boolean>(false);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-12-06T23:07:28.947Z",
|
||||
"reasonDetail": "Doesn't touch the DOM."
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/installScreen/InstallScreenQrCodeNotScannedStep.tsx",
|
||||
"line": " const qrCodeElRef = useRef<null | HTMLDivElement>(null);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-12-06T23:07:28.947Z",
|
||||
"reasonDetail": "Uses our QR code library."
|
||||
},
|
||||
{
|
||||
"rule": "React-createRef",
|
||||
"path": "ts/components/stickers/StickerManager.js",
|
||||
|
@ -8020,6 +8036,14 @@
|
|||
"reasonCategory": "falseMatch",
|
||||
"updated": "2021-11-04T16:14:03.477Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/state/smart/InstallScreen.tsx",
|
||||
"line": " const chooseDeviceNamePromiseWrapperRef = useRef(explodePromise<string>());",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-12-06T23:07:28.947Z",
|
||||
"reasonDetail": "Doesn't touch the DOM."
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-load(",
|
||||
"path": "ts/types/Stickers.js",
|
||||
|
@ -8310,244 +8334,6 @@
|
|||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/install_view.js",
|
||||
"line": " template: () => $('#link-flow-template').html(),",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/install_view.js",
|
||||
"line": " this.$('#qr img').remove();",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/install_view.js",
|
||||
"line": " this.$('#qr canvas').remove();",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/install_view.js",
|
||||
"line": " this.$('#qr .container').show();",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/install_view.js",
|
||||
"line": " this.$('#qr').removeClass('ready');",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/install_view.js",
|
||||
"line": " if ($('#qr').length === 0) {",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/install_view.js",
|
||||
"line": " this.$('#qr .container').hide();",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/install_view.js",
|
||||
"line": " this.qr = new window.QRCode(this.$('#qr')[0]).makeCode(url);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/install_view.js",
|
||||
"line": " this.$('#qr').removeAttr('title');",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/install_view.js",
|
||||
"line": " this.$('#qr').addClass('ready');",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/install_view.js",
|
||||
"line": " this.$(DEVICE_NAME_SELECTOR).val(deviceName || window.getHostName());",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/install_view.js",
|
||||
"line": " this.$(DEVICE_NAME_SELECTOR).focus();",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/install_view.js",
|
||||
"line": " this.$('#link-phone').submit((e) => {",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/install_view.js",
|
||||
"line": " let name = this.$(DEVICE_NAME_SELECTOR).val();",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/install_view.js",
|
||||
"line": " this.$(DEVICE_NAME_SELECTOR).focus();",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/install_view.js",
|
||||
"line": " this.$('#qr img').attr('alt', window.i18n('LinkScreen__scan-this-code'));",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-28T00:13:42.086Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-html(",
|
||||
"path": "ts/views/install_view.js",
|
||||
"line": " template: () => $('#link-flow-template').html(),",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/install_view.ts",
|
||||
"line": " template: () => $('#link-flow-template').html(),",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/install_view.ts",
|
||||
"line": " this.$('#qr img').remove();",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/install_view.ts",
|
||||
"line": " this.$('#qr canvas').remove();",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/install_view.ts",
|
||||
"line": " this.$('#qr .container').show();",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/install_view.ts",
|
||||
"line": " this.$('#qr').removeClass('ready');",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/install_view.ts",
|
||||
"line": " if ($('#qr').length === 0) {",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/install_view.ts",
|
||||
"line": " this.$('#qr .container').hide();",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/install_view.ts",
|
||||
"line": " this.qr = new window.QRCode(this.$('#qr')[0]).makeCode(url);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/install_view.ts",
|
||||
"line": " this.$('#qr').removeAttr('title');",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/install_view.ts",
|
||||
"line": " this.$('#qr').addClass('ready');",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/install_view.ts",
|
||||
"line": " this.$(DEVICE_NAME_SELECTOR).val(deviceName || window.getHostName());",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/install_view.ts",
|
||||
"line": " this.$(DEVICE_NAME_SELECTOR).focus();",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/install_view.ts",
|
||||
"line": " this.$('#link-phone').submit((e: SubmitEvent) => {",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/install_view.ts",
|
||||
"line": " let name = this.$(DEVICE_NAME_SELECTOR).val();",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/install_view.ts",
|
||||
"line": " this.$(DEVICE_NAME_SELECTOR).focus();",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "ts/views/install_view.ts",
|
||||
"line": " this.$('#qr img').attr('alt', window.i18n('LinkScreen__scan-this-code'));",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-28T00:13:42.086Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-html(",
|
||||
"path": "ts/views/install_view.ts",
|
||||
"line": " template: () => $('#link-flow-template').html(),",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-09-15T21:07:50.995Z"
|
||||
},
|
||||
{
|
||||
"rule": "DOM-innerHTML",
|
||||
"path": "ts/windows/loading/start.js",
|
||||
|
|
15
ts/util/loadable.ts
Normal file
15
ts/util/loadable.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
export enum LoadingState {
|
||||
Loading,
|
||||
Loaded,
|
||||
LoadFailed,
|
||||
}
|
||||
|
||||
export type Loadable<ValueT, ErrorT = unknown> =
|
||||
| {
|
||||
loadingState: LoadingState.Loading;
|
||||
}
|
||||
| { loadingState: LoadingState.Loaded; value: ValueT }
|
||||
| { loadingState: LoadingState.LoadFailed; error: ErrorT };
|
7
ts/util/normalizeDeviceName.ts
Normal file
7
ts/util/normalizeDeviceName.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
export function normalizeDeviceName(rawDeviceName: string): string {
|
||||
// We want to do additional normalization here. See DESKTOP-2845.
|
||||
return rawDeviceName.trim().replace(/\0/g, '');
|
||||
}
|
|
@ -1,236 +0,0 @@
|
|||
// Copyright 2015-2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as log from '../logging/log';
|
||||
import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser';
|
||||
import { HTTPError } from '../textsecure/Errors';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
const { Whisper } = window;
|
||||
|
||||
enum Steps {
|
||||
INSTALL_SIGNAL = 2,
|
||||
SCAN_QR_CODE = 3,
|
||||
ENTER_NAME = 4,
|
||||
PROGRESS_BAR = 5,
|
||||
TOO_MANY_DEVICES = 'TooManyDevices',
|
||||
NETWORK_ERROR = 'NetworkError',
|
||||
}
|
||||
|
||||
const DEVICE_NAME_SELECTOR = 'input.device-name';
|
||||
const CONNECTION_ERROR = -1;
|
||||
const TOO_MANY_DEVICES = 411;
|
||||
const TOO_OLD = 409;
|
||||
|
||||
Whisper.InstallView = Whisper.View.extend({
|
||||
template: () => $('#link-flow-template').html(),
|
||||
className: 'main full-screen-flow',
|
||||
events: {
|
||||
'click .try-again': 'connect',
|
||||
'click .second': 'shutdown',
|
||||
// the actual next step happens in confirmNumber() on submit form #link-phone
|
||||
},
|
||||
initialize(options: { hasExistingData?: boolean } = {}) {
|
||||
window.readyForUpdates();
|
||||
|
||||
this.selectStep(Steps.SCAN_QR_CODE);
|
||||
this.connect();
|
||||
this.on('disconnected', this.reconnect);
|
||||
|
||||
// Keep data around if it's a re-link, or the middle of a light import
|
||||
this.shouldRetainData =
|
||||
window.Signal.Util.Registration.everDone() || options.hasExistingData;
|
||||
},
|
||||
render_attributes() {
|
||||
let errorMessage;
|
||||
let errorButton = window.i18n('installTryAgain');
|
||||
let errorSecondButton = null;
|
||||
|
||||
if (this.error) {
|
||||
if (
|
||||
this.error instanceof HTTPError &&
|
||||
this.error.code === TOO_MANY_DEVICES
|
||||
) {
|
||||
errorMessage = window.i18n('installTooManyDevices');
|
||||
} else if (
|
||||
this.error instanceof HTTPError &&
|
||||
this.error.code === TOO_OLD
|
||||
) {
|
||||
errorMessage = window.i18n('installTooOld');
|
||||
errorButton = window.i18n('upgrade');
|
||||
errorSecondButton = window.i18n('quit');
|
||||
} else if (
|
||||
this.error instanceof HTTPError &&
|
||||
this.error.code === CONNECTION_ERROR
|
||||
) {
|
||||
errorMessage = window.i18n('installConnectionFailed');
|
||||
} else if (this.error.message === 'websocket closed') {
|
||||
// AccountManager.registerSecondDevice uses this specific
|
||||
// 'websocket closed' error message
|
||||
errorMessage = window.i18n('installConnectionFailed');
|
||||
}
|
||||
|
||||
return {
|
||||
isError: true,
|
||||
errorHeader: window.i18n('installErrorHeader'),
|
||||
errorMessage,
|
||||
errorButton,
|
||||
errorSecondButton,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isStep3: this.step === Steps.SCAN_QR_CODE,
|
||||
linkYourPhone: window.i18n('linkYourPhone'),
|
||||
signalSettings: window.i18n('signalSettings'),
|
||||
linkedDevices: window.i18n('linkedDevices'),
|
||||
androidFinalStep: window.i18n('plusButton'),
|
||||
appleFinalStep: window.i18n('linkNewDevice'),
|
||||
|
||||
isStep4: this.step === Steps.ENTER_NAME,
|
||||
chooseName: window.i18n('chooseDeviceName'),
|
||||
finishLinkingPhoneButton: window.i18n('finishLinkingPhone'),
|
||||
|
||||
isStep5: this.step === Steps.PROGRESS_BAR,
|
||||
syncing: window.i18n('initialSync'),
|
||||
};
|
||||
},
|
||||
selectStep(step: Steps) {
|
||||
this.step = step;
|
||||
this.render();
|
||||
},
|
||||
shutdown() {
|
||||
window.shutdown();
|
||||
},
|
||||
async connect() {
|
||||
if (this.error instanceof HTTPError && this.error.code === TOO_OLD) {
|
||||
openLinkInWebBrowser('https://signal.org/download');
|
||||
return;
|
||||
}
|
||||
|
||||
this.error = null;
|
||||
this.selectStep(Steps.SCAN_QR_CODE);
|
||||
this.clearQR();
|
||||
if (this.timeout) {
|
||||
clearTimeout(this.timeout);
|
||||
this.timeout = null;
|
||||
}
|
||||
|
||||
const accountManager = window.getAccountManager();
|
||||
|
||||
try {
|
||||
await accountManager.registerSecondDevice(
|
||||
this.setProvisioningUrl.bind(this),
|
||||
this.confirmNumber.bind(this)
|
||||
);
|
||||
} catch (err) {
|
||||
this.handleDisconnect(err);
|
||||
}
|
||||
},
|
||||
handleDisconnect(error: Error) {
|
||||
log.error(
|
||||
'provisioning failed',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
|
||||
this.error = error;
|
||||
this.render();
|
||||
|
||||
if (error.message === 'websocket closed') {
|
||||
this.trigger('disconnected');
|
||||
} else if (
|
||||
!(error instanceof HTTPError) ||
|
||||
(error.code !== CONNECTION_ERROR && error.code !== TOO_MANY_DEVICES)
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
reconnect() {
|
||||
if (this.timeout) {
|
||||
clearTimeout(this.timeout);
|
||||
this.timeout = null;
|
||||
}
|
||||
this.timeout = setTimeout(this.connect.bind(this), 10000);
|
||||
},
|
||||
clearQR() {
|
||||
this.$('#qr img').remove();
|
||||
this.$('#qr canvas').remove();
|
||||
this.$('#qr .container').show();
|
||||
this.$('#qr').removeClass('ready');
|
||||
},
|
||||
setProvisioningUrl(url: string) {
|
||||
if ($('#qr').length === 0) {
|
||||
log.error('Did not find #qr element in the DOM!');
|
||||
return;
|
||||
}
|
||||
|
||||
this.clearQR();
|
||||
this.$('#qr .container').hide();
|
||||
this.qr = new window.QRCode(this.$('#qr')[0]).makeCode(url);
|
||||
this.$('#qr').removeAttr('title');
|
||||
this.$('#qr').addClass('ready');
|
||||
this.$('#qr img').attr('alt', window.i18n('LinkScreen__scan-this-code'));
|
||||
},
|
||||
setDeviceNameDefault() {
|
||||
const deviceName = window.textsecure.storage.user.getDeviceName();
|
||||
|
||||
this.$(DEVICE_NAME_SELECTOR).val(deviceName || window.getHostName());
|
||||
this.$(DEVICE_NAME_SELECTOR).focus();
|
||||
},
|
||||
confirmNumber() {
|
||||
window.removeSetupMenuItems();
|
||||
this.selectStep(Steps.ENTER_NAME);
|
||||
this.setDeviceNameDefault();
|
||||
|
||||
return new Promise(resolve => {
|
||||
const onDeviceName = async (name: string) => {
|
||||
this.selectStep(Steps.PROGRESS_BAR);
|
||||
|
||||
const finish = () => {
|
||||
window.Signal.Util.postLinkExperience.start();
|
||||
return resolve(name);
|
||||
};
|
||||
|
||||
// Delete all data from database unless we're in the middle
|
||||
// of a re-link, or we are finishing a light import. Without this,
|
||||
// app restarts at certain times can cause weird things to happen,
|
||||
// like data from a previous incomplete light import showing up
|
||||
// after a new install.
|
||||
if (this.shouldRetainData) {
|
||||
return finish();
|
||||
}
|
||||
|
||||
try {
|
||||
await window.textsecure.storage.protocol.removeAllData();
|
||||
} catch (error) {
|
||||
log.error(
|
||||
'confirmNumber: error clearing database',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
} finally {
|
||||
finish();
|
||||
}
|
||||
};
|
||||
|
||||
if (window.CI) {
|
||||
onDeviceName(window.CI.deviceName);
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
this.$('#link-phone').submit((e: SubmitEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
let name = this.$(DEVICE_NAME_SELECTOR).val();
|
||||
name = name.replace(/\0/g, ''); // strip unicode null
|
||||
if (name.trim().length === 0) {
|
||||
this.$(DEVICE_NAME_SELECTOR).focus();
|
||||
return;
|
||||
}
|
||||
|
||||
onDeviceName(name);
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
30
ts/window.d.ts
vendored
30
ts/window.d.ts
vendored
|
@ -129,6 +129,32 @@ type ConfirmationDialogViewProps = {
|
|||
resolve: () => void;
|
||||
};
|
||||
|
||||
declare enum QRCodeCorrectLevel {
|
||||
H = 2,
|
||||
L = 1,
|
||||
M = 0,
|
||||
Q = 3,
|
||||
}
|
||||
|
||||
declare class QRCode {
|
||||
static CorrectLevel: typeof QRCodeCorrectLevel;
|
||||
constructor(
|
||||
el: HTMLElement | string,
|
||||
vOption?:
|
||||
| string
|
||||
| {
|
||||
colorDark?: string;
|
||||
colorLight?: string;
|
||||
correctLevel?: QRCodeCorrectLevel;
|
||||
height?: number;
|
||||
text?: string;
|
||||
width?: number;
|
||||
}
|
||||
);
|
||||
makeCode(sText: string): void;
|
||||
clear(): void;
|
||||
}
|
||||
|
||||
export declare class WebAudioRecorderClass {
|
||||
constructor(
|
||||
node: GainNode,
|
||||
|
@ -158,7 +184,6 @@ declare global {
|
|||
interface Window {
|
||||
startApp: () => void;
|
||||
|
||||
QRCode: any;
|
||||
removeSetupMenuItems: () => unknown;
|
||||
showPermissionsPopup: () => Promise<void>;
|
||||
|
||||
|
@ -266,6 +291,8 @@ declare global {
|
|||
updateTrayIcon: (count: number) => void;
|
||||
Backbone: typeof Backbone;
|
||||
CI?: CI;
|
||||
QRCode: typeof QRCode;
|
||||
|
||||
Accessibility: {
|
||||
reducedMotionSetting: boolean;
|
||||
};
|
||||
|
@ -580,7 +607,6 @@ export type WhisperType = {
|
|||
ConversationLoadingScreen: typeof AnyViewClass;
|
||||
GroupMemberList: typeof AnyViewClass;
|
||||
InboxView: typeof AnyViewClass;
|
||||
InstallView: typeof AnyViewClass;
|
||||
KeyVerificationPanelView: typeof AnyViewClass;
|
||||
ReactWrapperView: typeof BasicReactWrapperViewClass;
|
||||
SafetyNumberChangeDialogView: typeof AnyViewClass;
|
||||
|
|
Loading…
Add table
Reference in a new issue