diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index e7420151b..e168d78c8 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -1698,11 +1698,56 @@
"description": "Button action that the user can click to set a password"
},
"changePassword": {
- "message": "Set Password",
+ "message": "Change Password",
"description": "Button action that the user can click to change a password"
},
"removePassword": {
- "message": "Set Password",
+ "message": "Remove Password",
"description": "Button action that the user can click to remove a password"
+ },
+ "typeInOldPassword": {
+ "message": "Please type in your old password"
+ },
+ "invalidPassword": {
+ "message": "Invalid password"
+ },
+ "passwordsDoNotMatch": {
+ "message": "Passwords do not match"
+ },
+ "setPasswordFail": {
+ "message": "Failed to set password"
+ },
+ "removePasswordFail": {
+ "message": "Failed to remove password"
+ },
+ "changePasswordFail": {
+ "message": "Failed to change password"
+ },
+ "setPasswordSuccess": {
+ "message": "Password set"
+ },
+ "removePasswordSuccess": {
+ "message": "Password removed"
+ },
+ "changePasswordSuccess": {
+ "message": "Password changed"
+ },
+ "passwordLengthError": {
+ "message": "Password must be atleast 6 characters long",
+ "description": "Error string shown to the user when password doesn't meet length criteria"
+ },
+ "passwordTypeError": {
+ "message": "Password must be a string",
+ "description": "Error string shown to the user when password is not a string"
+ },
+ "change": {
+ "message": "Change"
+ },
+ "set": {
+ "message": "Set"
+ },
+ "remove": {
+ "message": "Remove"
}
+
}
diff --git a/app/password_util.js b/app/password_util.js
index b6c5e2b39..736a1c530 100644
--- a/app/password_util.js
+++ b/app/password_util.js
@@ -3,13 +3,13 @@ const { sha512 } = require('js-sha512');
const generateHash = (phrase) => phrase && sha512(phrase.trim());
const matchesHash = (phrase, hash) => phrase && sha512(phrase.trim()) === hash.trim();
-const validatePassword = (phrase) => {
+const validatePassword = (phrase, i18n) => {
if (typeof phrase !== 'string') {
- return 'Password must be a string'
+ return i18n ? i18n('passwordTypeError') : 'Password must be a string'
}
if (phrase && phrase.trim().length < 6) {
- return 'Password must be atleast 6 characters long';
+ return i18n ? i18n('passwordLengthError') : 'Password must be atleast 6 characters long';
}
// An empty password is still valid :P
diff --git a/background.html b/background.html
index 83b1806b8..ff0d7a76b 100644
--- a/background.html
+++ b/background.html
@@ -165,6 +165,35 @@
0:00
+
+
+
diff --git a/js/background.js b/js/background.js
index faef150b6..831ad3044 100644
--- a/js/background.js
+++ b/js/background.js
@@ -602,6 +602,12 @@
}
});
+ Whisper.events.on('showPasswordDialog', options => {
+ if (appView) {
+ appView.showPasswordDialog(options);
+ }
+ });
+
Whisper.events.on('calculatingPoW', ({ pubKey, timestamp }) => {
try {
const conversation = ConversationController.get(pubKey);
diff --git a/js/views/app_view.js b/js/views/app_view.js
index f21542c24..04c4b1514 100644
--- a/js/views/app_view.js
+++ b/js/views/app_view.js
@@ -189,5 +189,9 @@
});
this.el.append(dialog.el);
},
+ showPasswordDialog({ type, resolve, reject }) {
+ const dialog = Whisper.getPasswordDialogView(type, resolve, reject);
+ this.el.append(dialog.el);
+ },
});
})();
diff --git a/js/views/inbox_view.js b/js/views/inbox_view.js
index dc52dbbbe..ea5339192 100644
--- a/js/views/inbox_view.js
+++ b/js/views/inbox_view.js
@@ -334,29 +334,37 @@
const ourNumber = textsecure.storage.user.getNumber();
clipboard.writeText(ourNumber);
- const toast = new Whisper.MessageToastView({
- message: i18n('copiedPublicKey'),
- });
- toast.$el.appendTo(this.$('.gutter'));
- toast.render();
+ this.showToastMessageInGutter(i18n('copiedPublicKey'));
}),
this._mainHeaderItem('editDisplayName', () => {
window.Whisper.events.trigger('onEditProfile');
}),
- ...this.passwordHeaderItems || [],
];
},
async onPasswordUpdated() {
const hasPassword = await Signal.Data.getPasswordHash();
const items = this.getMainHeaderItems();
+
+ const showPasswordDialog = (type, resolve) => Whisper.events.trigger('showPasswordDialog', {
+ type,
+ resolve,
+ });
+
+ const passwordItem = (textKey, type) => this._mainHeaderItem(
+ textKey,
+ () => showPasswordDialog(type, () => {
+ this.showToastMessageInGutter(i18n(`${textKey}Success`));
+ })
+ );
+
if (hasPassword) {
items.push(
- this._mainHeaderItem('changePassword'),
- this._mainHeaderItem('removePassword')
+ passwordItem('changePassword', 'change'),
+ passwordItem('removePassword', 'remove')
);
} else {
items.push(
- this._mainHeaderItem('setPassword')
+ passwordItem('setPassword', 'set')
);
}
@@ -369,6 +377,13 @@
onClick,
};
},
+ showToastMessageInGutter(message) {
+ const toast = new Whisper.MessageToastView({
+ message,
+ });
+ toast.$el.appendTo(this.$('.gutter'));
+ toast.render();
+ },
});
Whisper.ExpiredAlertBanner = Whisper.View.extend({
diff --git a/js/views/nickname_dialog_view.js b/js/views/nickname_dialog_view.js
index 7af20cb46..aa0af8143 100644
--- a/js/views/nickname_dialog_view.js
+++ b/js/views/nickname_dialog_view.js
@@ -7,7 +7,7 @@
window.Whisper = window.Whisper || {};
Whisper.NicknameDialogView = Whisper.View.extend({
- className: 'nickname-dialog modal',
+ className: 'loki-dialog nickname-dialog modal',
templateName: 'nickname-dialog',
initialize(options) {
this.message = options.message;
diff --git a/js/views/password_dialog_view.js b/js/views/password_dialog_view.js
new file mode 100644
index 000000000..d919da1d2
--- /dev/null
+++ b/js/views/password_dialog_view.js
@@ -0,0 +1,214 @@
+/* global Whisper, i18n, _, Signal, passwordUtil */
+
+// eslint-disable-next-line func-names
+(function() {
+ 'use strict';
+
+ window.Whisper = window.Whisper || {};
+
+ const PasswordDialogView = Whisper.View.extend({
+ className: 'loki-dialog password-dialog modal',
+ templateName: 'password-dialog',
+ initialize(options) {
+ this.type = options.type;
+ this.resolve = options.resolve;
+ this.okText = options.okText || i18n('ok');
+
+ this.reject = options.reject;
+ this.cancelText = options.cancelText || i18n('cancel');
+
+ this.title = options.title;
+
+ this.render();
+ this.updateUI();
+ },
+ events: {
+ keyup: 'onKeyup',
+ 'click .ok': 'ok',
+ 'click .cancel': 'cancel',
+ },
+ render_attributes() {
+ return {
+ showCancel: !this.hideCancel,
+ cancel: this.cancelText,
+ ok: this.okText,
+ title: this.title,
+ };
+ },
+ async updateUI() {
+ if (this.disableOkButton()) {
+ this.$('.ok').prop('disabled', true);
+ } else {
+ this.$('.ok').prop('disabled', false);
+ }
+ },
+ disableOkButton() {
+ const password = this.$('#password').val();
+ return _.isEmpty(password);
+ },
+ async validate() {
+ const password = this.$('#password').val();
+ const passwordConfirmation = this.$('#password-confirmation').val();
+
+ const pairValidation = this.validatePasswordPair(password, passwordConfirmation);
+ const hashValidation = await this.validatePasswordHash(password);
+
+ return (pairValidation || hashValidation);
+ },
+ async validatePasswordHash(password) {
+ // Check if the password matches the hash we have stored
+ const hash = await Signal.Data.getPasswordHash();
+ if (hash && !passwordUtil.matchesHash(password, hash)) {
+ return i18n('invalidPassword');
+ }
+ return null;
+ },
+ validatePasswordPair(password, passwordConfirmation) {
+ if (!_.isEmpty(password)) {
+
+ // Check if the password is first valid
+ const passwordValidation = passwordUtil.validatePassword(password, i18n);
+ if (passwordValidation) {
+ return passwordValidation;
+ }
+
+ // Check if the confirmation password is the same
+ if (!passwordConfirmation || password.trim() !== passwordConfirmation.trim()) {
+ return i18n('passwordsDoNotMatch');
+ }
+ }
+ return null;
+ },
+ okPressed() {
+ const password = this.$('#password').val();
+ if (this.type === 'set') {
+ window.setPassword(password.trim());
+ } else if (this.type === 'remove') {
+ window.setPassword(null, password.trim());
+ }
+ },
+ okErrored() {
+ if (this.type === 'set') {
+ this.showError(i18n('setPasswordFail'));
+ } else if (this.type === 'remove') {
+ this.showError(i18n('removePasswordFail'));
+ }
+ },
+ async ok() {
+ const error = await this.validate();
+ if (error) {
+ this.showError(error);
+ return;
+ }
+
+ // Clear any errors
+ this.showError(null);
+
+ try {
+ this.okPressed();
+
+ this.remove();
+ if (this.resolve) {
+ this.resolve();
+ }
+ } catch (e) {
+ this.okErrored();
+ }
+ },
+ cancel() {
+ this.remove();
+ if (this.reject) {
+ this.reject();
+ }
+ },
+ onKeyup(event) {
+ this.updateUI();
+ switch (event.key) {
+ case 'Enter':
+ this.ok();
+ break;
+ case 'Escape':
+ case 'Esc':
+ this.cancel();
+ break;
+ default:
+ return;
+ }
+ event.preventDefault();
+ },
+ focusCancel() {
+ this.$('.cancel').focus();
+ },
+ showError(message) {
+ if (_.isEmpty(message)) {
+ this.$('.error').text('');
+ this.$('.error').hide();
+ } else {
+ this.$('.error').text(`Error: ${message}`);
+ this.$('.error').show();
+ }
+ },
+ });
+
+ const ChangePasswordDialogView = PasswordDialogView.extend({
+ templateName: 'password-change-dialog',
+ disableOkButton() {
+ const oldPassword = this.$('#old-password').val();
+ const newPassword = this.$('#new-password').val();
+ return _.isEmpty(oldPassword) || _.isEmpty(newPassword);
+ },
+ async validate() {
+ const oldPassword = this.$('#old-password').val();
+
+ // Validate the old password
+ if (!_.isEmpty(oldPassword) ) {
+ const oldPasswordValidation = passwordUtil.validatePassword(oldPassword, i18n);
+ if (oldPasswordValidation) {
+ return oldPasswordValidation;
+ }
+ } else {
+ return i18n('typeInOldPassword');
+ }
+
+ const password = this.$('#new-password').val();
+ const passwordConfirmation = this.$('#new-password-confirmation').val();
+
+ const pairValidation = this.validatePasswordPair(password, passwordConfirmation);
+ const hashValidation = await this.validatePasswordHash(oldPassword);
+
+ return pairValidation || hashValidation;
+ },
+ okPressed() {
+ const oldPassword = this.$('#old-password').val();
+ const newPassword = this.$('#new-password').val();
+ window.setPassword(newPassword.trim(), oldPassword.trim());
+ },
+ okErrored() {
+ this.showError(i18n('changePasswordFail'));
+ },
+ });
+
+ Whisper.getPasswordDialogView = (type, resolve, reject) => {
+
+ // This is a differently styled dialog
+ if (type === 'change') {
+ return new ChangePasswordDialogView({
+ title: i18n('changePassword'),
+ okTitle: i18n('change'),
+ resolve,
+ reject,
+ });
+ }
+
+ // Set and Remove is basically the same UI
+ const title = type === 'remove' ? i18n('removePassword') : i18n('setPassword');
+ const okTitle = type === 'remove' ? i18n('remove') : i18n('set');
+ return new PasswordDialogView({
+ title,
+ okTitle,
+ type,
+ resolve,
+ reject,
+ });
+ };
+})();
diff --git a/js/views/standalone_registration_view.js b/js/views/standalone_registration_view.js
index 6f32d874a..1c02a1407 100644
--- a/js/views/standalone_registration_view.js
+++ b/js/views/standalone_registration_view.js
@@ -193,12 +193,14 @@
const input = this.trim(this.$passwordInput.val());
const confirmationInput = this.trim(this.$passwordConfirmationInput.val());
- const error = passwordUtil.validatePassword(input);
- if (error)
+ const error = passwordUtil.validatePassword(input, i18n);
+ if (error) {
return error;
+ }
- if (input !== confirmationInput)
+ if (input !== confirmationInput) {
return 'Password don\'t match';
+ }
return null;
},
@@ -207,13 +209,30 @@
if (passwordValidation) {
this.$passwordInput.addClass('error-input');
this.$passwordConfirmationInput.addClass('error-input');
+
+ this.$passwordInput.removeClass('match-input');
+ this.$passwordConfirmationInput.removeClass('match-input');
+
this.$passwordInputError.text(passwordValidation);
this.$passwordInputError.show();
+
} else {
this.$passwordInput.removeClass('error-input');
this.$passwordConfirmationInput.removeClass('error-input');
+
this.$passwordInputError.text('');
this.$passwordInputError.hide();
+
+ // Show green box around inputs that match
+ const input = this.trim(this.$passwordInput.val());
+ const confirmationInput = this.trim(this.$passwordConfirmationInput.val());
+ if (input && input === confirmationInput) {
+ this.$passwordInput.addClass('match-input');
+ this.$passwordConfirmationInput.addClass('match-input');
+ } else {
+ this.$passwordInput.removeClass('match-input');
+ this.$passwordConfirmationInput.removeClass('match-input');
+ }
}
},
trim(value) {
diff --git a/password_preload.js b/password_preload.js
index f8dcb3eb3..1a857a58e 100644
--- a/password_preload.js
+++ b/password_preload.js
@@ -24,6 +24,8 @@ window.getEnvironment = () => config.environment;
window.getVersion = () => config.version;
window.getAppInstance = () => config.appInstance;
+window.passwordUtil = require('./app/password_util');
+
window.onLogin = (passPhrase) => new Promise((resolve, reject) => {
ipcRenderer.once('password-window-login-response', (event, error) => {
if (error) {
diff --git a/stylesheets/_conversation.scss b/stylesheets/_conversation.scss
index bfda09310..4c23e6bf5 100644
--- a/stylesheets/_conversation.scss
+++ b/stylesheets/_conversation.scss
@@ -353,7 +353,7 @@
}
}
-.nickname-dialog {
+.loki-dialog {
display: flex;
align-items: center;
justify-content: center;
@@ -366,55 +366,62 @@
border-radius: $border-radius;
overflow: auto;
box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, 0.3);
+ }
- .buttons {
-
- button {
- float: right;
- margin-left: 10px;
- background-color: $color-loki-green;
- border-radius: 100px;
- padding: 5px 15px;
- border: 1px solid $color-loki-green;
- color: white;
- outline: none;
+ button {
+ float: right;
+ margin-left: 10px;
+ background-color: $color-loki-green;
+ border-radius: 100px;
+ padding: 5px 15px;
+ border: 1px solid $color-loki-green;
+ color: white;
+ outline: none;
+
+ &:hover, &:disabled {
+ background-color: $color-loki-green-dark;
+ border-color: $color-loki-green-dark;
+ }
- &:hover {
- background-color: $color-loki-green-dark;
- border-color: $color-loki-green-dark;
- }
- }
+ &:disabled {
+ cursor: not-allowed;
}
+ }
+
+ input {
+ width: 100%;
+ padding: 8px;
+ margin-bottom: 15px;
+ border: 0;
+ outline: none;
+ border-radius: 4px;
+ background-color: $color-loki-light-gray;
- input {
- width: 100%;
- padding: 8px;
- margin-bottom: 15px;
- border: 0;
+ &:focus {
outline: none;
- border-radius: 4px;
- background-color: $color-loki-light-gray;
}
+ }
- h4 {
- margin-top: 8px;
- margin-bottom: 16px;
- white-space: -moz-pre-wrap; /* Mozilla */
- white-space: -hp-pre-wrap; /* HP printers */
- white-space: -o-pre-wrap; /* Opera 7 */
- white-space: -pre-wrap; /* Opera 4-6 */
- white-space: pre-wrap; /* CSS 2.1 */
- white-space: pre-line; /* CSS 3 (and 2.1 as well, actually) */
- word-wrap: break-word; /* IE */
- word-break: break-all;
- }
+ h4 {
+ margin-top: 8px;
+ margin-bottom: 16px;
+ white-space: -moz-pre-wrap; /* Mozilla */
+ white-space: -hp-pre-wrap; /* HP printers */
+ white-space: -o-pre-wrap; /* Opera 7 */
+ white-space: -pre-wrap; /* Opera 4-6 */
+ white-space: pre-wrap; /* CSS 2.1 */
+ white-space: pre-line; /* CSS 3 (and 2.1 as well, actually) */
+ word-wrap: break-word; /* IE */
+ word-break: break-all;
+ }
+}
- .message {
- font-style: italic;
- color: $grey;
- font-size: 12px;
- margin-bottom: 16px;
- }
+.nickname-dialog {
+ .message {
+ font-style: italic;
+ color: $grey;
+ font-size: 12px;
+ margin-bottom: 16px;
}
}
diff --git a/stylesheets/_global.scss b/stylesheets/_global.scss
index 3321781a2..d7f670714 100644
--- a/stylesheets/_global.scss
+++ b/stylesheets/_global.scss
@@ -993,6 +993,14 @@ textarea {
outline: none;
}
}
+
+ .match-input {
+ border: 3px solid $color-loki-green;
+
+ &:focus {
+ outline: none;
+ }
+ }
}
@media (min-height: 750px) and (min-width: 700px) {
diff --git a/stylesheets/_theme_dark.scss b/stylesheets/_theme_dark.scss
index 7d9137355..3c1194d9b 100644
--- a/stylesheets/_theme_dark.scss
+++ b/stylesheets/_theme_dark.scss
@@ -93,34 +93,34 @@ body.dark-theme {
}
}
- .nickname-dialog {
+ .loki-dialog {
.content {
background: $color-black;
color: $color-dark-05;
+ }
- .buttons {
- button {
- background-color: $color-dark-85;
- border-radius: $border-radius;
- border: 1px solid $color-dark-60;
- color: $color-dark-05;
-
- &:hover {
- background-color: $color-dark-70;
- border-color: $color-dark-55;
- }
- }
- }
+ button {
+ background-color: $color-dark-85;
+ border-radius: $border-radius;
+ border: 1px solid $color-dark-60;
+ color: $color-dark-05;
- input {
- color: $color-dark-05;
+ &:hover {
background-color: $color-dark-70;
border-color: $color-dark-55;
}
+ }
- .message {
- color: $color-light-35;
- }
+ input {
+ color: $color-dark-05;
+ background-color: $color-dark-70;
+ border-color: $color-dark-55;
+ }
+ }
+
+ .nickname-dialog {
+ .message {
+ color: $color-light-35;
}
}
diff --git a/test/index.html b/test/index.html
index b03374a61..81a82bd38 100644
--- a/test/index.html
+++ b/test/index.html
@@ -403,6 +403,7 @@
+