Merge branch 'brand-redesign' into brand-redesign

This commit is contained in:
Vince 2019-12-17 11:04:15 +11:00 committed by GitHub
commit 9cd27abf31
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 1612 additions and 154 deletions

View file

@ -2289,5 +2289,77 @@
"example": ""
}
}
},
"createAccount": {
"message": "Create Account"
},
"signIn": {
"message": "Sign In"
},
"yourUniqueSessionID": {
"message": "Your Unique Session ID"
},
"allUsersAreRandomly...": {
"message":
"All users are randomly generated a set of numbers that act as their unique Session ID. Share your Session ID in order to chat with your friends!"
},
"getStarted": {
"message": "Get started"
},
"generateSessionID": {
"message": "Generate Session ID"
},
"mnemonicSeed": {
"message": "Mnemonic Seed"
},
"enterSeed": {
"message": "Enter Seed"
},
"displayName": {
"message": "Display Name"
},
"enterDisplayName": {
"message": "Enter Display Name / Alias"
},
"optionalPassword": {
"message": "Optional Password"
},
"enterOptionalPassword": {
"message": "Enter Optional Password"
},
"verifyPassword": {
"message": "Verify Password"
},
"devicePairingHeader": {
"message":
"Open the Loki Messenger App on your primary device and select Device Pairing from the main menu. Then, enter your Session ID below to sign in."
},
"enterSessionIDHere": {
"message": "Enter your Session ID here"
},
"continueYourSession": {
"message": "Continue Your Session"
},
"restoreUsingSeed": {
"message": "Restore Using Seed"
},
"linkDeviceToExistingAccount": {
"message": "Link Device To Existing Account"
},
"or": {
"message": "or"
},
"ByUsingThiService...": {
"message":
"By using this service, you agree to our <a>Terms and Conditions</a> and <a>Privacy Statement</a>"
},
"beginYourSession": {
"message": "Begin<br />your<br />Session."
},
"welcomeToYourSession": {
"message": "Welcome to your Session!"
},
"completeSignUp": {
"message": "Complete Sign Up"
}
}

View file

@ -667,93 +667,6 @@
{{/isError}}
</script>
<script type='text/x-tmpl-mustache' id='standalone'>
<div class='step standalone'>
<div class='inner'>
<div class='step-body'>
<div class='header'>Create your Loki Messenger Account</div>
<!-- Registration -->
<div class='page'>
<!-- New account -->
<h4 class='section-toggle section-toggle-visible'>Register a new account</h4>
<div class='standalone-register section-content'>
<div class='standalone-register-warning'>
Please save the seed below in a safe location.
</br>
They can be used to restore your account if you lose access or migrate to a new device.
</div>
<div class='standalone-register-language'>
<span>Language:</span>
<div class='select-container'>
<select id='mnemonic-display-language'></select>
</div>
</div>
<div id='mnemonic-display' />
<div class='standalone-register-buttons'>
<a class='button grey' id='generate-mnemonic'>Generate Seed</a>
<a class='button grey' id='copy-mnemonic'>Copy Seed</a>
<a class='button' id='register' data-loading-text='Please wait...'>Register</a>
</div>
</div>
<!-- Restore using seed -->
<h4 class='section-toggle'>Restore using seed</h4>
<div class='standalone-mnemonic section-content'>
<div class='standalone-mnemonic-inputs'>
<input class='form-control' type='text' id='mnemonic' placeholder='Mnemonic Seed' autocomplete='off' spellcheck='false' />
</div>
<div id='error' class='collapse'></div>
<div class='restore standalone-register-language'>
<span>Language:</span>
<div class='select-container'>
<select id='mnemonic-language'></select>
</div>
</div>
<a class='button' id='register-mnemonic'>Restore</a>
<div id=status></div>
</div>
<!-- Link device to an existing account -->
<h4 class='section-toggle'>Link device to an existing account</h4>
<div class='standalone-secondary-device section-content'>
<p class='standalone-register-warning'>
You will need your Primary Device handy during this step.
</br>
Open the Loki Messenger App on your Primary Device
</br>
and select "Device Pairing" in the main menu.
</p>
<div class='standalone-secondary-device-inputs'>
<input class='form-control' type='text' id='primary-pubkey' placeholder='Primary Account Public Key' autocomplete='off' spellcheck='false' />
</div>
<div id='pubkey' class='collapse'></div>
<div id='error' class='collapse'></div>
<button type='button' class='button' id='register-secondary-device'>Link</button>
<button type='button' class='button' id='cancel-secondary-device' style="display: none;">Cancel</button>
</div>
</div>
<!-- Profile -->
<div class='page'>
<div class='display-name-input'>
<div class='input-header'>Enter your public display name</div>
<input class='form-control' type='text' id='display-name' placeholder='Display Name' autocomplete='off' spellcheck='false' maxlength='25'>
</div>
<div class='password-inputs'>
<div class='input-header'>Type an optional password for added security</div>
<input class='form-control' type='password' id='password' placeholder='Password (optional)' autocomplete='off' spellcheck='false'>
<input class='form-control' type='password' id='password-confirmation' placeholder='Retype your password' autocomplete='off' spellcheck='false'>
<div class='error'></div>
</div>
<div class='buttons'>
<button class='button grey' id='back-button' tabindex='2'>Back</button>
<button class='button' id='save-button' tabindex='1'>Save</button>
</div>
</div>
</div>
</div>
</div>
</script>
<script type='text/javascript' src='js/components.js'></script>
<script type='text/javascript' src='js/reliable_trigger.js'></script>
@ -814,7 +727,7 @@
<script type='text/javascript' src='js/views/install_view.js'></script>
<script type='text/javascript' src='js/views/banner_view.js'></script>
<script type="text/javascript" src="js/views/phone-input-view.js"></script>
<script type='text/javascript' src='js/views/standalone_registration_view.js'></script>
<script type='text/javascript' src='js/views/session_registration_view.js'></script>
<script type='text/javascript' src='js/views/app_view.js'></script>
<script type='text/javascript' src='js/views/import_view.js'></script>
<script type='text/javascript' src='js/views/clear_data_view.js'></script>
@ -825,7 +738,7 @@
<script type='text/javascript' src='js/views/edit_profile_dialog_view.js'></script>
<script type='text/javascript' src='js/views/invite_friends_dialog_view.js'></script>
<script type='text/javascript' src='js/views/user_details_dialog_view.js'></script>
<script type='text/javascript' src='js/wall_clock_listener.js'></script>
<script type='text/javascript' src='js/rotate_signed_prekey_listener.js'></script>
<script type='text/javascript' src='js/keychange_listener.js'></script>

