diff --git a/_locales/en/messages.json b/_locales/en/messages.json index c94edff84..ca1001bb6 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1376,7 +1376,7 @@ "description": "Description of the media permission description" }, "clearDataHeader": { - "message": "Clear Data", + "message": "Clear All Local Data", "description": "Header in the settings dialog for the section dealing with data deletion" }, @@ -2281,7 +2281,7 @@ }, "passwordViewTitle": { - "message": "Type in your password", + "message": "Type In Your Password", "description": "The title shown when user needs to type in a password to unlock the messenger" }, @@ -2309,6 +2309,9 @@ "message": "Remove Password", "description": "Button action that the user can click to remove a password" }, + "maxPasswordAttempts": { + "message": "Invalid Password. Would you like to reset the database?" + }, "typeInOldPassword": { "message": "Please type in your old password" }, @@ -2316,7 +2319,7 @@ "message": "Old password is invalid" }, "invalidPassword": { - "message": "Invalid password" + "message": "Invalid Password" }, "noGivenPassword": { "message": "Please enter your password" diff --git a/index.html b/index.html index 18d7ba8e7..2d781b911 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - Signal + Session diff --git a/js/modules/signal.js b/js/modules/signal.js index e44df7149..7075c5782 100644 --- a/js/modules/signal.js +++ b/js/modules/signal.js @@ -74,6 +74,9 @@ const { const { SessionPasswordModal, } = require('../../ts/components/session/SessionPasswordModal'); +const { + SessionPasswordPrompt, +} = require('../../ts/components/session/SessionPasswordPrompt'); const { SessionConfirm, @@ -293,6 +296,7 @@ exports.setup = (options = {}) => { SessionQRModal, SessionSeedModal, SessionPasswordModal, + SessionPasswordPrompt, SessionDropdown, SessionScrollButton, MediaGallery, diff --git a/js/views/clear_data_view.js b/js/views/clear_data_view.js deleted file mode 100644 index 7af25d893..000000000 --- a/js/views/clear_data_view.js +++ /dev/null @@ -1,72 +0,0 @@ -/* global i18n: false */ -/* global Whisper: false */ - -/* eslint-disable no-new */ - -// eslint-disable-next-line func-names -(function() { - 'use strict'; - - window.Whisper = window.Whisper || {}; - const { Logs } = window.Signal; - - const CLEAR_DATA_STEPS = { - CHOICE: 1, - DELETING: 2, - }; - window.Whisper.ClearDataView = Whisper.View.extend({ - templateName: 'clear-data', - className: 'full-screen-flow overlay', - events: { - 'click .cancel': 'onCancel', - 'click .delete-all-data': 'onDeleteAllData', - }, - initialize(onClear = null) { - this.step = CLEAR_DATA_STEPS.CHOICE; - this.onClear = onClear; - }, - onCancel() { - this.remove(); - }, - async onDeleteAllData() { - window.log.info('Deleting everything!'); - this.step = CLEAR_DATA_STEPS.DELETING; - this.render(); - - await this.clearAllData(); - }, - async clearAllData() { - if (this.onClear) { - this.onClear(); - } else { - try { - await Logs.deleteAll(); - - await window.Signal.Data.removeAll(); - await window.Signal.Data.close(); - await window.Signal.Data.removeDB(); - - await window.Signal.Data.removeOtherData(); - } catch (error) { - window.log.error( - 'Something went wrong deleting all data:', - error && error.stack ? error.stack : error - ); - } - window.restart(); - } - }, - render_attributes() { - return { - isStep1: this.step === CLEAR_DATA_STEPS.CHOICE, - header: i18n('deleteAllDataHeader'), - body: i18n('deleteAllDataBody'), - cancelButton: i18n('cancel'), - deleteButton: i18n('deleteAllDataButton'), - - isStep2: this.step === CLEAR_DATA_STEPS.DELETING, - deleting: i18n('deleteAllDataProgress'), - }; - }, - }); -})(); diff --git a/js/views/password_view.js b/js/views/password_view.js index a2ed42a26..f88ffbafa 100644 --- a/js/views/password_view.js +++ b/js/views/password_view.js @@ -1,7 +1,4 @@ -/* global i18n: false */ -/* global Whisper: false */ - -/* eslint-disable no-new */ +/* global Whisper */ // eslint-disable-next-line func-names (function() { @@ -9,63 +6,20 @@ window.Whisper = window.Whisper || {}; - const MIN_LOGIN_TRIES = 3; - Whisper.PasswordView = Whisper.View.extend({ - className: 'password full-screen-flow standalone-fullscreen', - templateName: 'password', - events: { - keyup: 'onKeyup', - 'click #unlock-button': 'onLogin', - 'click #reset-button': 'onReset', - }, initialize() { - this.errorCount = 0; this.render(); }, - render_attributes() { - return { - title: i18n('passwordViewTitle'), - buttonText: i18n('unlock'), - resetText: i18n('resetDatabase'), - showReset: this.errorCount >= MIN_LOGIN_TRIES, - }; - }, - onKeyup(event) { - switch (event.key) { - case 'Enter': - this.onLogin(); - break; - default: - return; - } - event.preventDefault(); - }, - async onLogin() { - const passPhrase = this.$('#passPhrase').val(); - const trimmed = passPhrase ? passPhrase.trim() : passPhrase; - this.setError(''); - try { - await window.onLogin(trimmed); - } catch (e) { - // Increment the error counter and show the button if necessary - this.errorCount += 1; - if (this.errorCount >= MIN_LOGIN_TRIES) { - this.render(); - } - this.setError(`Error: ${e}`); - } - }, - setError(string) { - this.$('.error').text(string); - }, - onReset() { - const clearDataView = new window.Whisper.ClearDataView(() => { - window.resetDatabase(); + render() { + this.passwordView = new window.Whisper.ReactWrapperView({ + className: 'password overlay', + Component: window.Signal.Components.SessionPasswordPrompt, + props: {}, }); - clearDataView.render(); - this.$el.append(clearDataView.el); + + this.$el.append(this.passwordView.el); + return this; }, }); })(); diff --git a/password.html b/password.html index 05f5078b7..05478651e 100644 --- a/password.html +++ b/password.html @@ -16,63 +16,12 @@ - - - + + -
diff --git a/password_preload.js b/password_preload.js index 8d8a5b7de..1144878ea 100644 --- a/password_preload.js +++ b/password_preload.js @@ -8,6 +8,9 @@ const config = url.parse(window.location.toString(), true).query; const { locale } = config; const localeMessages = ipcRenderer.sendSync('locale-data'); +window.React = require('react'); +window.ReactDOM = require('react-dom'); + window.theme = config.theme; window.i18n = i18n.setup(locale, localeMessages); @@ -17,6 +20,9 @@ window.getAppInstance = () => config.appInstance; // So far we're only using this for Signal.Types const Signal = require('./js/modules/signal'); +const electron = require('electron'); + +const ipc = electron.ipcRenderer; window.Signal = Signal.setup({ Attachments: null, @@ -24,12 +30,32 @@ window.Signal = Signal.setup({ getRegionCode: () => null, }); +window.Signal.Logs = require('./js/modules/logs'); + +window.CONSTANTS = { + MAX_LOGIN_TRIES: 3, + MAX_PASSWORD_LENGTH: 32, + MAX_USERNAME_LENGTH: 20, +}; + window.passwordUtil = require('./app/password_util'); +window.Signal.Logs = require('./js/modules/logs'); window.resetDatabase = () => { window.log.info('reset database'); ipcRenderer.send('resetDatabase'); }; + +window.restart = () => { + window.log.info('restart'); + ipc.send('restart'); +}; + +window.clearLocalData = async () => { + window.resetDatabase(); + window.restart(); +}; + window.onLogin = passPhrase => new Promise((resolve, reject) => { ipcRenderer.once('password-window-login-response', (event, error) => { diff --git a/preload.js b/preload.js index 338143a47..2445648c0 100644 --- a/preload.js +++ b/preload.js @@ -60,8 +60,9 @@ window.isBeforeVersion = (toCheck, baseVersion) => { }; window.CONSTANTS = { - maxPasswordLength: 32, - maxUsernameLength: 20, + MAX_LOGIN_TRIES: 3, + MAX_PASSWORD_LENGTH: 32, + MAX_USERNAME_LENGTH: 20, }; window.versionInfo = { @@ -134,6 +135,11 @@ window.restart = () => { ipc.send('restart'); }; +window.resetDatabase = () => { + window.log.info('reset database'); + ipc.send('resetDatabase'); +}; + // Events for updating block number states across different windows. // In this case we need these to update the blocked number // collection on the main window from the settings window. diff --git a/stylesheets/_session.scss b/stylesheets/_session.scss index 9bf8a4372..2fee6682c 100644 --- a/stylesheets/_session.scss +++ b/stylesheets/_session.scss @@ -225,6 +225,7 @@ $session_message-container-border-radius: 5px; } .button-group > div { + display: inline-flex; margin-left: 5px; margin-right: 5px; } @@ -258,6 +259,10 @@ $session_message-container-border-radius: 5px; &.brand { color: $session-color-white; + &:hover { + filter: brightness(90%); + } + &.green, &.white, &.primary, @@ -1438,3 +1443,89 @@ input { } } } + +.clear-data, +.password-prompt { + &-wrapper { + display: flex; + justify-content: center; + align-items: center; + + background-color: $session-color-black; + + width: 100%; + height: 100%; + + padding: 3 * $session-margin-lg; + } + + &-error-section { + width: 100%; + color: $session-color-white; + margin: -$session-margin-sm 0px 2 * $session-margin-lg 0px; + + .session-label { + &.primary { + background-color: rgba($session-color-primary, 0.3); + } + padding: $session-margin-xs $session-margin-sm; + font-size: $session-font-xs; + text-align: center; + } + } + + &-container { + font-family: 'SF Pro Text'; + color: $session-color-white; + display: inline-flex; + flex-direction: column; + align-items: center; + justify-content: center; + + width: 600px; + min-width: 420px; + padding: 3 * $session-margin-lg 2 * $session-margin-lg; + box-sizing: border-box; + background-color: $session-shade-4; + border: 1px solid $session-shade-8; + border-radius: 2px; + + .warning-info-area, + .password-info-area { + display: inline-flex; + justify-content: center; + align-items: center; + + h1 { + color: $session-color-white; + } + svg { + margin-right: $session-margin-lg; + } + } + + p, + input { + margin: $session-margin-lg 0px; + } + + .button-group { + display: inline-flex; + } + + #password-prompt-input { + width: 100%; + color: #fff; + background-color: #2e2e2e; + margin-top: 2 * $session-margin-lg; + padding: $session-margin-xs $session-margin-lg; + outline: none; + border: none; + border-radius: 2px; + text-align: center; + font-size: 24px; + letter-spacing: 5px; + font-family: 'SF Pro Text'; + } + } +} diff --git a/ts/components/EditProfileDialog.tsx b/ts/components/EditProfileDialog.tsx index 959601298..555986cdc 100644 --- a/ts/components/EditProfileDialog.tsx +++ b/ts/components/EditProfileDialog.tsx @@ -208,7 +208,7 @@ export class EditProfileDialog extends React.Component { value={this.state.profileName} placeholder={placeholderText} onChange={this.onNameEdited} - maxLength={window.CONSTANTS.maxUsernameLength} + maxLength={window.CONSTANTS.MAX_USERNAME_LENGTH} tabIndex={0} required={true} aria-required={true} @@ -302,7 +302,10 @@ export class EditProfileDialog extends React.Component { private onClickOK() { const newName = this.state.profileName.trim(); - if (newName.length === 0 || newName.length > window.CONSTANTS.maxUsernameLength) { + if ( + newName.length === 0 || + newName.length > window.CONSTANTS.MAX_USERNAME_LENGTH + ) { return; } diff --git a/ts/components/session/ActionsPanel.tsx b/ts/components/session/ActionsPanel.tsx index 073d69d23..cc2601893 100644 --- a/ts/components/session/ActionsPanel.tsx +++ b/ts/components/session/ActionsPanel.tsx @@ -64,7 +64,8 @@ export class ActionsPanel extends React.Component { const handleClick = onSelect ? () => { type === SectionType.Profile - ? this.editProfileHandle() + ? /* tslint:disable-next-line:no-void-expression */ + this.editProfileHandle() : /* tslint:disable-next-line:no-void-expression */ onSelect(type); } diff --git a/ts/components/session/RegistrationTabs.tsx b/ts/components/session/RegistrationTabs.tsx index cd1adc7e7..1f8bc5c31 100644 --- a/ts/components/session/RegistrationTabs.tsx +++ b/ts/components/session/RegistrationTabs.tsx @@ -449,7 +449,7 @@ export class RegistrationTabs extends React.Component<{}, State> { type="text" placeholder={window.i18n('enterDisplayName')} value={this.state.displayName} - maxLength={window.CONSTANTS.maxUsernameLength} + maxLength={window.CONSTANTS.MAX_USERNAME_LENGTH} onValueChanged={(val: string) => { this.onDisplayNameChanged(val); }} @@ -463,7 +463,7 @@ export class RegistrationTabs extends React.Component<{}, State> { error={this.state.passwordErrorString} type="password" placeholder={window.i18n('enterOptionalPassword')} - maxLength={window.CONSTANTS.maxPasswordLength} + maxLength={window.CONSTANTS.MAX_PASSWORD_LENGTH} onValueChanged={(val: string) => { this.onPasswordChanged(val); }} @@ -477,7 +477,7 @@ export class RegistrationTabs extends React.Component<{}, State> { error={passwordsDoNotMatch} type="password" placeholder={window.i18n('optionalPassword')} - maxLength={window.CONSTANTS.maxPasswordLength} + maxLength={window.CONSTANTS.MAX_PASSWORD_LENGTH} onValueChanged={(val: string) => { this.onPasswordVerifyChanged(val); }} diff --git a/ts/components/session/SessionInput.tsx b/ts/components/session/SessionInput.tsx index ab892a5d0..11432e80a 100644 --- a/ts/components/session/SessionInput.tsx +++ b/ts/components/session/SessionInput.tsx @@ -34,7 +34,14 @@ export class SessionInput extends React.PureComponent { } public render() { - const { placeholder, type, value, maxLength, enableShowHide, error } = this.props; + const { + placeholder, + type, + value, + maxLength, + enableShowHide, + error, + } = this.props; const { forceShow } = this.state; const correctType = forceShow ? 'text' : type; diff --git a/ts/components/session/SessionPasswordModal.tsx b/ts/components/session/SessionPasswordModal.tsx index c939cbfcb..fe626db39 100644 --- a/ts/components/session/SessionPasswordModal.tsx +++ b/ts/components/session/SessionPasswordModal.tsx @@ -58,14 +58,14 @@ export class SessionPasswordModal extends React.Component { type="password" id="password-modal-input" placeholder={placeholders[0]} - maxLength={window.CONSTANTS.maxPasswordLength} + maxLength={window.CONSTANTS.MAX_PASSWORD_LENGTH} /> {action !== PasswordAction.Remove && ( )}
@@ -120,7 +120,7 @@ export class SessionPasswordModal extends React.Component { $('#password-modal-input-confirm').val() ); - if (enteredPassword.length === 0 || enteredPasswordConfirm.length === 0){ + if (enteredPassword.length === 0 || enteredPasswordConfirm.length === 0) { return; } diff --git a/ts/components/session/SessionPasswordPrompt.tsx b/ts/components/session/SessionPasswordPrompt.tsx new file mode 100644 index 000000000..a48af57ce --- /dev/null +++ b/ts/components/session/SessionPasswordPrompt.tsx @@ -0,0 +1,196 @@ +import React from 'react'; +import classNames from 'classnames'; + +import { SessionIcon, SessionIconType } from './icon'; +import { + SessionButton, + SessionButtonColor, + SessionButtonType, +} from './SessionButton'; + +interface State { + error: string; + errorCount: number; + clearDataView: boolean; +} + +export class SessionPasswordPrompt extends React.PureComponent<{}, State> { + constructor(props: any) { + super(props); + + this.state = { + error: '', + errorCount: 0, + clearDataView: false, + }; + + this.onKeyUp = this.onKeyUp.bind(this); + this.initLogin = this.initLogin.bind(this); + this.initClearDataView = this.initClearDataView.bind(this); + + window.addEventListener('keyup', this.onKeyUp); + } + + public render() { + const showResetElements = + this.state.errorCount >= window.CONSTANTS.MAX_LOGIN_TRIES; + + const wrapperClass = this.state.clearDataView + ? 'clear-data-wrapper' + : 'password-prompt-wrapper'; + const containerClass = this.state.clearDataView + ? 'clear-data-container' + : 'password-prompt-container'; + const infoAreaClass = this.state.clearDataView + ? 'warning-info-area' + : 'password-info-area'; + const infoTitle = this.state.clearDataView + ? window.i18n('clearDataHeader') + : window.i18n('passwordViewTitle'); + const buttonGroup = this.state.clearDataView + ? this.renderClearDataViewButtons() + : this.renderPasswordViewButtons(); + const featureElement = this.state.clearDataView ? ( +

{window.i18n('clearDataExplanation')}

+ ) : ( + + ); + const infoIcon = this.state.clearDataView ? ( + + ) : ( + + ); + const errorSection = !this.state.clearDataView && ( +
+ {this.state.error && ( + <> + {showResetElements ? ( +
+ {window.i18n('maxPasswordAttempts')} +
+ ) : ( +
{this.state.error}
+ )} + + )} +
+ ); + + return ( +
+
+
+ {infoIcon} + +

{infoTitle}

+
+ + {featureElement} + {errorSection} + {buttonGroup} +
+
+ ); + } + + public async onKeyUp(event: any) { + switch (event.key) { + case 'Enter': + await this.initLogin(); + break; + default: + } + event.preventDefault(); + } + + public async onLogin(passPhrase: string) { + const trimmed = passPhrase ? passPhrase.trim() : passPhrase; + + try { + await window.onLogin(trimmed); + } catch (e) { + // Increment the error counter and show the button if necessary + this.setState({ + errorCount: this.state.errorCount + 1, + }); + + this.setState({ error: e }); + } + } + + private async initLogin() { + const passPhrase = String($('#password-prompt-input').val()); + await this.onLogin(passPhrase); + } + + private initClearDataView() { + this.setState({ + error: '', + errorCount: 0, + clearDataView: true, + }); + } + + private renderPasswordViewButtons(): JSX.Element { + const showResetElements = + this.state.errorCount >= window.CONSTANTS.MAX_LOGIN_TRIES; + + return ( +
+ {showResetElements && ( + <> + + + )} + +
+ ); + } + + private renderClearDataViewButtons(): JSX.Element { + return ( +
+ { + this.setState({ clearDataView: false }); + }} + /> + + +
+ ); + } +} diff --git a/ts/components/session/SessionScrollButton.tsx b/ts/components/session/SessionScrollButton.tsx index 39644e0d6..6c6950611 100644 --- a/ts/components/session/SessionScrollButton.tsx +++ b/ts/components/session/SessionScrollButton.tsx @@ -1,12 +1,8 @@ import React from 'react'; -import { SessionIconButton, SessionIconType, SessionIconSize } from './icon'; +import { SessionIconButton, SessionIconSize, SessionIconType } from './icon'; -interface Props { - count: number; -} - -export class SessionScrollButton extends React.PureComponent { +export class SessionScrollButton extends React.PureComponent { constructor(props: any) { super(props); } diff --git a/ts/components/session/icon/SessionIcon.tsx b/ts/components/session/icon/SessionIcon.tsx index 6f1bb4b0b..de8e7c43c 100644 --- a/ts/components/session/icon/SessionIcon.tsx +++ b/ts/components/session/icon/SessionIcon.tsx @@ -5,7 +5,7 @@ import { icons, SessionIconSize, SessionIconType } from '../icon'; export interface Props { iconType: SessionIconType; - iconSize: SessionIconSize; + iconSize: SessionIconSize | number; iconColor: string; iconPadded: boolean; iconRotation: number; @@ -33,21 +33,25 @@ export class SessionIcon extends React.PureComponent { } = this.props; let iconDimensions; - switch (iconSize) { - case SessionIconSize.Small: - iconDimensions = '15'; - break; - case SessionIconSize.Medium: - iconDimensions = '20'; - break; - case SessionIconSize.Large: - iconDimensions = '25'; - break; - case SessionIconSize.Huge: - iconDimensions = '30'; - break; - default: - iconDimensions = '20'; + if (typeof iconSize === 'number') { + iconDimensions = iconSize; + } else { + switch (iconSize) { + case SessionIconSize.Small: + iconDimensions = '15'; + break; + case SessionIconSize.Medium: + iconDimensions = '20'; + break; + case SessionIconSize.Large: + iconDimensions = '25'; + break; + case SessionIconSize.Huge: + iconDimensions = '30'; + break; + default: + iconDimensions = '20'; + } } const iconDef = icons[iconType]; diff --git a/ts/global.d.ts b/ts/global.d.ts index 0a426177f..dda00f796 100644 --- a/ts/global.d.ts +++ b/ts/global.d.ts @@ -4,6 +4,7 @@ interface Window { Events: any; deleteAllData: any; + clearLocalData: any; getAccountManager: any; mnemonic: any; clipboard: any; @@ -23,6 +24,7 @@ interface Window { // Following function needs to be written in background.js // getMemberList: any; + onLogin: any; setPassword: any; textsecure: any; Session: any; @@ -50,6 +52,8 @@ interface Window { getSettingValue: any; setSettingValue: any; lokiFeatureFlags: any; + + resetDatabase: any; } interface Promise {