From ec5001c4a9a708f1eccf1553f1a2bb4a3fad0bcb Mon Sep 17 00:00:00 2001 From: Vincent Date: Tue, 21 Jan 2020 13:35:47 +1100 Subject: [PATCH] Settings lock view --- .../session/settings/SessionSettings.tsx | 494 ++++++++++++------ 1 file changed, 321 insertions(+), 173 deletions(-) diff --git a/ts/components/session/settings/SessionSettings.tsx b/ts/components/session/settings/SessionSettings.tsx index 08d9d3261..3825a7880 100644 --- a/ts/components/session/settings/SessionSettings.tsx +++ b/ts/components/session/settings/SessionSettings.tsx @@ -30,8 +30,22 @@ export interface SettingsViewProps { interface State { hasPassword: boolean | null; - shouldLockSettings: boolean | null; pwdLockError: string | null; + shouldLockSettings: boolean | null; + linkedPubKeys: Array; +} + +interface LocalSettingType { + category: SessionSettingCategory; + description: string | undefined; + comparisonValue: string | undefined; + id: any; + content: any | undefined; + hidden: any; + title: string; + type: SessionSettingType | undefined; + setFn: any; + onClick: any; } export class SettingsView extends React.Component { @@ -44,6 +58,7 @@ export class SettingsView extends React.Component { hasPassword: null, pwdLockError: null, shouldLockSettings: true, + linkedPubKeys: new Array(), }; this.settingsViewRef = React.createRef(); @@ -51,16 +66,228 @@ export class SettingsView extends React.Component { this.validatePasswordLock = this.validatePasswordLock.bind(this); this.hasPassword(); + this.refreshLinkedDevice = this.refreshLinkedDevice.bind(this); + } + + public componentDidMount() { + window.Whisper.events.on('refreshLinkedDeviceList', async () => { + setTimeout(() => { + this.refreshLinkedDevice(); + }, 1000); + }); + this.refreshLinkedDevice(); + } + + public componentWillUnmount() { + window.Whisper.events.off('refreshLinkedDeviceList'); } /* tslint:disable-next-line:max-func-body-length */ - public renderSettingInCategory() { + public renderSettingInCategory(): JSX.Element { + const { category } = this.props; + + let settings: Array; + + if (category === SessionSettingCategory.Devices) { + // special case for linked devices + settings = this.getLinkedDeviceSettings(); + } else { + // Grab initial values from database on startup + // ID corresponds to installGetter parameters in preload.js + // They are NOT arbitrary; add with caution + + settings = this.getLocalSettings(); + } + + return ( + <> + {this.state.hasPassword !== null && + settings.map(setting => { + const content = setting.content || undefined; + const shouldRenderSettings = setting.category === category; + const description = setting.description || ''; + + const comparisonValue = setting.comparisonValue || null; + const value = + window.getSettingValue(setting.id, comparisonValue) || + (setting.content && setting.content.defaultValue); + + const sliderFn = + setting.type === SessionSettingType.Slider + ? (settingValue: any) => + window.setSettingValue(setting.id, settingValue) + : () => null; + + const onClickFn = + setting.onClick || + (() => { + this.updateSetting(setting); + }); + + return ( +
+ {shouldRenderSettings && + !setting.hidden && ( + + )} +
+ ); + })} + + ); + } + + public renderPasswordLock() { + return ( +
+
+

{window.i18n('password')}

+ + + {this.state.pwdLockError && ( + <> +
+ {this.state.pwdLockError} +
+
+ + )} + + +
+
+ ); + } + + public async validatePasswordLock() { + const enteredPassword = String($('#password-lock-input').val()); + + if (!enteredPassword) { + this.setState({ + pwdLockError: window.i18n('noGivenPassword'), + }); + return false; + } + + // Check if the password matches the hash we have stored + const hash = await window.Signal.Data.getPasswordHash(); + if (hash && !window.passwordUtil.matchesHash(enteredPassword, hash)) { + this.setState({ + pwdLockError: window.i18n('invalidPassword'), + }); + + return false; + } + + // Unlocked settings + this.setState({ + shouldLockSettings: false, + pwdLockError: null, + }); + + return true; + } + + public render() { + const { category } = this.props; + const shouldRenderPasswordLock = + this.state.shouldLockSettings && this.state.hasPassword; + + return ( +
+ + {shouldRenderPasswordLock ? ( + this.renderPasswordLock() + ) : ( +
+ {this.renderSettingInCategory()} +
+ )} +
+ ); + } + + public setOptionsSetting(settingID: string) { + const selectedValue = $(`#${settingID} .session-radio input:checked`).val(); + window.setSettingValue(settingID, selectedValue); + } + + public hasPassword() { + const hashPromise = window.Signal.Data.getPasswordHash(); + + hashPromise.then((hash: any) => { + this.setState({ + hasPassword: !!hash, + }); + }); + } + + public updateSetting(item: any) { + // If there's a custom afterClick function, + // execute it instead of automatically updating settings + if (item.setFn) { + item.setFn(); + } else { + if (item.type === SessionSettingType.Toggle) { + // If no custom afterClick function given, alter values in storage here + // Switch to opposite state + const newValue = !window.getSettingValue(item.id); + window.setSettingValue(item.id, newValue); + } + } + } + + public onPasswordUpdated(action: string) { + if (action === 'set' || action === 'change') { + this.setState({ + hasPassword: true, + shouldLockSettings: true, + pwdLockError: null, + }); + } + + if (action === 'remove') { + this.setState({ + hasPassword: false, + }); + } + } + + private getPubkeyName(pubKey: string | null) { + if (!pubKey) { + return {}; + } + + const secretWords = window.mnemonic.pubkey_to_secret_words(pubKey); + const conv = window.ConversationController.get(pubKey); + const deviceAlias = conv ? conv.getNickname() : 'Unnamed Device'; + + return { deviceAlias, secretWords }; + } + + // tslint:disable-next-line: max-func-body-length + private getLocalSettings(): Array { const { Settings } = window.Signal.Types; - // Grab initial values from database on startup - // ID corresponds to instalGetter parameters in preload.js - // They are NOT arbitrary; add with caution - const localSettings = [ + return [ { id: 'theme-setting', title: window.i18n('themeToggleTitle'), @@ -70,7 +297,8 @@ export class SettingsView extends React.Component { type: SessionSettingType.Toggle, category: SessionSettingCategory.General, setFn: window.toggleTheme, - content: {}, + content: undefined, + onClick: undefined, }, { id: 'hide-menu-bar', @@ -80,7 +308,9 @@ export class SettingsView extends React.Component { type: SessionSettingType.Toggle, category: SessionSettingCategory.General, setFn: window.toggleMenuBar, - content: {}, + content: undefined, + comparisonValue: undefined, + onClick: undefined, }, { id: 'spell-check', @@ -90,7 +320,9 @@ export class SettingsView extends React.Component { type: SessionSettingType.Toggle, category: SessionSettingCategory.General, setFn: window.toggleSpellCheck, - content: {}, + content: undefined, + comparisonValue: undefined, + onClick: undefined, }, { id: 'link-preview-setting', @@ -100,13 +332,19 @@ export class SettingsView extends React.Component { type: SessionSettingType.Toggle, category: SessionSettingCategory.General, setFn: window.toggleLinkPreview, - content: {}, + content: undefined, + comparisonValue: undefined, + onClick: undefined, }, { id: 'notification-setting', title: window.i18n('notificationSettingsDialog'), type: SessionSettingType.Options, category: SessionSettingCategory.Notifications, + comparisonValue: undefined, + description: undefined, + hidden: undefined, + onClick: undefined, setFn: () => { this.setOptionsSetting('notification-setting'); }, @@ -144,7 +382,9 @@ export class SettingsView extends React.Component { type: SessionSettingType.Toggle, category: SessionSettingCategory.Permissions, setFn: window.toggleMediaPermissions, - content: {}, + content: undefined, + comparisonValue: undefined, + onClick: undefined, }, { id: 'message-ttl', @@ -154,6 +394,8 @@ export class SettingsView extends React.Component { type: SessionSettingType.Slider, category: SessionSettingCategory.Privacy, setFn: undefined, + comparisonValue: undefined, + onClick: undefined, content: { defaultValue: 24, }, @@ -166,6 +408,7 @@ export class SettingsView extends React.Component { type: SessionSettingType.Button, category: SessionSettingCategory.Privacy, setFn: undefined, + comparisonValue: undefined, content: { buttonText: window.i18n('setPassword'), buttonColor: SessionButtonColor.Primary, @@ -184,6 +427,7 @@ export class SettingsView extends React.Component { type: SessionSettingType.Button, category: SessionSettingCategory.Privacy, setFn: undefined, + comparisonValue: undefined, content: { buttonText: window.i18n('changePassword'), buttonColor: SessionButtonColor.Primary, @@ -202,6 +446,7 @@ export class SettingsView extends React.Component { type: SessionSettingType.Button, category: SessionSettingCategory.Privacy, setFn: undefined, + comparisonValue: undefined, content: { buttonText: window.i18n('removePassword'), buttonColor: SessionButtonColor.Danger, @@ -213,175 +458,78 @@ export class SettingsView extends React.Component { }), }, ]; - - return ( - <> - {this.state.hasPassword !== null && - localSettings.map(setting => { - const { category } = this.props; - const content = setting.content || undefined; - const shouldRenderSettings = setting.category === category; - const description = setting.description || ''; - - const comparisonValue = setting.comparisonValue || null; - const value = - window.getSettingValue(setting.id, comparisonValue) || - setting.content.defaultValue; - - const sliderFn = - setting.type === SessionSettingType.Slider - ? (settingValue: any) => - window.setSettingValue(setting.id, settingValue) - : () => null; - - const onClickFn = - setting.onClick || - (() => { - this.updateSetting(setting); - }); - - return ( -
- {shouldRenderSettings && - !setting.hidden && ( - - )} -
- ); - })} - - ); } - public renderPasswordLock() { - return ( -
-
-

{window.i18n('password')}

- - - {this.state.pwdLockError && ( - <> -
- {this.state.pwdLockError} -
-
- - )} - - -
-
- ); - } - - public async validatePasswordLock() { - const enteredPassword = String($('#password-lock-input').val()); - - if (!enteredPassword) { - this.setState({ - pwdLockError: window.i18n('noGivenPassword'), - }); - return false; - } - - // Check if the password matches the hash we have stored - const hash = await window.Signal.Data.getPasswordHash(); - if (hash && !window.passwordUtil.matchesHash(enteredPassword, hash)) { - this.setState({ - pwdLockError: window.i18n('invalidPassword'), - }); - - return false; - } - - // Unlocked settings - this.setState({ - shouldLockSettings: false, - }); - - return true; - } - - public hasPassword() { - const hashPromise = window.Signal.Data.getPasswordHash(); - - hashPromise.then((hash: any) => { - this.setState({ - hasPassword: !!hash, + private getLinkedDeviceSettings(): Array { + const { linkedPubKeys } = this.state; + + if (linkedPubKeys && linkedPubKeys.length > 0) { + return linkedPubKeys.map((pubkey: any) => { + const { deviceAlias, secretWords } = this.getPubkeyName(pubkey); + const description = `${secretWords} ${window.shortenPubkey(pubkey)}`; + + if (window.lokiFeatureFlags.multiDeviceUnpairing) { + return { + id: pubkey, + title: deviceAlias, + description: description, + type: SessionSettingType.Button, + category: SessionSettingCategory.Devices, + content: { + buttonColor: SessionButtonColor.Danger, + buttonText: window.i18n('unpairDevice'), + }, + comparisonValue: undefined, + setFn: () => { + window.Whisper.events.trigger('showDevicePairingDialog', { + pubKeyToUnpair: pubkey, + }); + }, + hidden: undefined, + onClick: undefined, + }; + } else { + return { + id: pubkey, + title: deviceAlias, + description: description, + type: undefined, + category: SessionSettingCategory.Devices, + content: {}, + comparisonValue: undefined, + setFn: undefined, + hidden: undefined, + onClick: undefined, + }; + } }); - }); - } - - public render() { - const { category } = this.props; - const shouldRenderPasswordLock = - this.state.shouldLockSettings && this.state.hasPassword; - - return ( -
- - {shouldRenderPasswordLock ? ( - this.renderPasswordLock() - ) : ( -
- {this.renderSettingInCategory()} -
- )} -
- ); - } - - public updateSetting(item: any) { - // If there's a custom afterClick function, - // execute it instead of automatically updating settings - if (item.setFn) { - item.setFn(); } else { - if (item.type === SessionSettingType.Toggle) { - // If no custom afterClick function given, alter values in storage here - // Switch to opposite state - const newValue = !window.getSettingValue(item.id); - window.setSettingValue(item.id, newValue); - } + return [ + { + id: 'no-linked-device', + title: window.i18n('noPairedDevices'), + type: undefined, + description: '', + category: SessionSettingCategory.Devices, + content: {}, + comparisonValue: undefined, + onClick: undefined, + setFn: undefined, + hidden: undefined, + }, + ]; } } - public setOptionsSetting(settingID: string) { - const selectedValue = $(`#${settingID} .session-radio input:checked`).val(); - window.setSettingValue(settingID, selectedValue); - } + private refreshLinkedDevice() { + const ourPubKey = window.textsecure.storage.user.getNumber(); - public onPasswordUpdated(action: string) { - if (action === 'set' || action === 'change') { - this.setState({ - hasPassword: true, - shouldLockSettings: true, - }); - } - - if (action === 'remove') { - this.setState({ - hasPassword: false, + window.libloki.storage + .getSecondaryDevicesFor(ourPubKey) + .then((pubKeys: any) => { + this.setState({ + linkedPubKeys: pubKeys, + }); }); - } } -} +} \ No newline at end of file