31
images/session/brand.svg Normal file
View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 404.08533 448.40668"
height="448.40668"
width="404.08533"
xml:space="preserve"
id="svg2"
version="1.1"><metadata
id="metadata8"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
id="defs6"><clipPath
id="clipPath18"
clipPathUnits="userSpaceOnUse"><path
id="path16"
d="M 0,336.305 H 303.064 V 0 H 0 Z" /></clipPath></defs><g
transform="matrix(1.3333333,0,0,-1.3333333,0,448.40667)"
id="g10"><g
id="g12"><g
clip-path="url(#clipPath18)"
id="g14"><g
transform="translate(216.4556,21.0229)"
id="g20"><path
id="path22"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 0,0 h -147.251 c -25.182,0 -46.881,19.311 -48.123,44.461 -1.328,26.885 20.162,49.182 46.765,49.182 h 84.804 c 5.189,0 9.395,4.206 9.395,9.394 v 69.229 L 29.025,126.032 C 51.087,113.807 64.975,90.781 65.565,65.646 66.415,29.551 36.104,0 0,0 m -158.872,168.227 c -22.062,12.225 -35.95,35.251 -36.541,60.386 -0.849,36.095 29.462,65.646 65.565,65.646 H 17.403 c 25.182,0 46.881,-19.311 48.124,-44.462 1.328,-26.884 -20.162,-49.181 -46.764,-49.181 0,0 -60.987,-0.01 -84.808,-0.014 -5.186,-0.001 -9.374,-4.206 -9.376,-9.392 l -0.016,-69.217 z M 39.213,144.42 -24.265,179.593 h 43.028 c 37.408,0 67.845,30.434 67.845,67.843 0,37.408 -30.437,67.846 -67.845,67.846 h -150.224 c -46.864,0 -84.995,-38.131 -84.995,-84.995 0,-33.425 18.162,-64.248 47.396,-80.449 l 63.477,-35.172 h -43.026 c -37.41,0 -67.847,-30.435 -67.847,-67.843 0,-37.408 30.437,-67.846 67.847,-67.846 H 1.613 c 46.866,0 84.995,38.13 84.995,84.995 0,33.425 -18.162,64.248 -47.395,80.448" /></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -321,7 +321,7 @@
window.Events = {
getDeviceName: () => textsecure.storage.user.getDeviceName(),
getThemeSetting: () => storage.get('theme-setting', 'light'),
getThemeSetting: () => storage.get('theme-setting', 'dark'),
setThemeSetting: value => {
storage.put('theme-setting', value);
onChangeTheme();
@ -953,6 +953,12 @@
}
});
Whisper.events.on('openInbox', () => {
appView.openInbox({
initialLoadComplete,
});
});
Whisper.events.on('onEditProfile', async () => {
const ourNumber = window.storage.get('primaryDevicePubKey');
const conversation = await ConversationController.getOrCreateAndWait(

View file

@ -51,6 +51,9 @@ const {
const { EditProfileDialog } = require('../../ts/components/EditProfileDialog');
const { UserDetailsDialog } = require('../../ts/components/UserDetailsDialog');
const { SessionToast } = require('../../ts/components/session/SessionToast');
const {
SessionRegistrationView,
} = require('../../ts/components/session/SessionRegistrationView');
const {
UpdateGroupDialog,
@ -240,6 +243,7 @@ exports.setup = (options = {}) => {
CreateGroupDialog,
EditProfileDialog,
UserDetailsDialog,
SessionRegistrationView,
ConfirmDialog,
UpdateGroupDialog,
InviteFriendsDialog,

View file

@ -105,7 +105,7 @@
openStandalone() {
window.addSetupMenuItems();
this.resetViews();
this.standaloneView = new Whisper.StandaloneRegistrationView();
this.standaloneView = new Whisper.SessionRegistrationView();
this.openView(this.standaloneView);
},
closeStandalone() {

View file

@ -0,0 +1,133 @@
/* eslint-disable no-plusplus */
/* global
Whisper,
*/
/* eslint-disable more/no-then */
// eslint-disable-next-line func-names
(function() {
'use strict';
window.Whisper = window.Whisper || {};
Whisper.SessionRegistrationView = Whisper.View.extend({
className: 'session-fullscreen',
initialize() {
this.render();
},
render() {
this.session_registration_view = new Whisper.ReactWrapperView({
className: 'session-full-screen-flow session-fullscreen',
Component: window.Signal.Components.SessionRegistrationView,
props: {},
});
this.$el.append(this.session_registration_view.el);
return this;
},
log(s) {
window.log.info(s);
this.$('#status').text(s);
},
displayError(error) {
this.$('#error')
.hide()
.text(error)
.addClass('in')
.fadeIn();
},
showToast(message) {
const toast = new Whisper.MessageToastView({
message,
});
toast.$el.appendTo(this.$el);
toast.render();
},
});
class TextScramble {
constructor(el) {
this.el = el;
this.chars = '0123456789qwertyuiopasdfghjklzxcvbnm';
this.update = this.update.bind(this);
}
setText(newText) {
const oldText = this.el.innerText;
const length = Math.max(oldText.length, newText.length);
// eslint-disable-next-line no-return-assign
const promise = new Promise(resolve => (this.resolve = resolve));
this.queue = [];
for (let i = 0; i < length; i++) {
const from = oldText[i] || '';
const to = newText[i] || '';
const start = Math.floor(Math.random() * 40);
const end = start + Math.floor(Math.random() * 40);
this.queue.push({
from,
to,
start,
end,
});
}
cancelAnimationFrame(this.frameRequest);
this.frame = 0;
this.update();
return promise;
}
update() {
let output = '';
let complete = 0;
for (let i = 0, n = this.queue.length; i < n; i++) {
const { from, to, start, end } = this.queue[i];
let { char } = this.queue[i];
if (this.frame >= end) {
complete++;
output += to;
} else if (this.frame >= start) {
if (!char || Math.random() < 0.28) {
char = this.randomChar();
this.queue[i].char = char;
}
output += `<span class="dud">${char}</span>`;
} else {
output += from;
}
}
this.el.innerHTML = output;
if (complete === this.queue.length) {
this.resolve();
} else {
this.frameRequest = requestAnimationFrame(this.update);
this.frame++;
}
}
randomChar() {
return this.chars[Math.floor(Math.random() * this.chars.length)];
}
}
window.Session = window.Session || {};
window.Session.setNewSessionID = sessionID => {
const el = document.querySelector('.session-signin-enter-session-id');
const fx = new TextScramble(el);
el.innerHTML = sessionID;
fx.setText(sessionID);
};
window.Session.emptyContentEditableDivs = () => {
window.$('div[contenteditable]').html('');
};
})();

View file

@ -40,6 +40,7 @@
"test-node-coverage-html": "nyc --reporter=lcov --reporter=html mocha --recursive test/app test/modules ts/test libloki/test/node",
"eslint": "eslint .",
"lint": "yarn format --list-different && yarn lint-windows",
"dev-lint": "yarn format --list-different; yarn lint-windows",
"lint-windows": "yarn eslint && yarn tslint",
"lint-deps": "node ts/util/lint/linter.js",
"tslint": "tslint --format stylish --project .",
@ -54,6 +55,7 @@
"dependencies": {
"@journeyapps/sqlcipher": "https://github.com/scottnonnenberg-signal/node-sqlcipher.git#2e28733b61640556b0272a3bfc78b0357daf71e6",
"@sindresorhus/is": "0.8.0",
"@types/dompurify": "^2.0.0",
"backbone": "1.3.3",
"blob-util": "1.3.0",
"blueimp-canvas-to-blob": "3.14.0",
@ -63,6 +65,7 @@
"classnames": "2.2.5",
"color": "^3.1.2",
"config": "1.28.1",
"dompurify": "^2.0.7",
"electron-context-menu": "^0.15.0",
"electron-editor-context-menu": "1.1.1",
"electron-is-dev": "0.3.0",

View file

@ -472,3 +472,9 @@ window.lokiFeatureFlags = {
multiDeviceUnpairing: true,
privateGroupChats: false,
};
// eslint-disable-next-line no-extend-native,func-names
Promise.prototype.ignore = function() {
// eslint-disable-next-line more/no-then
this.then(() => {});
};

View file

@ -21,13 +21,15 @@
font-weight: bold;
font-style: italic;
}
@font-face {
font-family: 'Wasa';
src: url('../fonts/Wasa-Bold.otf') format('truetype');
src: url('../fonts/Wasa-Bold.otf') format('opentype');
font-weight: bold;
}
// Session Colors
$session-font-family: 'Wasa';
$session-color-green: #00f782;
$session-color-green-alt-1: #00f480;
$session-color-green-alt-2: #00fd73;
@ -56,6 +58,7 @@ $session-opaque-dark-2: rgba(0, 0, 0, 0.37);
$session-opaque-dark-3: rgba(0, 0, 0, 0.5);
$session-color-white: #fff;
$session-color-dark-grey: #353535;
$session-color-black: #000;
$session-color-danger: #ff4538;
$session-color-primary: $session-shade-13;
@ -64,7 +67,9 @@ $session-color-secondary: $session-shade-16;
$session-color-info: $session-shade-11;
$session-color-success: #35d388;
$session-color-error: #edd422;
$session-color-warning: #e54e45;
$session-color-warning: $session-shade-17;
$session-color-light-grey: #a0a0a0;
$session-shadow-opacity: 0.15;
$session-overlay-opacity: 0.3;
@ -77,7 +82,6 @@ $session-margin-lg: 20px;
color: rgba($color, 0.6);
}
$session-font-family: 'Wasa';
$session-transition-duration: 0.25s;
$session-icon-size-sm: 15px;
@ -86,6 +90,18 @@ $session-icon-size-lg: 30px;
$session-conversation-header-height: 60px;
@mixin fontWasaBold {
font-weight: 700;
font-family: $session-font-family;
}
a,
div,
span,
label {
user-select: none;
}
$session-gradient-green: linear-gradient(
270deg,
rgba($session-color-green-alt-1, 1),
@ -134,6 +150,7 @@ $session_message-container-border-radius: 5px;
font-weight: 700;
user-select: none;
cursor: pointer;
transition: $session-transition-duration;
&.default,
&.square,
@ -141,7 +158,11 @@ $session_message-container-border-radius: 5px;
color: $session-color-white;
&.green {
border: 2px solid $session-color-green;
background-color: $session-color-green;
&:hover {
@include transparent-background($session-color-green);
}
}
&.white {
background-color: $session-color-white;
@ -165,9 +186,16 @@ $session_message-container-border-radius: 5px;
&.square-outline {
&.green {
@include transparent-background($session-color-green);
&:hover {
@include transparent-background($session-color-white);
}
}
&.white {
@include transparent-background($session-color-white);
&:hover {
@include transparent-background($session-color-green);
}
}
&.primary {
@include transparent-background($session-color-primary);
@ -187,7 +215,7 @@ $session_message-container-border-radius: 5px;
min-width: 165px;
height: 45px;
line-height: 45px;
padding: 0 35px 0 35px;
padding: 0;
font-size: 15px;
font-family: $session-font-family;
border-radius: 500px;
@ -279,47 +307,107 @@ $session_message-container-border-radius: 5px;
padding-top: 5px;
}
.module-message__container {
.odule-message__container {
border-radius: $session_message-container-border-radius;
}
.module-message__attachment-container,
.module-image--curved-bottom-right,
.module-image--curved-bottom-left {
border-top-left-radius: 0px;
border-top-right-radius: 0px;
border-bottom-left-radius: $session_message-container-border-radius;
border-bottom-right-radius: $session_message-container-border-radius;
}
label {
user-select: none;
}
.conversation-header .session-icon-button {
@include standard-icon-button();
}
.module-message__attachment-container,
.module-image--curved-bottom-right,
.module-image--curved-bottom-left {
border-top-left-radius: 0px;
border-top-right-radius: 0px;
border-bottom-left-radius: $session_message-container-border-radius;
border-bottom-right-radius: $session_message-container-border-radius;
}
.module-conversation-header,
.message-selection-overlay {
height: $session-conversation-header-height;
}
.conversation-header .session-icon-button {
@include standard-icon-button();
}
.message-selection-overlay {
position: absolute;
left: 15px;
right: 15px;
display: none;
.module-conversation-header,
.message-selection-overlay {
height: $session-conversation-header-height;
}
.close-button {
float: left;
margin-top: 17px;
margin-left: 7px;
.message-selection-overlay {
position: absolute;
left: 15px;
right: 15px;
display: none;
.close-button {
float: left;
margin-top: 17px;
margin-left: 7px;
}
}
.message-selection-overlay div[role='button'] {
display: inline-block;
}
.message-selection-overlay .button-group {
float: right;
margin-top: 13.5px;
}
}
.message-selection-overlay div[role='button'] {
display: inline-block;
.hidden {
visibility: hidden;
}
.message-selection-overlay .button-group {
float: right;
margin-top: 13.5px;
.input-with-label-container {
height: 46.5px;
width: 280px;
color: $session-color-white;
padding: 2px 0 2px 0;
transition: opacity $session-transition-duration;
opacity: 1;
position: relative;
label {
line-height: 14px;
opacity: 0;
color: #737373;
font-size: 10px;
line-height: 11px;
position: absolute;
top: 0px;
}
&.filled {
opacity: 1;
}
input {
border: none;
outline: 0;
height: 14px;
width: 280px;
background: transparent;
color: $session-color-white;
font-size: 12px;
line-height: 14px;
position: absolute;
top: 50%;
transform: translateY(-50%);
}
hr {
border: 1px solid $session-color-white;
width: 100%;
position: absolute;
bottom: 0px;
}
button {
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 0px;
}
}
#session-toast-container {

View file

@ -0,0 +1,245 @@
.session {
&-fullscreen {
overflow-y: auto;
height: 100%;
background: linear-gradient(90deg, #121212 100%, #171717 0%);
}
&-content {
width: 100%;
height: 100%;
display: flex;
align-items: center;
&-accent {
flex-grow: 1;
padding-left: 20px;
&-text {
color: $session-color-white;
font-family: $session-font-family;
.title {
font-size: 100px;
font-weight: 700;
line-height: 120px;
}
}
}
&-registration {
height: 45%;
padding-right: 128px;
}
&-close-button {
position: absolute;
top: 17px;
left: 20px;
}
&-session-button {
position: absolute;
top: 17px;
right: 20px;
img {
width: 30px;
}
}
}
&-registration {
&-container {
display: flex;
flex-direction: column;
width: 289px;
}
&__content {
width: 100%;
overflow-y: auto;
padding-top: 20px;
}
&__sections {
display: flex;
flex-grow: 1;
flex-direction: column;
}
&__tab-container {
display: flex;
flex-grow: 0;
flex-shrink: 0;
cursor: pointer;
width: 289px;
height: 30px;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
color: $session-color-white;
}
&__tab {
@include fontWasaBold();
width: 100%;
padding-bottom: 10px;
background-color: transparent;
text-align: center;
color: $session-color-white;
border-bottom: 2px solid $session-color-dark-grey;
transition: border-color $session-transition-duration linear;
line-height: 17px;
font-size: 15px;
&--active {
border-bottom: 4px solid $session-color-green;
}
}
@mixin registration-label-mixin {
color: $session-color-white;
text-align: center;
font-size: 17px;
font-weight: 700;
line-height: 17px;
padding: 12px;
}
&__or {
@include registration-label-mixin;
}
&__welcome-session {
@include registration-label-mixin;
font-size: 12px;
font-weight: 700;
line-height: 12px;
padding-top: 2em;
}
&__unique-session-id {
@include registration-label-mixin;
padding-top: 3em;
}
&__entry-fields {
margin: 0px;
padding-bottom: 30px;
}
}
&-input-floating-label-show-hide {
padding-right: 30px;
}
&-input-with-label-container {
height: 46.5px;
width: 280px;
color: $session-color-white;
padding: 2px 0 2px 0;
transition: opacity $session-transition-duration;
opacity: 1;
position: relative;
label {
line-height: 14px;
opacity: 0;
color: #737373;
font-size: 10px;
line-height: 11px;
position: absolute;
top: 0px;
}
&.filled {
opacity: 1;
}
input {
border: none;
outline: 0;
height: 14px;
width: 280px;
background: transparent;
color: $session-color-white;
font-size: 12px;
line-height: 14px;
position: absolute;
top: 50%;
transform: translateY(-50%);
}
hr {
border: 1px solid $session-color-light-grey;
width: 100%;
position: absolute;
bottom: 0px;
}
.session-icon-button {
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 0px;
}
}
&-terms-conditions-agreement {
padding-top: 10px;
color: $session-color-light-grey;
text-align: center;
font-size: 12px;
a {
white-space: nowrap;
font-weight: bold;
color: $session-color-light-grey;
transition: $session-transition-duration;
&:visited &:link {
color: $session-color-light-grey;
}
&:hover {
color: $session-color-white;
}
}
}
&-signup-header,
&-signin-device-pairing-header {
padding-top: 10px;
padding-bottom: 10px;
color: $session-color-light-grey;
text-align: center;
font-size: 12px;
line-height: 20px;
}
&-signin-enter-session-id {
height: 94px;
width: 289px;
border-radius: 8px;
border: 2px solid $session-color-dark-grey;
outline: 0;
background: transparent;
color: $session-color-white;
font-size: 15px;
line-height: 18px;
text-align: center;
margin-bottom: 20px;
overflow-wrap: break-word;
padding: 20px 5px 20px 5px;
display: inline-block;
font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
}
}
[contenteditable='true']:empty::before {
content: attr(placeholder);
color: $session-color-light-grey;
}

View file

@ -25,6 +25,7 @@
// New CSS
@import 'modules';
@import 'session';
@import 'session_signin';
@import 'session_theme_dark';
// Installer

View file

@ -1,8 +1,6 @@
import React from 'react';
import { Avatar } from './Avatar';
import { SessionToast, SessionToastType } from './session/SessionToast';
declare global {
interface Window {
displayNameRegex: any;
@ -48,11 +46,6 @@ export class UserDetailsDialog extends React.Component<Props> {
{cancelText}
</button>
<SessionToast
title="This is a notification!"
type={SessionToastType.Error}
/>
<button
className="ok"
tabIndex={0}

View file

@ -4,6 +4,7 @@ import LinkifyIt from 'linkify-it';
import { RenderTextCallbackType } from '../../types/Util';
import { isLinkSneaky } from '../../../js/modules/link_previews';
import { SessionHtmlRenderer } from '../session/SessionHTMLRenderer';
const linkify = LinkifyIt();
@ -28,19 +29,7 @@ export class Linkify extends React.Component<Props> {
let count = 1;
if (isRss && text.indexOf('</') !== -1) {
results.push(
<div
key={count++}
dangerouslySetInnerHTML={{
__html: text
.replace(
/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
''
)
.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, ''),
}}
/>
);
results.push(<SessionHtmlRenderer key={count++} html={text} tag="div" />);
// should already have links
return results;

View file

@ -0,0 +1,11 @@
import React from 'react';
import { SessionHtmlRenderer } from './SessionHTMLRenderer';
export const AccentText: React.FC = () => (
<div className="session-content-accent-text">
<div className="session-content-accent-text title">
<SessionHtmlRenderer html={window.i18n('beginYourSession')} />
</div>
</div>
);

View file

@ -0,0 +1,763 @@
import React from 'react';
import classNames from 'classnames';
import { SessionInput } from './SessionInput';
import {
SessionButton,
SessionButtonColor,
SessionButtonType,
} from './SessionButton';
import { trigger } from '../../shims/events';
import { SessionHtmlRenderer } from './SessionHTMLRenderer';
enum SignInMode {
Default,
UsingSeed,
LinkingDevice,
}
enum SignUpMode {
Default,
SessionIDShown,
EnterDetails,
}
enum TabType {
Create,
SignIn,
}
interface State {
selectedTab: TabType;
signInMode: SignInMode;
signUpMode: SignUpMode;
displayName: string;
password: string;
validatePassword: string;
passwordErrorString: string;
passwordFieldsMatch: boolean;
mnemonicSeed: string;
hexGeneratedPubKey: string;
primaryDevicePubKey: string;
}
const Tab = ({
isSelected,
label,
onSelect,
type,
}: {
isSelected: boolean;
label: string;
onSelect?: (event: TabType) => void;
type: TabType;
}) => {
const handleClick = onSelect
? () => {
onSelect(type);
}
: undefined;
return (
<div
className={classNames(
'session-registration__tab',
isSelected ? 'session-registration__tab--active' : null
)}
onClick={handleClick}
role="tab"
>
{label}
</div>
);
};
export class RegistrationTabs extends React.Component<{}, State> {
private readonly accountManager: any;
constructor(props: any) {
super(props);
this.onSeedChanged = this.onSeedChanged.bind(this);
this.onDisplayNameChanged = this.onDisplayNameChanged.bind(this);
this.onPasswordChanged = this.onPasswordChanged.bind(this);
this.onPasswordVerifyChanged = this.onPasswordVerifyChanged.bind(this);
this.onSignUpGenerateSessionIDClick = this.onSignUpGenerateSessionIDClick.bind(
this
);
this.onSignUpGetStartedClick = this.onSignUpGetStartedClick.bind(this);
this.onSecondDeviceSessionIDChanged = this.onSecondDeviceSessionIDChanged.bind(
this
);
this.onSecondaryDeviceRegistered = this.onSecondaryDeviceRegistered.bind(
this
);
this.onCompleteSignUpClick = this.onCompleteSignUpClick.bind(this);
this.handlePressEnter = this.handlePressEnter.bind(this);
this.handleContinueYourSessionClick = this.handleContinueYourSessionClick.bind(
this
);
this.state = {
selectedTab: TabType.Create,
signInMode: SignInMode.Default,
signUpMode: SignUpMode.Default,
displayName: '',
password: '',
validatePassword: '',
passwordErrorString: '',
passwordFieldsMatch: false,
mnemonicSeed: '',
hexGeneratedPubKey: '',
primaryDevicePubKey: '',
};
this.accountManager = window.getAccountManager();
// Clean status in case the app closed unexpectedly
window.textsecure.storage.remove('secondaryDeviceStatus');
}
public render() {
this.generateMnemonicAndKeyPair().ignore();
return this.renderTabs();
}
private async generateMnemonicAndKeyPair() {
if (this.state.mnemonicSeed === '') {
const language = 'english';
const mnemonic = await this.accountManager.generateMnemonic(language);
let seedHex = window.mnemonic.mn_decode(mnemonic, language);
// handle shorter than 32 bytes seeds
const privKeyHexLength = 32 * 2;
if (seedHex.length !== privKeyHexLength) {
seedHex = seedHex.concat(seedHex);
seedHex = seedHex.substring(0, privKeyHexLength);
}
const privKeyHex = window.mnemonic.sc_reduce32(seedHex);
const privKey = window.dcodeIO.ByteBuffer.wrap(
privKeyHex,
'hex'
).toArrayBuffer();
const keyPair = await window.libsignal.Curve.async.createKeyPair(privKey);
const hexGeneratedPubKey = Buffer.from(keyPair.pubKey).toString('hex');
this.setState({
mnemonicSeed: mnemonic,
hexGeneratedPubKey, // our 'frontend' sessionID
});
}
}
private renderTabs() {
const { selectedTab } = this.state;
const createAccount = window.i18n('createAccount');
const signIn = window.i18n('signIn');
const isCreateSelected = selectedTab === TabType.Create;
const isSignInSelected = selectedTab === TabType.SignIn;
return (
<div className="session-registration-container">
<div className="session-registration__tab-container">
<Tab
label={createAccount}
type={TabType.Create}
isSelected={isCreateSelected}
onSelect={this.handleTabSelect}
/>
<Tab
label={signIn}
type={TabType.SignIn}
isSelected={isSignInSelected}
onSelect={this.handleTabSelect}
/>
</div>
{this.renderSections()}
</div>
);
}
private readonly handleTabSelect = (tabType: TabType): void => {
if (tabType !== TabType.SignIn) {
this.cancelSecondaryDevice().ignore();
}
this.setState({
selectedTab: tabType,
signInMode: SignInMode.Default,
signUpMode: SignUpMode.Default,
displayName: '',
password: '',
validatePassword: '',
passwordErrorString: '',
passwordFieldsMatch: false,
mnemonicSeed: '',
hexGeneratedPubKey: '',
primaryDevicePubKey: '',
});
};
private onSeedChanged(val: string) {
this.setState({ mnemonicSeed: val });
}
private onDisplayNameChanged(val: string) {
const sanitizedName = this.sanitiseNameInput(val);
this.setState({ displayName: sanitizedName });
}
private onPasswordChanged(val: string) {
this.setState({ password: val });
this.onValidatePassword(); // FIXME add bubbles or something to help the user know what he did wrong
}
private onPasswordVerifyChanged(val: string) {
this.setState({ validatePassword: val });
}
private renderSections() {
const { selectedTab } = this.state;
if (selectedTab === TabType.Create) {
return this.renderSignUp();
}
return this.renderSignIn();
}
private renderSignUp() {
const { signUpMode } = this.state;
switch (signUpMode) {
case SignUpMode.Default:
return (
<div className="session-registration__content">
{this.renderSignUpHeader()}
{this.renderSignUpButton()}
</div>
);
case SignUpMode.SessionIDShown:
return (
<div className="session-registration__content">
{this.renderSignUpHeader()}
<div className="session-registration__unique-session-id">
{window.i18n('yourUniqueSessionID')}
</div>
{this.renderEnterSessionID(false)}
{this.renderSignUpButton()}
{this.getRenderTermsConditionAgreement()}
</div>
);
default:
return (
<div className="session-registration__content">
<div className="session-registration__welcome-session">
{window.i18n('welcomeToYourSession')}
</div>
{this.renderRegistrationContent()}
<SessionButton
onClick={() => {
this.onCompleteSignUpClick();
}}
buttonType={SessionButtonType.Brand}
buttonColor={SessionButtonColor.Green}
text={window.i18n('completeSignUp')}
/>
</div>
);
}
}
private getRenderTermsConditionAgreement() {
const { selectedTab, signInMode, signUpMode } = this.state;
if (selectedTab === TabType.Create) {
return signUpMode !== SignUpMode.Default
? this.renderTermsConditionAgreement()
: null;
} else {
return signInMode !== SignInMode.Default
? this.renderTermsConditionAgreement()
: null;
}
}
private renderSignUpHeader() {
const allUsersAreRandomly = window.i18n('allUsersAreRandomly...');
return <div className="session-signup-header">{allUsersAreRandomly}</div>;
}
private renderSignUpButton() {
const { signUpMode } = this.state;
let buttonType: SessionButtonType;
let buttonColor: SessionButtonColor;
let buttonText: string;
if (signUpMode !== SignUpMode.Default) {
buttonType = SessionButtonType.Brand;
buttonColor = SessionButtonColor.Green;
buttonText = window.i18n('getStarted');
} else {
buttonType = SessionButtonType.BrandOutline;
buttonColor = SessionButtonColor.Green;
buttonText = window.i18n('generateSessionID');
}
return (
<SessionButton
onClick={() => {
if (signUpMode === SignUpMode.Default) {
this.onSignUpGenerateSessionIDClick().ignore();
} else {
this.onSignUpGetStartedClick();
}
}}
buttonType={buttonType}
buttonColor={buttonColor}
text={buttonText}
/>
);
}
private async onSignUpGenerateSessionIDClick() {
this.setState(
{
signUpMode: SignUpMode.SessionIDShown,
},
() => {
window.Session.setNewSessionID(this.state.hexGeneratedPubKey);
}
);
}
private onSignUpGetStartedClick() {
this.setState({
signUpMode: SignUpMode.EnterDetails,
});
}
private onCompleteSignUpClick() {
this.register('english').ignore();
}
private renderSignIn() {
return (
<div className="session-registration__content">
{this.renderRegistrationContent()}
{this.renderSignInButtons()}
{this.getRenderTermsConditionAgreement()}
</div>
);
}
private renderRegistrationContent() {
const { signInMode, signUpMode } = this.state;
if (signInMode === SignInMode.UsingSeed) {
return (
<div className={classNames('session-registration__entry-fields')}>
<SessionInput
label={window.i18n('mnemonicSeed')}
type="password"
placeholder={window.i18n('enterSeed')}
enableShowHide={true}
onValueChanged={(val: string) => {
this.onSeedChanged(val);
}}
onEnterPressed={() => {
this.handlePressEnter();
}}
/>
{this.renderNamePasswordAndVerifyPasswordFields()}
</div>
);
}
if (signInMode === SignInMode.LinkingDevice) {
return (
<div className="">
<div className="session-signin-device-pairing-header">
{window.i18n('devicePairingHeader')}
</div>
{this.renderEnterSessionID(true)}
</div>
);
}
if (signUpMode === SignUpMode.EnterDetails) {
return (
<div className={classNames('session-registration__entry-fields')}>
{this.renderNamePasswordAndVerifyPasswordFields()};
</div>
);
}
return null;
}
private renderNamePasswordAndVerifyPasswordFields() {
return (
<div className="inputfields">
<SessionInput
label={window.i18n('displayName')}
type="text"
placeholder={window.i18n('enterDisplayName')}
value={this.state.displayName}
onValueChanged={(val: string) => {
this.onDisplayNameChanged(val);
}}
onEnterPressed={() => {
this.handlePressEnter();
}}
/>
<SessionInput
label={window.i18n('optionalPassword')}
type="password"
placeholder={window.i18n('enterOptionalPassword')}
onValueChanged={(val: string) => {
this.onPasswordChanged(val);
}}
onEnterPressed={() => {
this.handlePressEnter();
}}
/>
<SessionInput
label={window.i18n('verifyPassword')}
type="password"
placeholder={window.i18n('optionalPassword')}
onValueChanged={(val: string) => {
this.onPasswordVerifyChanged(val);
}}
onEnterPressed={() => {
this.handlePressEnter();
}}
/>
</div>
);
}
private renderEnterSessionID(contentEditable: boolean) {
const enterSessionIDHere = window.i18n('enterSessionIDHere');
return (
<div
className="session-signin-enter-session-id"
placeholder={enterSessionIDHere}
contentEditable={contentEditable}
onInput={(e: any) => {
if (contentEditable) {
this.onSecondDeviceSessionIDChanged(e);
}
}}
/>
);
}
private onSecondDeviceSessionIDChanged(e: any) {
e.preventDefault();
const hexEncodedPubKey = e.target.innerHTML;
this.setState({
primaryDevicePubKey: hexEncodedPubKey,
});
}
private renderSignInButtons() {
const { signInMode } = this.state;
const or = window.i18n('or');
if (signInMode === SignInMode.Default) {
return (
<div>
{this.renderRestoreUsingSeedButton(
SessionButtonType.BrandOutline,
SessionButtonColor.Green
)}
<div className="session-registration__or">{or}</div>
{this.renderLinkDeviceToExistingAccountButton()}
</div>
);
}
if (signInMode === SignInMode.LinkingDevice) {
return (
<div>
{this.renderContinueYourSessionButton()}
<div className="session-registration__or">{or}</div>
{this.renderRestoreUsingSeedButton(
SessionButtonType.BrandOutline,
SessionButtonColor.Green
)}
</div>
);
}
return (
<div>
{this.renderContinueYourSessionButton()}
<div className="session-registration__or">{or}</div>
{this.renderLinkDeviceToExistingAccountButton()}
</div>
);
}
private renderTermsConditionAgreement() {
// FIXME link to our Terms and Conditions and privacy statement
return (
<div className="session-terms-conditions-agreement">
<SessionHtmlRenderer html={window.i18n('ByUsingThiService...')} />
</div>
);
}
private handleContinueYourSessionClick() {
if (this.state.signInMode === SignInMode.UsingSeed) {
this.register('english').ignore();
} else {
this.registerSecondaryDevice().ignore();
}
}
private renderContinueYourSessionButton() {
return (
<SessionButton
onClick={() => {
this.handleContinueYourSessionClick();
}}
buttonType={SessionButtonType.Brand}
buttonColor={SessionButtonColor.Green}
text={window.i18n('continueYourSession')}
/>
);
}
private renderRestoreUsingSeedButton(
buttonType: SessionButtonType,
buttonColor: SessionButtonColor
) {
return (
<SessionButton
onClick={() => {
this.cancelSecondaryDevice().ignore();
this.setState({
signInMode: SignInMode.UsingSeed,
primaryDevicePubKey: '',
mnemonicSeed: '',
displayName: '',
signUpMode: SignUpMode.Default,
});
//FIXME ugly hack to empty the content editable div used on enter session ID
window.Session.emptyContentEditableDivs();
}}
buttonType={buttonType}
buttonColor={buttonColor}
text={window.i18n('restoreUsingSeed')}
/>
);
}
private renderLinkDeviceToExistingAccountButton() {
return (
<SessionButton
onClick={() => {
this.setState({
signInMode: SignInMode.LinkingDevice,
mnemonicSeed: '',
displayName: '',
signUpMode: SignUpMode.Default,
});
}}
buttonType={SessionButtonType.BrandOutline}
buttonColor={SessionButtonColor.White}
text={window.i18n('linkDeviceToExistingAccount')}
/>
);
}
private handlePressEnter() {
const { signInMode, signUpMode } = this.state;
if (signUpMode === SignUpMode.EnterDetails) {
this.onCompleteSignUpClick();
return;
}
if (signInMode === SignInMode.UsingSeed) {
this.handleContinueYourSessionClick();
return;
}
}
private trim(value: string) {
return value ? value.trim() : value;
}
private validatePassword() {
const input = this.trim(this.state.password);
const confirmationInput = this.trim(this.state.validatePassword);
// If user hasn't set a value then skip
if (!input && !confirmationInput) {
return null;
}
const error = window.passwordUtil.validatePassword(input, window.i18n);
if (error) {
return error;
}
if (input !== confirmationInput) {
return "Password don't match";
}
return null;
}
private onValidatePassword() {
const passwordValidation = this.validatePassword();
if (passwordValidation) {
this.setState({ passwordErrorString: passwordValidation });
} else {
// Show green box around inputs that match
const input = this.trim(this.state.password);
const confirmationInput = this.trim(this.state.validatePassword);
const passwordFieldsMatch =
input !== undefined && input === confirmationInput;
this.setState({
passwordErrorString: '',
passwordFieldsMatch,
});
}
}
private sanitiseNameInput(val: string) {
return val.trim().replace(window.displayNameRegex, '');
}
private async resetRegistration() {
await window.Signal.Data.removeAllIdentityKeys();
await window.Signal.Data.removeAllPrivateConversations();
window.Whisper.Registration.remove();
// Do not remove all items since they are only set
// at startup.
window.textsecure.storage.remove('identityKey');
window.textsecure.storage.remove('secondaryDeviceStatus');
window.ConversationController.reset();
await window.ConversationController.load();
window.Whisper.RotateSignedPreKeyListener.stop(window.Whisper.events);
}
private async register(language: string) {
const { password, mnemonicSeed, displayName } = this.state;
// Make sure the password is valid
if (this.validatePassword()) {
//this.showToast(window.i18n('invalidPassword'));
return;
}
if (!mnemonicSeed) {
return;
}
if (!displayName) {
return;
}
// Ensure we clear the secondary device registration status
window.textsecure.storage.remove('secondaryDeviceStatus');
try {
await this.resetRegistration();
await window.setPassword(password);
await this.accountManager.registerSingleDevice(
mnemonicSeed,
language,
displayName
);
trigger('openInbox');
} catch (e) {
if (typeof e === 'string') {
//this.showToast(e);
}
//this.log(e);
}
}
private async cancelSecondaryDevice() {
window.Whisper.events.off(
'secondaryDeviceRegistration',
this.onSecondaryDeviceRegistered
);
await this.resetRegistration();
}
private async registerSecondaryDevice() {
// tslint:disable-next-line: no-backbone-get-set-outside-model
if (window.textsecure.storage.get('secondaryDeviceStatus') === 'ongoing') {
return;
}
await this.resetRegistration();
window.textsecure.storage.put('secondaryDeviceStatus', 'ongoing');
const primaryPubKey = this.state.primaryDevicePubKey;
// Ensure only one listener
window.Whisper.events.off(
'secondaryDeviceRegistration',
this.onSecondaryDeviceRegistered
);
window.Whisper.events.once(
'secondaryDeviceRegistration',
this.onSecondaryDeviceRegistered
);
const onError = async (error: any) => {
window.console.error(error);
await this.resetRegistration();
};
const c = new window.Whisper.Conversation({
id: primaryPubKey,
type: 'private',
});
const validationError = c.validateNumber();
if (validationError) {
onError('Invalid public key').ignore();
return;
}
try {
const fakeMnemonic = this.state.mnemonicSeed;
await this.accountManager.registerSingleDevice(
fakeMnemonic,
'english',
null
);
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}`);
} catch (e) {
window.console.log(e);
//onError(e);
}
}
private async onSecondaryDeviceRegistered() {
// Ensure the left menu is updated
trigger('userChanged', { isSecondaryDevice: true });
// will re-run the background initialisation
trigger('registration_done');
trigger('openInbox');
}
}

View file

@ -42,13 +42,18 @@ export class SessionButton extends React.PureComponent<Props> {
public render() {
const { buttonType, buttonColor, text } = this.props;
const buttonTypes = [];
buttonTypes.push(buttonType);
if (buttonType.includes('-outline')) {
buttonTypes.push(buttonType.replace('-outline', ''));
}
return (
<div
onClick={e => {
this.clickHandler(e);
}}
className={classNames('session-button', buttonType, buttonColor)}
className={classNames('session-button', ...buttonTypes, buttonColor)}
role="button"
onClick={this.clickHandler}
>
{text}
</div>

View file

@ -0,0 +1,26 @@
import React from 'react';
import DOMPurify from 'dompurify';
interface ReceivedProps {
html: string;
tag?: string;
key?: any;
}
type Props = ReceivedProps;
export const SessionHtmlRenderer: React.SFC<Props> = ({
tag = 'div',
key,
html,
}) => {
const clean = DOMPurify.sanitize(html, {
USE_PROFILES: { html: true },
FORBID_ATTR: ['style', 'script'],
});
return React.createElement(tag, {
key,
dangerouslySetInnerHTML: { __html: clean },
});
};

View file

@ -0,0 +1,103 @@
import React from 'react';
import classNames from 'classnames';
import { SessionIconButton, SessionIconSize, SessionIconType } from './icon';
interface Props {
label: string;
type: string;
value?: string;
placeholder: string;
enableShowHide?: boolean;
onValueChanged?: any;
onEnterPressed?: any;
}
interface State {
inputValue: string;
forceShow: boolean;
}
export class SessionInput extends React.PureComponent<Props, State> {
constructor(props: any) {
super(props);
this.updateInputValue = this.updateInputValue.bind(this);
this.renderShowHideButton = this.renderShowHideButton.bind(this);
this.state = {
inputValue: '',
forceShow: false,
};
}
public render() {
const { placeholder, type, label, value, enableShowHide } = this.props;
const { inputValue, forceShow } = this.state;
const correctType = forceShow ? 'text' : type;
return (
<div className="session-input-with-label-container">
<label
htmlFor="session-input-floating-label"
className={classNames(
inputValue !== ''
? 'session-input-with-label-container filled'
: 'session-input-with-label-container'
)}
>
{label}
</label>
<input
id="session-input-floating-label"
type={correctType}
placeholder={placeholder}
value={value}
onChange={e => {
this.updateInputValue(e);
}}
className={classNames(
enableShowHide ? 'session-input-floating-label-show-hide' : ''
)}
onKeyPress={event => {
event.persist();
if (event.key === 'Enter' && this.props.onEnterPressed) {
this.props.onEnterPressed();
}
}}
/>
{enableShowHide && this.renderShowHideButton()}
<hr />
</div>
);
}
private renderShowHideButton() {
return (
<SessionIconButton
iconType={SessionIconType.Eye}
iconSize={SessionIconSize.Medium}
iconPadded={false}
onClick={() => {
this.setState({
forceShow: !this.state.forceShow,
});
}}
/>
);
}
private updateInputValue(e: any) {
e.preventDefault();
this.setState({
inputValue: e.target.value,
});
if (this.props.onValueChanged) {
this.props.onValueChanged(e.target.value);
}
}
}

View file

@ -0,0 +1,30 @@
import React from 'react';
import { AccentText } from './AccentText';
import { RegistrationTabs } from './RegistrationTabs';
import { SessionIconButton, SessionIconSize, SessionIconType } from './icon';
export const SessionRegistrationView: React.FC = () => (
<div className="session-content">
<div id="error" className="collapse" />
<div className="session-content-close-button">
<SessionIconButton
iconSize={SessionIconSize.Medium}
iconType={SessionIconType.Exit}
onClick={() => {
window.close();
}}
/>
</div>
<div className="session-content-accent">
<AccentText />
</div>
<div className="session-content-registration">
<RegistrationTabs />
</div>
<div className="session-content-session-button">
<img alt="brand" src="./images/session/brand.svg" />
</div>
</div>
);

19
ts/global.d.ts vendored Normal file
View file

@ -0,0 +1,19 @@
interface Window {
getAccountManager: any;
mnemonic: any;
passwordUtil: any;
dcodeIO: any;
libsignal: any;
displayNameRegex: any;
Signal: any;
Whisper: any;
ConversationController: any;
setPassword: any;
textsecure: any;
Session: any;
i18n: any;
}
interface Promise<T> {
ignore(): void;
}

View file

@ -141,9 +141,9 @@
"react-no-dangerous-html": [
true,
{
"file": "ts/components/conversation/Linkify.tsx",
"method": "render",
"comment": "Usage has been approved by Ryan Tharp on 2019-07-22"
"file": "ts/components/session/SessionHTMLRenderer.tsx",
"method": "<unknown>",
"comment": "Usage has been approved by Maxim on 13 Dec 2019"
}
],
// Reasonable functions can exceed the default of 100 lines

View file

@ -138,6 +138,13 @@
resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.5.tgz#b14efa8852b7768d898906613c23f688713e02cd"
integrity sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==
"@types/dompurify@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-2.0.0.tgz#9616caa5bf2569aea2e4889d4f929d968c081b40"
integrity sha512-g/ilp+Bo6Ljy60i5LnjkGw00X7EIoFjoPGlxqZhV8TJ9fWEzXheioU1O+U/UzCzUA7pUDy/JNMytTQDJctpUHg==
dependencies:
"@types/trusted-types" "*"
"@types/events@*":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
@ -324,6 +331,11 @@
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-2.3.5.tgz#9da44ed75571999b65c37b60c9b2b88db54c585d"
integrity sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg==
"@types/trusted-types@*":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-1.0.4.tgz#922d092c84a776a59acb0bd6785fd82b59b9bad5"
integrity sha512-6jtHrHpmiXOXoJ31Cg9R+iEVwuEKPf0XHwFUI93eEPXx492/J2JHyafkleKE2EYzZprayk9FSjTyK1GDqcwDng==
"@types/uuid@3.4.4":
version "3.4.4"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.4.tgz#7af69360fa65ef0decb41fd150bf4ca5c0cefdf5"
@ -2581,6 +2593,11 @@ domain-browser@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
dompurify@^2.0.7:
version "2.0.7"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.0.7.tgz#f8266ad38fe1602fb5b3222f31eedbf5c16c4fd5"
integrity sha512-S3O0lk6rFJtO01ZTzMollCOGg+WAtCwS3U5E2WSDY/x/sy7q70RjEC4Dmrih5/UqzLLB9XoKJ8KqwBxaNvBu4A==
dot-prop@^4.1.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.1.1.tgz#a8493f0b7b5eeec82525b5c7587fa7de7ca859c1"