Update safety number change warning dialog
This commit is contained in:
parent
e87a0103cc
commit
5b83485c89
38 changed files with 1221 additions and 425 deletions
|
@ -52,6 +52,28 @@ addDecorator((storyFn /* , context */) => {
|
||||||
const secondPaneDeviceTheme = makeDeviceThemeKnob('Second');
|
const secondPaneDeviceTheme = makeDeviceThemeKnob('Second');
|
||||||
const secondPaneMode = makeModeKnob('Second');
|
const secondPaneMode = makeModeKnob('Second');
|
||||||
|
|
||||||
|
// Adding it to the body as well so that we can cover modals and other
|
||||||
|
// components that are rendered outside of this decorator container
|
||||||
|
if (firstPaneTheme === '') {
|
||||||
|
document.body.classList.remove('dark-theme');
|
||||||
|
} else {
|
||||||
|
document.body.classList.add('dark-theme');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstPaneDeviceTheme === '') {
|
||||||
|
document.body.classList.remove('ios-theme');
|
||||||
|
} else {
|
||||||
|
document.body.classList.add('ios-theme');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstPaneMode === 'mouse-mode') {
|
||||||
|
document.body.classList.remove('keyboard-mode');
|
||||||
|
document.body.classList.add('mouse-mode');
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove('mouse-mode');
|
||||||
|
document.body.classList.add('keyboard-mode');
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<ClassyProvider themes={['dark']}>
|
<ClassyProvider themes={['dark']}>
|
||||||
|
|
|
@ -345,20 +345,6 @@
|
||||||
"message": "Your safety numbers with these group members have changed since you last verified. Click a group member to see your new safety number with them.",
|
"message": "Your safety numbers with these group members have changed since you last verified. Click a group member to see your new safety number with them.",
|
||||||
"description": "When there are multiple previously-verified group members with safety number changes, a banner will be shown. The list of contacts with safety number changes is shown, and this text introduces that list."
|
"description": "When there are multiple previously-verified group members with safety number changes, a banner will be shown. The list of contacts with safety number changes is shown, and this text introduces that list."
|
||||||
},
|
},
|
||||||
"changedSinceVerifiedMultiple": {
|
|
||||||
"message": "Your safety numbers with multiple group members have changed since you last verified. This could mean that someone is trying to intercept your communication or that they have simply reinstalled Signal.",
|
|
||||||
"description": "Shown on confirmation dialog when user attempts to send a message"
|
|
||||||
},
|
|
||||||
"changedSinceVerified": {
|
|
||||||
"message": "Your safety number with $name$ has changed since you last verified. This could mean that someone is trying to intercept your communication or that $name$ has simply reinstalled Signal.",
|
|
||||||
"description": "Shown on confirmation dialog when user attempts to send a message",
|
|
||||||
"placeholders": {
|
|
||||||
"name": {
|
|
||||||
"content": "$1",
|
|
||||||
"example": "Bob"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"changedRightAfterVerify": {
|
"changedRightAfterVerify": {
|
||||||
"message": "The safety number you are trying to verify has changed. Please review your new safety number with $name$. Remember, this change could mean that someone is trying to intercept your communication or that $name$ has simply reinstalled Signal.",
|
"message": "The safety number you are trying to verify has changed. Please review your new safety number with $name$. Remember, this change could mean that someone is trying to intercept your communication or that $name$ has simply reinstalled Signal.",
|
||||||
"description": "Shown on the safety number screen when the user has selected to verify/unverify a contact's safety number, and we immediately discover a safety number change",
|
"description": "Shown on the safety number screen when the user has selected to verify/unverify a contact's safety number, and we immediately discover a safety number change",
|
||||||
|
@ -369,20 +355,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"changedRecentlyMultiple": {
|
"changedVerificationWarning": {
|
||||||
"message": "Your safety numbers with multiple group members have changed recently. This could mean that someone is trying to intercept your communication or that they have simply reinstalled Signal.",
|
"message": "The following people may have reinstalled or changed devices. Verify your safety number with them to ensure privacy.",
|
||||||
"description": "Shown on confirmation dialog when user attempts to send a message"
|
"description": "Shown on confirmation dialog when user attempts to send a message"
|
||||||
},
|
},
|
||||||
"changedRecently": {
|
|
||||||
"message": "Your safety number with $name$ has changed recently. This could mean that someone is trying to intercept your communication or that $name$ has simply reinstalled Signal.",
|
|
||||||
"description": "Shown on confirmation dialog when user attempts to send a message",
|
|
||||||
"placeholders": {
|
|
||||||
"name": {
|
|
||||||
"content": "$1",
|
|
||||||
"example": "Bob"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"identityKeyErrorOnSend": {
|
"identityKeyErrorOnSend": {
|
||||||
"message": "Your safety number with $name$ has changed. This could either mean that someone is trying to intercept your communication or that $name$ has simply reinstalled Signal. You may wish to verify your safety number with this contact.",
|
"message": "Your safety number with $name$ has changed. This could either mean that someone is trying to intercept your communication or that $name$ has simply reinstalled Signal. You may wish to verify your safety number with this contact.",
|
||||||
"description": "Shown when user clicks on a failed recipient in the message detail view after an identity key change",
|
"description": "Shown when user clicks on a failed recipient in the message detail view after an identity key change",
|
||||||
|
@ -1579,6 +1555,10 @@
|
||||||
"message": "Safety Number has changed",
|
"message": "Safety Number has changed",
|
||||||
"description": "A notification shown in the conversation when a contact reinstalls"
|
"description": "A notification shown in the conversation when a contact reinstalls"
|
||||||
},
|
},
|
||||||
|
"safetyNumberChanges": {
|
||||||
|
"message": "Safety Number Changes",
|
||||||
|
"description": "Title for safety number changed modal"
|
||||||
|
},
|
||||||
"safetyNumberChangedGroup": {
|
"safetyNumberChangedGroup": {
|
||||||
"message": "Safety Number with $name$ has changed",
|
"message": "Safety Number with $name$ has changed",
|
||||||
"description": "A notification shown in a group conversation when a contact reinstalls, showing the contact name",
|
"description": "A notification shown in a group conversation when a contact reinstalls, showing the contact name",
|
||||||
|
|
|
@ -123,6 +123,10 @@
|
||||||
</div>
|
</div>
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script type='text/x-tmpl-mustache' id='safety-number-change-dialog'>
|
||||||
|
<div class='safety-number-change-dialog-wrapper'></div>
|
||||||
|
</script>
|
||||||
|
|
||||||
<script type='text/x-tmpl-mustache' id='identicon-svg'>
|
<script type='text/x-tmpl-mustache' id='identicon-svg'>
|
||||||
<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'>
|
<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'>
|
||||||
<circle cx='50' cy='50' r='40' fill='{{ color }}' />
|
<circle cx='50' cy='50' r='40' fill='{{ color }}' />
|
||||||
|
@ -156,33 +160,8 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script type='text/x-tmpl-mustache' id='key-verification'>
|
<script type='text/x-tmpl-mustache' id='key-verification'>
|
||||||
<div class='container' tabindex='0'>
|
<div class="container" tabindex="0">
|
||||||
{{ ^hasTheirKey }}
|
<div class="key-verification-wrapper"></div>
|
||||||
<div class='placeholder'>{{ theirKeyUnknown }}</div>
|
|
||||||
{{ /hasTheirKey }}
|
|
||||||
{{ #hasTheirKey }}
|
|
||||||
<label> {{ yourSafetyNumberWith }} </label>
|
|
||||||
<!--<div class='qr'></div>-->
|
|
||||||
<div class='key'>
|
|
||||||
{{ #chunks }} <span>{{ . }}</span> {{ /chunks }}
|
|
||||||
</div>
|
|
||||||
{{ /hasTheirKey }}
|
|
||||||
{{ verifyHelp }}
|
|
||||||
<p> {{> link_to_support }} </p>
|
|
||||||
<div class='summary'>
|
|
||||||
{{ #isVerified }}
|
|
||||||
<span class='icon verified'></span>
|
|
||||||
{{ /isVerified }}
|
|
||||||
{{ ^isVerified }}
|
|
||||||
<span class='icon shield'></span>
|
|
||||||
{{ /isVerified }}
|
|
||||||
{{ verifiedStatus }}
|
|
||||||
</div>
|
|
||||||
<div class='verify'>
|
|
||||||
<button class='verify grey'>
|
|
||||||
{{ verifyButton }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -387,6 +366,7 @@
|
||||||
<script type='text/javascript' src='js/views/install_view.js'></script>
|
<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/banner_view.js'></script>
|
||||||
<script type="text/javascript" src="js/views/phone-input-view.js"></script>
|
<script type="text/javascript" src="js/views/phone-input-view.js"></script>
|
||||||
|
<script type='text/javascript' src='js/views/safety_number_change_dialog_view.js'></script>
|
||||||
<script type='text/javascript' src='js/views/standalone_registration_view.js'></script>
|
<script type='text/javascript' src='js/views/standalone_registration_view.js'></script>
|
||||||
<script type='text/javascript' src='js/views/app_view.js'></script>
|
<script type='text/javascript' src='js/views/app_view.js'></script>
|
||||||
<script type='text/javascript' src='js/views/clear_data_view.js'></script>
|
<script type='text/javascript' src='js/views/clear_data_view.js'></script>
|
||||||
|
|
|
@ -451,9 +451,12 @@
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
uuid: this.get('uuid'),
|
||||||
|
e164: this.get('e164'),
|
||||||
|
|
||||||
isArchived: this.get('isArchived'),
|
isArchived: this.get('isArchived'),
|
||||||
isBlocked: this.isBlocked(),
|
isBlocked: this.isBlocked(),
|
||||||
|
isVerified: this.isVerified(),
|
||||||
activeAt: this.get('active_at'),
|
activeAt: this.get('active_at'),
|
||||||
avatarPath: this.getAvatarPath(),
|
avatarPath: this.getAvatarPath(),
|
||||||
color,
|
color,
|
||||||
|
|
|
@ -40,6 +40,9 @@ const {
|
||||||
MessageDetail,
|
MessageDetail,
|
||||||
} = require('../../ts/components/conversation/MessageDetail');
|
} = require('../../ts/components/conversation/MessageDetail');
|
||||||
const { Quote } = require('../../ts/components/conversation/Quote');
|
const { Quote } = require('../../ts/components/conversation/Quote');
|
||||||
|
const {
|
||||||
|
SafetyNumberChangeDialog,
|
||||||
|
} = require('../../ts/components/SafetyNumberChangeDialog');
|
||||||
const {
|
const {
|
||||||
StagedLinkPreview,
|
StagedLinkPreview,
|
||||||
} = require('../../ts/components/conversation/StagedLinkPreview');
|
} = require('../../ts/components/conversation/StagedLinkPreview');
|
||||||
|
@ -51,6 +54,9 @@ const {
|
||||||
} = require('../../ts/state/roots/createCompositionArea');
|
} = require('../../ts/state/roots/createCompositionArea');
|
||||||
const { createCallManager } = require('../../ts/state/roots/createCallManager');
|
const { createCallManager } = require('../../ts/state/roots/createCallManager');
|
||||||
const { createLeftPane } = require('../../ts/state/roots/createLeftPane');
|
const { createLeftPane } = require('../../ts/state/roots/createLeftPane');
|
||||||
|
const {
|
||||||
|
createSafetyNumberViewer,
|
||||||
|
} = require('../../ts/state/roots/createSafetyNumberViewer');
|
||||||
const {
|
const {
|
||||||
createStickerManager,
|
createStickerManager,
|
||||||
} = require('../../ts/state/roots/createStickerManager');
|
} = require('../../ts/state/roots/createStickerManager');
|
||||||
|
@ -274,6 +280,7 @@ exports.setup = (options = {}) => {
|
||||||
MediaGallery,
|
MediaGallery,
|
||||||
MessageDetail,
|
MessageDetail,
|
||||||
Quote,
|
Quote,
|
||||||
|
SafetyNumberChangeDialog,
|
||||||
StagedLinkPreview,
|
StagedLinkPreview,
|
||||||
Types: {
|
Types: {
|
||||||
Message: MediaGalleryMessage,
|
Message: MediaGalleryMessage,
|
||||||
|
@ -284,6 +291,7 @@ exports.setup = (options = {}) => {
|
||||||
createCallManager,
|
createCallManager,
|
||||||
createCompositionArea,
|
createCompositionArea,
|
||||||
createLeftPane,
|
createLeftPane,
|
||||||
|
createSafetyNumberViewer,
|
||||||
createShortcutGuideModal,
|
createShortcutGuideModal,
|
||||||
createStickerManager,
|
createStickerManager,
|
||||||
createStickerPreviewModal,
|
createStickerPreviewModal,
|
||||||
|
|
|
@ -2502,33 +2502,17 @@
|
||||||
|
|
||||||
showSendAnywayDialog(contacts) {
|
showSendAnywayDialog(contacts) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
let message;
|
const dialog = new Whisper.SafetyNumberChangeDialogView({
|
||||||
const isUnverified = this.model.isUnverified();
|
contacts,
|
||||||
|
reject: () => {
|
||||||
if (contacts.length > 1) {
|
resolve(false);
|
||||||
if (isUnverified) {
|
},
|
||||||
message = i18n('changedSinceVerifiedMultiple');
|
resolve: () => {
|
||||||
} else {
|
resolve(true);
|
||||||
message = i18n('changedRecentlyMultiple');
|
},
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const contactName = contacts.at(0).getTitle();
|
|
||||||
if (isUnverified) {
|
|
||||||
message = i18n('changedSinceVerified', [contactName, contactName]);
|
|
||||||
} else {
|
|
||||||
message = i18n('changedRecently', [contactName, contactName]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const dialog = new Whisper.ConfirmationDialogView({
|
|
||||||
message,
|
|
||||||
okText: i18n('sendAnyway'),
|
|
||||||
resolve: () => resolve(true),
|
|
||||||
reject: () => resolve(false),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.$el.prepend(dialog.el);
|
this.$el.prepend(dialog.el);
|
||||||
dialog.focusCancel();
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/* global Whisper, textsecure, QRCode, dcodeIO, libsignal, i18n, _ */
|
/* global Whisper, Signal */
|
||||||
|
|
||||||
/* eslint-disable more/no-then */
|
/* eslint-disable more/no-then */
|
||||||
|
|
||||||
|
@ -9,152 +9,18 @@
|
||||||
window.Whisper = window.Whisper || {};
|
window.Whisper = window.Whisper || {};
|
||||||
|
|
||||||
Whisper.KeyVerificationPanelView = Whisper.View.extend({
|
Whisper.KeyVerificationPanelView = Whisper.View.extend({
|
||||||
className: 'key-verification panel',
|
className: 'panel',
|
||||||
templateName: 'key-verification',
|
templateName: 'key-verification',
|
||||||
events: {
|
|
||||||
'click button.verify': 'toggleVerified',
|
|
||||||
},
|
|
||||||
initialize(options) {
|
initialize(options) {
|
||||||
this.ourNumber = textsecure.storage.user.getNumber();
|
|
||||||
this.ourUuid = textsecure.storage.user.getUuid();
|
|
||||||
if (options.newKey) {
|
|
||||||
this.theirKey = options.newKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loadTheirKey();
|
|
||||||
this.loadOurKey();
|
|
||||||
|
|
||||||
this.render();
|
this.render();
|
||||||
if (options.onLoad) {
|
|
||||||
options.onLoad(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loadKeys().then(() => {
|
const view = new Whisper.ReactWrapperView({
|
||||||
this.listenTo(this.model, 'change', this.render);
|
JSX: Signal.State.Roots.createSafetyNumberViewer(window.reduxStore, {
|
||||||
});
|
contactID: options.model.get('id'),
|
||||||
},
|
}),
|
||||||
async loadKeys() {
|
|
||||||
await this.generateSecurityNumber();
|
|
||||||
this.render();
|
|
||||||
},
|
|
||||||
makeQRCode() {
|
|
||||||
// Per Lilia: We can't turn this on until it generates a Latin1 string, as is
|
|
||||||
// required by the mobile clients.
|
|
||||||
new QRCode(this.$('.qr')[0]).makeCode(
|
|
||||||
dcodeIO.ByteBuffer.wrap(this.ourKey).toString('base64')
|
|
||||||
);
|
|
||||||
},
|
|
||||||
loadTheirKey() {
|
|
||||||
const item = textsecure.storage.protocol.getIdentityRecord(
|
|
||||||
this.model.get('id')
|
|
||||||
);
|
|
||||||
this.theirKey = item ? item.publicKey : null;
|
|
||||||
},
|
|
||||||
loadOurKey() {
|
|
||||||
const item = textsecure.storage.protocol.getIdentityRecord(
|
|
||||||
this.ourUuid || this.ourNumber
|
|
||||||
);
|
|
||||||
this.ourKey = item ? item.publicKey : null;
|
|
||||||
},
|
|
||||||
generateSecurityNumber() {
|
|
||||||
return new libsignal.FingerprintGenerator(5200)
|
|
||||||
.createFor(
|
|
||||||
// TODO: we cannot use UUIDs for safety numbers yet
|
|
||||||
// this.ourUuid || this.ourNumber,
|
|
||||||
this.ourNumber,
|
|
||||||
this.ourKey,
|
|
||||||
// TODO: we cannot use UUIDs for safety numbers yet
|
|
||||||
// this.model.get('uuid') || this.model.get('e164'),
|
|
||||||
this.model.get('e164'),
|
|
||||||
this.theirKey
|
|
||||||
)
|
|
||||||
.then(securityNumber => {
|
|
||||||
this.securityNumber = securityNumber;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onSafetyNumberChanged() {
|
|
||||||
this.model.getProfiles().then(this.loadKeys.bind(this));
|
|
||||||
|
|
||||||
const dialog = new Whisper.ConfirmationDialogView({
|
|
||||||
message: i18n('changedRightAfterVerify', [
|
|
||||||
this.model.getTitle(),
|
|
||||||
this.model.getTitle(),
|
|
||||||
]),
|
|
||||||
hideCancel: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
dialog.$el.insertBefore(this.el);
|
this.$('.key-verification-wrapper').append(view.el);
|
||||||
dialog.focusCancel();
|
|
||||||
},
|
|
||||||
toggleVerified() {
|
|
||||||
this.$('button.verify').attr('disabled', true);
|
|
||||||
this.model
|
|
||||||
.toggleVerified()
|
|
||||||
.catch(result => {
|
|
||||||
if (result instanceof Error) {
|
|
||||||
if (result.name === 'OutgoingIdentityKeyError') {
|
|
||||||
this.onSafetyNumberChanged();
|
|
||||||
} else {
|
|
||||||
window.log.error(
|
|
||||||
'failed to toggle verified:',
|
|
||||||
result && result.stack ? result.stack : result
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const keyError = _.some(
|
|
||||||
result.errors,
|
|
||||||
error => error.name === 'OutgoingIdentityKeyError'
|
|
||||||
);
|
|
||||||
if (keyError) {
|
|
||||||
this.onSafetyNumberChanged();
|
|
||||||
} else {
|
|
||||||
_.forEach(result.errors, error => {
|
|
||||||
window.log.error(
|
|
||||||
'failed to toggle verified:',
|
|
||||||
error && error.stack ? error.stack : error
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.$('button.verify').removeAttr('disabled');
|
|
||||||
});
|
|
||||||
},
|
|
||||||
render_attributes() {
|
|
||||||
const s = this.securityNumber;
|
|
||||||
const chunks = [];
|
|
||||||
if (s) {
|
|
||||||
for (let i = 0; i < s.length; i += 5) {
|
|
||||||
chunks.push(s.substring(i, i + 5));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (let i = 0; i < 12; i += 1) {
|
|
||||||
chunks.push('XXXXX');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const name = this.model.getTitle();
|
|
||||||
const yourSafetyNumberWith = i18n(
|
|
||||||
'yourSafetyNumberWith',
|
|
||||||
this.model.getTitle()
|
|
||||||
);
|
|
||||||
const isVerified = this.model.isVerified();
|
|
||||||
const verifyButton = isVerified ? i18n('unverify') : i18n('verify');
|
|
||||||
const verifiedStatus = isVerified
|
|
||||||
? i18n('isVerified', name)
|
|
||||||
: i18n('isNotVerified', name);
|
|
||||||
|
|
||||||
return {
|
|
||||||
learnMore: i18n('learnMore'),
|
|
||||||
theirKeyUnknown: i18n('theirIdentityUnknown'),
|
|
||||||
yourSafetyNumberWith,
|
|
||||||
verifyHelp: i18n('verifyHelp', this.model.getTitle()),
|
|
||||||
verifyButton,
|
|
||||||
hasTheirKey: this.theirKey !== undefined,
|
|
||||||
chunks,
|
|
||||||
isVerified,
|
|
||||||
verifiedStatus,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
39
js/views/safety_number_change_dialog_view.js
Normal file
39
js/views/safety_number_change_dialog_view.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
/* global Whisper, Signal */
|
||||||
|
|
||||||
|
// eslint-disable-next-line func-names
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
window.Whisper = window.Whisper || {};
|
||||||
|
|
||||||
|
Whisper.SafetyNumberChangeDialogView = Whisper.View.extend({
|
||||||
|
templateName: 'safety-number-change-dialog',
|
||||||
|
initialize(options) {
|
||||||
|
const dialog = new Whisper.ReactWrapperView({
|
||||||
|
Component: window.Signal.Components.SafetyNumberChangeDialog,
|
||||||
|
props: {
|
||||||
|
contacts: options.contacts.map(contact => contact.cachedProps),
|
||||||
|
i18n: window.i18n,
|
||||||
|
onCancel: () => {
|
||||||
|
dialog.remove();
|
||||||
|
this.remove();
|
||||||
|
options.reject();
|
||||||
|
},
|
||||||
|
onConfirm: () => {
|
||||||
|
dialog.remove();
|
||||||
|
this.remove();
|
||||||
|
options.resolve();
|
||||||
|
},
|
||||||
|
renderSafetyNumber(props) {
|
||||||
|
return Signal.State.Roots.createSafetyNumberViewer(
|
||||||
|
window.reduxStore,
|
||||||
|
props
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$('.safety-number-change-dialog-wrapper').append(dialog.el);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})();
|
|
@ -118,105 +118,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.key-verification {
|
|
||||||
.container {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
display: block;
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
height: 1.25em;
|
|
||||||
width: 1.25em;
|
|
||||||
vertical-align: text-bottom;
|
|
||||||
display: inline-block;
|
|
||||||
|
|
||||||
&.verified {
|
|
||||||
@include light-theme {
|
|
||||||
@include color-svg('../images/icons/v2/check-24.svg', $color-gray-95);
|
|
||||||
}
|
|
||||||
@include dark-theme {
|
|
||||||
@include color-svg('../images/icons/v2/check-24.svg', $color-gray-02);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.shield {
|
|
||||||
@include light-theme {
|
|
||||||
@include color-svg(
|
|
||||||
'../images/icons/v2/safety-number-outline-24.svg',
|
|
||||||
$color-gray-95
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@include dark-theme {
|
|
||||||
@include color-svg(
|
|
||||||
'../images/icons/v2/safety-number-solid-24.svg',
|
|
||||||
$color-gray-02
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.key,
|
|
||||||
.placeholder {
|
|
||||||
padding: 0 1em;
|
|
||||||
-webkit-user-select: text;
|
|
||||||
}
|
|
||||||
.key {
|
|
||||||
font-family: monospace;
|
|
||||||
padding: 10px;
|
|
||||||
margin: 20px auto 20px auto;
|
|
||||||
width: 16em;
|
|
||||||
border-radius: 5px;
|
|
||||||
|
|
||||||
@include light-theme {
|
|
||||||
background: $color-gray-02;
|
|
||||||
border: solid 1px $color-gray-15;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include dark-theme {
|
|
||||||
color: $color-gray-02;
|
|
||||||
background: $color-gray-90;
|
|
||||||
border: solid 1px $color-gray-45;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.placeholder {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.qr {
|
|
||||||
border-radius: 200px;
|
|
||||||
border: solid 1px $color-gray-15;
|
|
||||||
width: 150px;
|
|
||||||
height: 150px;
|
|
||||||
text-align: center;
|
|
||||||
padding: 25px;
|
|
||||||
margin: 10px auto;
|
|
||||||
canvas {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
img {
|
|
||||||
display: inline-block;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary {
|
|
||||||
margin: 30px 0 10px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.verify {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
button.verify {
|
|
||||||
border-radius: 5px;
|
|
||||||
font-weight: bold;
|
|
||||||
padding: 10px;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.typing-bubble-wrapper {
|
.typing-bubble-wrapper {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6139,6 +6139,279 @@ button.module-image__border-overlay:focus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Module: SafetyNumberChangeDialog
|
||||||
|
|
||||||
|
.module-sfn-dialog__title {
|
||||||
|
@include font-body-1-bold;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
@include dark-theme {
|
||||||
|
color: $color-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-sfn-dialog__message {
|
||||||
|
@include font-body-2;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
color: $color-gray-60;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include dark-theme {
|
||||||
|
color: $color-gray-25;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-sfn-dialog__contacts {
|
||||||
|
list-style-type: none;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-sfn-dialog__contact {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
&--wrapper {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--name {
|
||||||
|
@include font-body-1-bold;
|
||||||
|
|
||||||
|
@include dark-theme {
|
||||||
|
color: $color-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--number {
|
||||||
|
@include light-theme {
|
||||||
|
color: $color-gray-60;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include dark-theme {
|
||||||
|
color: $color-gray-25;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--view {
|
||||||
|
@include font-body-1-bold;
|
||||||
|
background: inherit;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 2px;
|
||||||
|
outline: none;
|
||||||
|
padding: 8px 14px;
|
||||||
|
|
||||||
|
@include keyboard-mode {
|
||||||
|
&:focus {
|
||||||
|
box-shadow: 0px 0px 0px 2px $ultramarine-ui-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
color: $ultramarine-ui-light;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include dark-theme {
|
||||||
|
color: $ultramarine-ui-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-sfn-dialog__actions {
|
||||||
|
border-top: 1px solid $color-gray-05;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-bottom: -18px;
|
||||||
|
margin-left: -16px;
|
||||||
|
margin-right: -16px;
|
||||||
|
margin-top: -14px;
|
||||||
|
padding-left: 16px;
|
||||||
|
padding-right: 16px;
|
||||||
|
padding-top: 16px;
|
||||||
|
|
||||||
|
&--cancel {
|
||||||
|
@include font-body-1-bold;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
outline: none;
|
||||||
|
padding: 7px 14px;
|
||||||
|
|
||||||
|
@include mouse-mode {
|
||||||
|
&:hover {
|
||||||
|
background: $color-gray-15;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include keyboard-mode {
|
||||||
|
&:focus {
|
||||||
|
box-shadow: 0px 0px 0px 2px $ultramarine-ui-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
background-color: $color-gray-05;
|
||||||
|
color: $ultramarine-ui-light;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include dark-theme {
|
||||||
|
background-color: $color-gray-75;
|
||||||
|
color: $ultramarine-ui-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--confirm {
|
||||||
|
@include font-body-1-bold;
|
||||||
|
background: $ultramarine-ui-light;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: $color-white;
|
||||||
|
margin-left: 12px;
|
||||||
|
outline: none;
|
||||||
|
padding: 7px 14px;
|
||||||
|
|
||||||
|
@include mouse-mode {
|
||||||
|
&:hover {
|
||||||
|
background: $ultramarine-brand-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include keyboard-mode {
|
||||||
|
&:focus {
|
||||||
|
box-shadow: 0px 0px 0px 2px $ultramarine-brand-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Safety Number verification */
|
||||||
|
|
||||||
|
.module-safety-number {
|
||||||
|
&__icon {
|
||||||
|
height: 1.25em;
|
||||||
|
width: 1.25em;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__verification-label {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon--verified {
|
||||||
|
display: inline-block;
|
||||||
|
height: 1.25em;
|
||||||
|
margin-right: 4px;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
width: 1.25em;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
-webkit-mask: url('../images/icons/v2/check-24.svg') no-repeat center;
|
||||||
|
-webkit-mask-size: 100%;
|
||||||
|
background-color: #121212;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include dark-theme {
|
||||||
|
-webkit-mask: url('../images/icons/v2/check-24.svg') no-repeat center;
|
||||||
|
-webkit-mask-size: 100%;
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon--shield {
|
||||||
|
display: inline-block;
|
||||||
|
height: 1.25em;
|
||||||
|
margin-right: 4px;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
width: 1.25em;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
-webkit-mask: url('../images/icons/v2/safety-number-outline-24.svg')
|
||||||
|
no-repeat center;
|
||||||
|
-webkit-mask-size: 100%;
|
||||||
|
background-color: #121212;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include dark-theme {
|
||||||
|
-webkit-mask: url('../images/icons/v2/safety-number-solid-24.svg')
|
||||||
|
no-repeat center;
|
||||||
|
-webkit-mask-size: 100%;
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__verify-container {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button--verify {
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
outline: none;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__number {
|
||||||
|
background: #f6f6f6;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: solid 1px #dedede;
|
||||||
|
font-family: monospace;
|
||||||
|
margin: 20px auto 20px auto;
|
||||||
|
padding: 10px;
|
||||||
|
text-align: center;
|
||||||
|
width: 16em;
|
||||||
|
|
||||||
|
@include dark-theme {
|
||||||
|
background: #1b1b1b;
|
||||||
|
border: solid 1px #848484;
|
||||||
|
color: #f6f6f6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__verification-status {
|
||||||
|
margin: 30px 0 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__close-button {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: inherit;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
@include keyboard-mode {
|
||||||
|
&:focus {
|
||||||
|
border: 1px solid $ultramarine-ui-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: inline-block;
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-60);
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Module: StickerPicker
|
// Module: StickerPicker
|
||||||
|
|
||||||
.module-sticker-picker {
|
.module-sticker-picker {
|
||||||
|
@ -7094,7 +7367,7 @@ button.module-image__border-overlay:focus {
|
||||||
}
|
}
|
||||||
|
|
||||||
@include dark-theme() {
|
@include dark-theme() {
|
||||||
background: $color-gray-75;
|
background: $color-gray-80;
|
||||||
color: $color-gray-05;
|
color: $color-gray-05;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -167,33 +167,8 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script type='text/x-tmpl-mustache' id='key-verification'>
|
<script type='text/x-tmpl-mustache' id='key-verification'>
|
||||||
<div class='container'>
|
<div class="container" tabindex="0">
|
||||||
{{ ^hasTheirKey }}
|
<div class="key-verification-wrapper"></div>
|
||||||
<div class='placeholder'>{{ theirKeyUnknown }}</div>
|
|
||||||
{{ /hasTheirKey }}
|
|
||||||
{{ #hasTheirKey }}
|
|
||||||
<label> {{ yourSafetyNumberWith }} </label>
|
|
||||||
<!--<div class='qr'></div>-->
|
|
||||||
<div class='key'>
|
|
||||||
{{ #chunks }} <span>{{ . }}</span> {{ /chunks }}
|
|
||||||
</div>
|
|
||||||
{{ /hasTheirKey }}
|
|
||||||
{{ verifyHelp }}
|
|
||||||
<p> {{> link_to_support }} </p>
|
|
||||||
<div class='summary'>
|
|
||||||
{{ #isVerified }}
|
|
||||||
<span class='icon verified'></span>
|
|
||||||
{{ /isVerified }}
|
|
||||||
{{ ^isVerified }}
|
|
||||||
<span class='icon shield'></span>
|
|
||||||
{{ /isVerified }}
|
|
||||||
{{ verifiedStatus }}
|
|
||||||
</div>
|
|
||||||
<div class='verify'>
|
|
||||||
<button class='verify grey'>
|
|
||||||
{{ verifyButton }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
// tslint:disable: no-backbone-get-set-outside-model
|
|
||||||
import { get, throttle } from 'lodash';
|
import { get, throttle } from 'lodash';
|
||||||
import { WebAPIType } from './textsecure/WebAPI';
|
import { WebAPIType } from './textsecure/WebAPI';
|
||||||
|
|
||||||
|
|
|
@ -70,33 +70,35 @@ export const ConfirmationDialog = React.memo(
|
||||||
<div className="module-confirmation-dialog__container__content">
|
<div className="module-confirmation-dialog__container__content">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
<div className="module-confirmation-dialog__container__buttons">
|
{actions.length > 0 && (
|
||||||
<button
|
<div className="module-confirmation-dialog__container__buttons">
|
||||||
onClick={handleCancel}
|
|
||||||
ref={focusRef}
|
|
||||||
className="module-confirmation-dialog__container__buttons__button"
|
|
||||||
>
|
|
||||||
{i18n('confirmation-dialog--Cancel')}
|
|
||||||
</button>
|
|
||||||
{actions.map((action, i) => (
|
|
||||||
<button
|
<button
|
||||||
key={i}
|
onClick={handleCancel}
|
||||||
onClick={handleAction}
|
ref={focusRef}
|
||||||
data-action={i}
|
className="module-confirmation-dialog__container__buttons__button"
|
||||||
className={classNames(
|
|
||||||
'module-confirmation-dialog__container__buttons__button',
|
|
||||||
action.style === 'affirmative'
|
|
||||||
? 'module-confirmation-dialog__container__buttons__button--affirmative'
|
|
||||||
: null,
|
|
||||||
action.style === 'negative'
|
|
||||||
? 'module-confirmation-dialog__container__buttons__button--negative'
|
|
||||||
: null
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{action.text}
|
{i18n('confirmation-dialog--Cancel')}
|
||||||
</button>
|
</button>
|
||||||
))}
|
{actions.map((action, i) => (
|
||||||
</div>
|
<button
|
||||||
|
key={i}
|
||||||
|
onClick={handleAction}
|
||||||
|
data-action={i}
|
||||||
|
className={classNames(
|
||||||
|
'module-confirmation-dialog__container__buttons__button',
|
||||||
|
action.style === 'affirmative'
|
||||||
|
? 'module-confirmation-dialog__container__buttons__button--affirmative'
|
||||||
|
: null,
|
||||||
|
action.style === 'negative'
|
||||||
|
? 'module-confirmation-dialog__container__buttons__button--negative'
|
||||||
|
: null
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{action.text}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
76
ts/components/SafetyNumberChangeDialog.stories.tsx
Normal file
76
ts/components/SafetyNumberChangeDialog.stories.tsx
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { SafetyNumberChangeDialog } from './SafetyNumberChangeDialog';
|
||||||
|
import { ConversationType } from '../state/ducks/conversations';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||||
|
// @ts-ignore
|
||||||
|
import enMessages from '../../_locales/en/messages.json';
|
||||||
|
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
|
||||||
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
const contact = {
|
||||||
|
avatarPath: undefined,
|
||||||
|
color: 'signal-blue',
|
||||||
|
profileName: undefined,
|
||||||
|
name: 'Rick Sanchez',
|
||||||
|
phoneNumber: '3051234567',
|
||||||
|
} as ConversationType;
|
||||||
|
|
||||||
|
storiesOf('Components/SafetyNumberChangeDialog', module)
|
||||||
|
.add('Single Contact Dialog', () => {
|
||||||
|
return (
|
||||||
|
<SafetyNumberChangeDialog
|
||||||
|
contacts={[contact]}
|
||||||
|
i18n={i18n}
|
||||||
|
onCancel={action('cancel')}
|
||||||
|
onConfirm={action('confirm')}
|
||||||
|
renderSafetyNumber={() => {
|
||||||
|
action('renderSafetyNumber');
|
||||||
|
return <div>This is a mock Safety Number View</div>;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.add('Multi Contact Dialog', () => {
|
||||||
|
return (
|
||||||
|
<SafetyNumberChangeDialog
|
||||||
|
contacts={[contact, contact, contact, contact]}
|
||||||
|
i18n={i18n}
|
||||||
|
onCancel={action('cancel')}
|
||||||
|
onConfirm={action('confirm')}
|
||||||
|
renderSafetyNumber={() => {
|
||||||
|
action('renderSafetyNumber');
|
||||||
|
return <div>This is a mock Safety Number View</div>;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.add('Scroll Dialog', () => {
|
||||||
|
return (
|
||||||
|
<SafetyNumberChangeDialog
|
||||||
|
contacts={[
|
||||||
|
contact,
|
||||||
|
contact,
|
||||||
|
contact,
|
||||||
|
contact,
|
||||||
|
contact,
|
||||||
|
contact,
|
||||||
|
contact,
|
||||||
|
contact,
|
||||||
|
contact,
|
||||||
|
contact,
|
||||||
|
]}
|
||||||
|
i18n={i18n}
|
||||||
|
onCancel={action('cancel')}
|
||||||
|
onConfirm={action('confirm')}
|
||||||
|
renderSafetyNumber={() => {
|
||||||
|
action('renderSafetyNumber');
|
||||||
|
return <div>This is a mock Safety Number View</div>;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
135
ts/components/SafetyNumberChangeDialog.tsx
Normal file
135
ts/components/SafetyNumberChangeDialog.tsx
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Avatar } from './Avatar';
|
||||||
|
import { ConfirmationModal } from './ConfirmationModal';
|
||||||
|
import { ConversationType } from '../state/ducks/conversations';
|
||||||
|
import { LocalizerType } from '../types/Util';
|
||||||
|
|
||||||
|
type SafetyNumberProps = {
|
||||||
|
contactID: string;
|
||||||
|
onClose?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
readonly contacts: Array<ConversationType>;
|
||||||
|
readonly i18n: LocalizerType;
|
||||||
|
readonly onCancel: () => void;
|
||||||
|
readonly onConfirm: () => void;
|
||||||
|
readonly renderSafetyNumber: (props: SafetyNumberProps) => JSX.Element;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SafetyDialogContentProps = Props & {
|
||||||
|
readonly onView: (contact: ConversationType) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SafetyDialogContents = ({
|
||||||
|
contacts,
|
||||||
|
i18n,
|
||||||
|
onCancel,
|
||||||
|
onConfirm,
|
||||||
|
onView,
|
||||||
|
}: SafetyDialogContentProps): JSX.Element => {
|
||||||
|
const cancelButtonRef = React.createRef<HTMLButtonElement>();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (cancelButtonRef && cancelButtonRef.current) {
|
||||||
|
cancelButtonRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [contacts]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1 className="module-sfn-dialog__title">
|
||||||
|
{i18n('safetyNumberChanges')}
|
||||||
|
</h1>
|
||||||
|
<div className="module-sfn-dialog__message">
|
||||||
|
{i18n('changedVerificationWarning')}
|
||||||
|
</div>
|
||||||
|
<ul className="module-sfn-dialog__contacts">
|
||||||
|
{contacts.map((contact: ConversationType) => (
|
||||||
|
<li className="module-sfn-dialog__contact" key={contact.phoneNumber}>
|
||||||
|
<Avatar
|
||||||
|
avatarPath={contact.avatarPath}
|
||||||
|
color={contact.color}
|
||||||
|
conversationType="direct"
|
||||||
|
i18n={i18n}
|
||||||
|
name={contact.name}
|
||||||
|
phoneNumber={contact.phoneNumber}
|
||||||
|
profileName={contact.profileName}
|
||||||
|
size={52}
|
||||||
|
/>
|
||||||
|
<div className="module-sfn-dialog__contact--wrapper">
|
||||||
|
{contact.name && (
|
||||||
|
<>
|
||||||
|
<div className="module-sfn-dialog__contact--name">
|
||||||
|
{contact.name}
|
||||||
|
</div>
|
||||||
|
<div className="module-sfn-dialog__contact--number">
|
||||||
|
{contact.phoneNumber}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!contact.name && (
|
||||||
|
<div className="module-sfn-dialog__contact--name">
|
||||||
|
{contact.phoneNumber}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="module-sfn-dialog__contact--view"
|
||||||
|
onClick={() => {
|
||||||
|
onView(contact);
|
||||||
|
}}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
{i18n('view')}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div className="module-sfn-dialog__actions">
|
||||||
|
<button
|
||||||
|
className="module-sfn-dialog__actions--cancel"
|
||||||
|
onClick={onCancel}
|
||||||
|
ref={cancelButtonRef}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
{i18n('cancel')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="module-sfn-dialog__actions--confirm"
|
||||||
|
onClick={onConfirm}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
{i18n('sendMessageToContact')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SafetyNumberChangeDialog = (props: Props): JSX.Element => {
|
||||||
|
const { i18n, onCancel, renderSafetyNumber } = props;
|
||||||
|
const [contact, setViewSafetyNumber] = React.useState<
|
||||||
|
ConversationType | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
const onClose = contact
|
||||||
|
? () => {
|
||||||
|
setViewSafetyNumber(undefined);
|
||||||
|
}
|
||||||
|
: onCancel;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfirmationModal actions={[]} i18n={i18n} onClose={onClose}>
|
||||||
|
{contact && renderSafetyNumber({ contactID: contact.id, onClose })}
|
||||||
|
{!contact && (
|
||||||
|
<SafetyDialogContents
|
||||||
|
{...props}
|
||||||
|
onView={selectedContact => {
|
||||||
|
setViewSafetyNumber(selectedContact);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ConfirmationModal>
|
||||||
|
);
|
||||||
|
};
|
85
ts/components/SafetyNumberViewer.stories.tsx
Normal file
85
ts/components/SafetyNumberViewer.stories.tsx
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { SafetyNumberViewer } from './SafetyNumberViewer';
|
||||||
|
import { ConversationType } from '../state/ducks/conversations';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||||
|
// @ts-ignore
|
||||||
|
import enMessages from '../../_locales/en/messages.json';
|
||||||
|
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import { boolean, text } from '@storybook/addon-knobs';
|
||||||
|
import { storiesOf } from '@storybook/react';
|
||||||
|
|
||||||
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
contact: {
|
||||||
|
title: 'Summer Smith',
|
||||||
|
isVerified: true,
|
||||||
|
} as ConversationType,
|
||||||
|
generateSafetyNumber: action('generate-safety-number'),
|
||||||
|
i18n,
|
||||||
|
safetyNumber: 'XXX',
|
||||||
|
safetyNumberChanged: false,
|
||||||
|
toggleVerified: action('toggle-verified'),
|
||||||
|
verificationDisabled: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const permutations = [
|
||||||
|
{
|
||||||
|
title: 'Safety Number',
|
||||||
|
props: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Safety Number (not verified)',
|
||||||
|
props: {
|
||||||
|
contact: {
|
||||||
|
title: 'Morty Smith',
|
||||||
|
isVerified: false,
|
||||||
|
} as ConversationType,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Verification Disabled',
|
||||||
|
props: {
|
||||||
|
verificationDisabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Safety Number Changed',
|
||||||
|
props: {
|
||||||
|
safetyNumberChanged: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Safety Number (dialog close)',
|
||||||
|
props: {
|
||||||
|
onClose: action('close'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
storiesOf('Components/SafetyNumberViewer', module)
|
||||||
|
.add('Knobs Playground', () => {
|
||||||
|
const safetyNumber = text('safetyNumber', 'XXX');
|
||||||
|
const safetyNumberChanged = boolean('safetyNumberChanged', false);
|
||||||
|
const verificationDisabled = boolean('verificationDisabled', false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafetyNumberViewer
|
||||||
|
{...defaultProps}
|
||||||
|
safetyNumber={safetyNumber}
|
||||||
|
safetyNumberChanged={safetyNumberChanged}
|
||||||
|
verificationDisabled={verificationDisabled}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.add('Iterations', () => {
|
||||||
|
return permutations.map(({ props, title }) => (
|
||||||
|
<>
|
||||||
|
<h3>{title}</h3>
|
||||||
|
<SafetyNumberViewer {...defaultProps} {...props} />
|
||||||
|
</>
|
||||||
|
));
|
||||||
|
});
|
82
ts/components/SafetyNumberViewer.tsx
Normal file
82
ts/components/SafetyNumberViewer.tsx
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { ConversationType } from '../state/ducks/conversations';
|
||||||
|
import { LocalizerType } from '../types/Util';
|
||||||
|
import { getPlaceholder } from '../util/safetyNumber';
|
||||||
|
|
||||||
|
type SafetyNumberViewerProps = {
|
||||||
|
contact?: ConversationType;
|
||||||
|
generateSafetyNumber: (contact: ConversationType) => void;
|
||||||
|
i18n: LocalizerType;
|
||||||
|
onClose?: () => void;
|
||||||
|
safetyNumber: string;
|
||||||
|
safetyNumberChanged?: boolean;
|
||||||
|
toggleVerified: (contact: ConversationType) => void;
|
||||||
|
verificationDisabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SafetyNumberViewer = ({
|
||||||
|
contact,
|
||||||
|
generateSafetyNumber,
|
||||||
|
i18n,
|
||||||
|
onClose,
|
||||||
|
safetyNumber,
|
||||||
|
safetyNumberChanged,
|
||||||
|
toggleVerified,
|
||||||
|
verificationDisabled,
|
||||||
|
}: SafetyNumberViewerProps): JSX.Element | null => {
|
||||||
|
if (!contact) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
generateSafetyNumber(contact);
|
||||||
|
}, [safetyNumber]);
|
||||||
|
|
||||||
|
const name = contact.title;
|
||||||
|
const isVerified = contact.isVerified;
|
||||||
|
const verifiedStatus = isVerified
|
||||||
|
? i18n('isVerified', [name])
|
||||||
|
: i18n('isNotVerified', [name]);
|
||||||
|
const verifyButtonText = isVerified ? i18n('unverify') : i18n('verify');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="module-safety-number">
|
||||||
|
{onClose && (
|
||||||
|
<div className="module-safety-number__close-button">
|
||||||
|
<button onClick={onClose} tabIndex={0}>
|
||||||
|
<span />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="module-safety-number__verification-label">
|
||||||
|
{safetyNumberChanged
|
||||||
|
? i18n('changedRightAfterVerify', [name, name])
|
||||||
|
: i18n('yourSafetyNumberWith', [name])}
|
||||||
|
</div>
|
||||||
|
<div className="module-safety-number__number">
|
||||||
|
{safetyNumber || getPlaceholder()}
|
||||||
|
</div>
|
||||||
|
{i18n('verifyHelp', [name])}
|
||||||
|
<div className="module-safety-number__verification-status">
|
||||||
|
{isVerified ? (
|
||||||
|
<span className="module-safety-number__icon--verified" />
|
||||||
|
) : (
|
||||||
|
<span className="module-safety-number__icon--shield" />
|
||||||
|
)}
|
||||||
|
{verifiedStatus}
|
||||||
|
</div>
|
||||||
|
<div className="module-safety-number__verify-container">
|
||||||
|
<button
|
||||||
|
className="module-safety-number__button--verify"
|
||||||
|
disabled={verificationDisabled}
|
||||||
|
onClick={() => {
|
||||||
|
toggleVerified(contact);
|
||||||
|
}}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
{verifyButtonText}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -37,7 +37,6 @@ const SENT = 'sent' as 'sent';
|
||||||
const START_NEW_CONVERSATION = 'start-new-conversation' as 'start-new-conversation';
|
const START_NEW_CONVERSATION = 'start-new-conversation' as 'start-new-conversation';
|
||||||
const SMS_MMS_NOT_SUPPORTED = 'sms-mms-not-supported-text' as 'sms-mms-not-supported-text';
|
const SMS_MMS_NOT_SUPPORTED = 'sms-mms-not-supported-text' as 'sms-mms-not-supported-text';
|
||||||
|
|
||||||
// tslint:disable-next-line no-backbone-get-set-outside-model
|
|
||||||
messageLookup.set('1-guid-guid-guid-guid-guid', {
|
messageLookup.set('1-guid-guid-guid-guid-guid', {
|
||||||
id: '1-guid-guid-guid-guid-guid',
|
id: '1-guid-guid-guid-guid-guid',
|
||||||
conversationId: '(202) 555-0015',
|
conversationId: '(202) 555-0015',
|
||||||
|
@ -56,7 +55,6 @@ messageLookup.set('1-guid-guid-guid-guid-guid', {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// tslint:disable-next-line no-backbone-get-set-outside-model
|
|
||||||
messageLookup.set('2-guid-guid-guid-guid-guid', {
|
messageLookup.set('2-guid-guid-guid-guid-guid', {
|
||||||
id: '2-guid-guid-guid-guid-guid',
|
id: '2-guid-guid-guid-guid-guid',
|
||||||
conversationId: '(202) 555-0016',
|
conversationId: '(202) 555-0016',
|
||||||
|
@ -73,7 +71,6 @@ messageLookup.set('2-guid-guid-guid-guid-guid', {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// tslint:disable-next-line no-backbone-get-set-outside-model
|
|
||||||
messageLookup.set('3-guid-guid-guid-guid-guid', {
|
messageLookup.set('3-guid-guid-guid-guid-guid', {
|
||||||
id: '3-guid-guid-guid-guid-guid',
|
id: '3-guid-guid-guid-guid-guid',
|
||||||
conversationId: 'EveryoneGroupID',
|
conversationId: 'EveryoneGroupID',
|
||||||
|
@ -91,7 +88,6 @@ messageLookup.set('3-guid-guid-guid-guid-guid', {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// tslint:disable-next-line no-backbone-get-set-outside-model
|
|
||||||
messageLookup.set('4-guid-guid-guid-guid-guid', {
|
messageLookup.set('4-guid-guid-guid-guid-guid', {
|
||||||
id: '4-guid-guid-guid-guid-guid',
|
id: '4-guid-guid-guid-guid-guid',
|
||||||
conversationId: 'EveryoneGroupID',
|
conversationId: 'EveryoneGroupID',
|
||||||
|
|
13
ts/shims/contactVerification.ts
Normal file
13
ts/shims/contactVerification.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
export async function toggleVerification(id: string): Promise<void> {
|
||||||
|
const contact = window.getConversations().get(id);
|
||||||
|
if (contact) {
|
||||||
|
await contact.toggleVerified();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function reloadProfiles(id: string): Promise<void> {
|
||||||
|
const contact = window.getConversations().get(id);
|
||||||
|
if (contact) {
|
||||||
|
await contact.getProfiles();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
// tslint:disable no-backbone-get-set-outside-model no-console no-default-export no-unnecessary-local-variable
|
// tslint:disable no-console no-default-export no-unnecessary-local-variable
|
||||||
|
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import mkdirp from 'mkdirp';
|
import mkdirp from 'mkdirp';
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { actions as emojis } from './ducks/emojis';
|
||||||
import { actions as expiration } from './ducks/expiration';
|
import { actions as expiration } from './ducks/expiration';
|
||||||
import { actions as items } from './ducks/items';
|
import { actions as items } from './ducks/items';
|
||||||
import { actions as network } from './ducks/network';
|
import { actions as network } from './ducks/network';
|
||||||
|
import { actions as safetyNumber } from './ducks/safetyNumber';
|
||||||
import { actions as search } from './ducks/search';
|
import { actions as search } from './ducks/search';
|
||||||
import { actions as stickers } from './ducks/stickers';
|
import { actions as stickers } from './ducks/stickers';
|
||||||
import { actions as updates } from './ducks/updates';
|
import { actions as updates } from './ducks/updates';
|
||||||
|
@ -16,6 +17,7 @@ export const mapDispatchToProps = {
|
||||||
...expiration,
|
...expiration,
|
||||||
...items,
|
...items,
|
||||||
...network,
|
...network,
|
||||||
|
...safetyNumber,
|
||||||
...search,
|
...search,
|
||||||
...stickers,
|
...stickers,
|
||||||
...updates,
|
...updates,
|
||||||
|
|
|
@ -24,12 +24,15 @@ export type DBConversationType = {
|
||||||
};
|
};
|
||||||
export type ConversationType = {
|
export type ConversationType = {
|
||||||
id: string;
|
id: string;
|
||||||
|
uuid?: string;
|
||||||
|
e164: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
profileName?: string;
|
profileName?: string;
|
||||||
avatarPath?: string;
|
avatarPath?: string;
|
||||||
color?: ColorType;
|
color?: ColorType;
|
||||||
isArchived?: boolean;
|
isArchived?: boolean;
|
||||||
isBlocked?: boolean;
|
isBlocked?: boolean;
|
||||||
|
isVerified?: boolean;
|
||||||
activeAt?: number;
|
activeAt?: number;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
inboxPosition: number;
|
inboxPosition: number;
|
||||||
|
@ -42,6 +45,7 @@ export type ConversationType = {
|
||||||
type: 'direct' | 'group';
|
type: 'direct' | 'group';
|
||||||
isMe: boolean;
|
isMe: boolean;
|
||||||
lastUpdated: number;
|
lastUpdated: number;
|
||||||
|
title: string;
|
||||||
unreadCount: number;
|
unreadCount: number;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
typingContact?: {
|
typingContact?: {
|
||||||
|
|
214
ts/state/ducks/safetyNumber.ts
Normal file
214
ts/state/ducks/safetyNumber.ts
Normal file
|
@ -0,0 +1,214 @@
|
||||||
|
import { generateSecurityNumberBlock } from '../../util/safetyNumber';
|
||||||
|
import { ConversationType } from './conversations';
|
||||||
|
import {
|
||||||
|
reloadProfiles,
|
||||||
|
toggleVerification,
|
||||||
|
} from '../../shims/contactVerification';
|
||||||
|
|
||||||
|
export type SafetyNumberContactType = {
|
||||||
|
safetyNumber: string;
|
||||||
|
safetyNumberChanged?: boolean;
|
||||||
|
verificationDisabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SafetyNumberStateType = {
|
||||||
|
contacts: {
|
||||||
|
[key: string]: SafetyNumberContactType;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const GENERATE = 'safetyNumber/GENERATE';
|
||||||
|
const GENERATE_FULFILLED = 'safetyNumber/GENERATE_FULFILLED';
|
||||||
|
const TOGGLE_VERIFIED = 'safetyNumber/TOGGLE_VERIFIED';
|
||||||
|
const TOGGLE_VERIFIED_FULFILLED = 'safetyNumber/TOGGLE_VERIFIED_FULFILLED';
|
||||||
|
const TOGGLE_VERIFIED_PENDING = 'safetyNumber/TOGGLE_VERIFIED_PENDING';
|
||||||
|
|
||||||
|
type GenerateAsyncActionType = {
|
||||||
|
contact: ConversationType;
|
||||||
|
safetyNumber: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GenerateActionType = {
|
||||||
|
type: 'safetyNumber/GENERATE';
|
||||||
|
payload: Promise<GenerateAsyncActionType>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GenerateFulfilledActionType = {
|
||||||
|
type: 'safetyNumber/GENERATE_FULFILLED';
|
||||||
|
payload: GenerateAsyncActionType;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ToggleVerifiedAsyncActionType = {
|
||||||
|
contact: ConversationType;
|
||||||
|
safetyNumber?: string;
|
||||||
|
safetyNumberChanged?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ToggleVerifiedActionType = {
|
||||||
|
type: 'safetyNumber/TOGGLE_VERIFIED';
|
||||||
|
payload: {
|
||||||
|
data: { contact: ConversationType };
|
||||||
|
promise: Promise<ToggleVerifiedAsyncActionType>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type ToggleVerifiedPendingActionType = {
|
||||||
|
type: 'safetyNumber/TOGGLE_VERIFIED_PENDING';
|
||||||
|
payload: ToggleVerifiedAsyncActionType;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ToggleVerifiedFulfilledActionType = {
|
||||||
|
type: 'safetyNumber/TOGGLE_VERIFIED_FULFILLED';
|
||||||
|
payload: ToggleVerifiedAsyncActionType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SafetyNumberActionTypes =
|
||||||
|
| GenerateActionType
|
||||||
|
| GenerateFulfilledActionType
|
||||||
|
| ToggleVerifiedActionType
|
||||||
|
| ToggleVerifiedPendingActionType
|
||||||
|
| ToggleVerifiedFulfilledActionType;
|
||||||
|
|
||||||
|
function generate(contact: ConversationType): GenerateActionType {
|
||||||
|
return {
|
||||||
|
type: GENERATE,
|
||||||
|
payload: doGenerate(contact),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doGenerate(
|
||||||
|
contact: ConversationType
|
||||||
|
): Promise<GenerateAsyncActionType> {
|
||||||
|
const securityNumberBlock = await generateSecurityNumberBlock(contact);
|
||||||
|
return {
|
||||||
|
contact,
|
||||||
|
safetyNumber: securityNumberBlock.join(' '),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleVerified(contact: ConversationType): ToggleVerifiedActionType {
|
||||||
|
return {
|
||||||
|
type: TOGGLE_VERIFIED,
|
||||||
|
payload: {
|
||||||
|
data: { contact },
|
||||||
|
promise: doToggleVerified(contact),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function alterVerification(contact: ConversationType): Promise<void> {
|
||||||
|
try {
|
||||||
|
await toggleVerification(contact.id);
|
||||||
|
} catch (result) {
|
||||||
|
if (result instanceof Error) {
|
||||||
|
if (result.name === 'OutgoingIdentityKeyError') {
|
||||||
|
throw result;
|
||||||
|
} else {
|
||||||
|
window.log.error(
|
||||||
|
'failed to toggle verified:',
|
||||||
|
result && result.stack ? result.stack : result
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const keyError = result.errors.find(
|
||||||
|
(error: Error) => error.name === 'OutgoingIdentityKeyError'
|
||||||
|
);
|
||||||
|
if (keyError) {
|
||||||
|
throw keyError;
|
||||||
|
} else {
|
||||||
|
result.errors.forEach((error: Error) => {
|
||||||
|
window.log.error(
|
||||||
|
'failed to toggle verified:',
|
||||||
|
error && error.stack ? error.stack : error
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doToggleVerified(
|
||||||
|
contact: ConversationType
|
||||||
|
): Promise<ToggleVerifiedAsyncActionType> {
|
||||||
|
try {
|
||||||
|
await alterVerification(contact);
|
||||||
|
} catch (err) {
|
||||||
|
if (err.name === 'OutgoingIdentityKeyError') {
|
||||||
|
await reloadProfiles(contact.id);
|
||||||
|
const securityNumberBlock = await generateSecurityNumberBlock(contact);
|
||||||
|
|
||||||
|
return {
|
||||||
|
contact,
|
||||||
|
safetyNumber: securityNumberBlock.join(' '),
|
||||||
|
safetyNumberChanged: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { contact };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
generateSafetyNumber: generate,
|
||||||
|
toggleVerified,
|
||||||
|
};
|
||||||
|
|
||||||
|
function getEmptyState(): SafetyNumberStateType {
|
||||||
|
return {
|
||||||
|
contacts: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reducer(
|
||||||
|
state: SafetyNumberStateType = getEmptyState(),
|
||||||
|
action: SafetyNumberActionTypes
|
||||||
|
): SafetyNumberStateType {
|
||||||
|
if (action.type === TOGGLE_VERIFIED_PENDING) {
|
||||||
|
const { contact } = action.payload;
|
||||||
|
const { id } = contact;
|
||||||
|
const record = state.contacts[id];
|
||||||
|
return {
|
||||||
|
contacts: {
|
||||||
|
...state.contacts,
|
||||||
|
[id]: {
|
||||||
|
...record,
|
||||||
|
safetyNumberChanged: false,
|
||||||
|
verificationDisabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.type === TOGGLE_VERIFIED_FULFILLED) {
|
||||||
|
const { contact, ...restProps } = action.payload;
|
||||||
|
const { id } = contact;
|
||||||
|
const record = state.contacts[id];
|
||||||
|
return {
|
||||||
|
contacts: {
|
||||||
|
...state.contacts,
|
||||||
|
[id]: {
|
||||||
|
...record,
|
||||||
|
...restProps,
|
||||||
|
verificationDisabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.type === GENERATE_FULFILLED) {
|
||||||
|
const { contact, safetyNumber } = action.payload;
|
||||||
|
const { id } = contact;
|
||||||
|
const record = state.contacts[id];
|
||||||
|
return {
|
||||||
|
contacts: {
|
||||||
|
...state.contacts,
|
||||||
|
[id]: {
|
||||||
|
...record,
|
||||||
|
safetyNumber,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
|
@ -30,6 +30,11 @@ import {
|
||||||
NetworkStateType,
|
NetworkStateType,
|
||||||
reducer as network,
|
reducer as network,
|
||||||
} from './ducks/network';
|
} from './ducks/network';
|
||||||
|
import {
|
||||||
|
reducer as safetyNumber,
|
||||||
|
SafetyNumberActionTypes,
|
||||||
|
SafetyNumberStateType,
|
||||||
|
} from './ducks/safetyNumber';
|
||||||
import {
|
import {
|
||||||
reducer as search,
|
reducer as search,
|
||||||
SEARCH_TYPES as SearchActionType,
|
SEARCH_TYPES as SearchActionType,
|
||||||
|
@ -54,6 +59,7 @@ export type StateType = {
|
||||||
expiration: ExpirationStateType;
|
expiration: ExpirationStateType;
|
||||||
items: ItemsStateType;
|
items: ItemsStateType;
|
||||||
network: NetworkStateType;
|
network: NetworkStateType;
|
||||||
|
safetyNumber: SafetyNumberStateType;
|
||||||
search: SearchStateType;
|
search: SearchStateType;
|
||||||
stickers: StickersStateType;
|
stickers: StickersStateType;
|
||||||
updates: UpdatesStateType;
|
updates: UpdatesStateType;
|
||||||
|
@ -67,6 +73,7 @@ export type ActionsType =
|
||||||
| ConversationActionType
|
| ConversationActionType
|
||||||
| ItemsActionType
|
| ItemsActionType
|
||||||
| NetworkActionType
|
| NetworkActionType
|
||||||
|
| SafetyNumberActionTypes
|
||||||
| StickersActionType
|
| StickersActionType
|
||||||
| SearchActionType
|
| SearchActionType
|
||||||
| UpdatesActionType;
|
| UpdatesActionType;
|
||||||
|
@ -78,6 +85,7 @@ export const reducers = {
|
||||||
expiration,
|
expiration,
|
||||||
items,
|
items,
|
||||||
network,
|
network,
|
||||||
|
safetyNumber,
|
||||||
search,
|
search,
|
||||||
stickers,
|
stickers,
|
||||||
updates,
|
updates,
|
||||||
|
|
21
ts/state/roots/createSafetyNumberViewer.tsx
Normal file
21
ts/state/roots/createSafetyNumberViewer.tsx
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
|
import { Store } from 'redux';
|
||||||
|
|
||||||
|
import { SmartSafetyNumberViewer } from '../smart/SafetyNumberViewer';
|
||||||
|
|
||||||
|
// Workaround: A react component's required properties are filtering up through connect()
|
||||||
|
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
|
||||||
|
const FilteredSafetyNumberViewer = SmartSafetyNumberViewer as any;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
contactID: string;
|
||||||
|
onClose?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createSafetyNumberViewer = (store: Store, props: Props) => (
|
||||||
|
<Provider store={store}>
|
||||||
|
<FilteredSafetyNumberViewer {...props} />
|
||||||
|
</Provider>
|
||||||
|
);
|
24
ts/state/selectors/safetyNumber.ts
Normal file
24
ts/state/selectors/safetyNumber.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
import { StateType } from '../reducer';
|
||||||
|
import {
|
||||||
|
SafetyNumberContactType,
|
||||||
|
SafetyNumberStateType,
|
||||||
|
} from '../ducks/safetyNumber';
|
||||||
|
|
||||||
|
const getSafetyNumber = (state: StateType): SafetyNumberStateType =>
|
||||||
|
state.safetyNumber;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
contactID: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getContactID = (_: StateType, props: Props): string => props.contactID;
|
||||||
|
|
||||||
|
export const getContactSafetyNumber = createSelector(
|
||||||
|
[getSafetyNumber, getContactID],
|
||||||
|
(
|
||||||
|
{ contacts }: SafetyNumberStateType,
|
||||||
|
contactID: string
|
||||||
|
): SafetyNumberContactType => contacts[contactID]
|
||||||
|
);
|
25
ts/state/smart/SafetyNumberViewer.tsx
Normal file
25
ts/state/smart/SafetyNumberViewer.tsx
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { mapDispatchToProps } from '../actions';
|
||||||
|
import { SafetyNumberViewer } from '../../components/SafetyNumberViewer';
|
||||||
|
import { StateType } from '../reducer';
|
||||||
|
import { getContactSafetyNumber } from '../selectors/safetyNumber';
|
||||||
|
import { getConversationSelector } from '../selectors/conversations';
|
||||||
|
import { getIntl } from '../selectors/user';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
contactID: string;
|
||||||
|
onClose?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = (state: StateType, props: Props) => {
|
||||||
|
return {
|
||||||
|
...props,
|
||||||
|
...getContactSafetyNumber(state, props),
|
||||||
|
contact: getConversationSelector(state)(props.contactID),
|
||||||
|
i18n: getIntl(state),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const smart = connect(mapStateToProps, mapDispatchToProps);
|
||||||
|
|
||||||
|
export const SmartSafetyNumberViewer = smart(SafetyNumberViewer);
|
|
@ -14,6 +14,7 @@ describe('state/selectors/conversations', () => {
|
||||||
const data: ConversationLookupType = {
|
const data: ConversationLookupType = {
|
||||||
id1: {
|
id1: {
|
||||||
id: 'id1',
|
id: 'id1',
|
||||||
|
e164: '+18005551111',
|
||||||
activeAt: Date.now(),
|
activeAt: Date.now(),
|
||||||
name: 'No timestamp',
|
name: 'No timestamp',
|
||||||
timestamp: 0,
|
timestamp: 0,
|
||||||
|
@ -24,6 +25,7 @@ describe('state/selectors/conversations', () => {
|
||||||
type: 'direct',
|
type: 'direct',
|
||||||
isMe: false,
|
isMe: false,
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
|
title: 'No timestamp',
|
||||||
unreadCount: 1,
|
unreadCount: 1,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
typingContact: {
|
typingContact: {
|
||||||
|
@ -36,6 +38,7 @@ describe('state/selectors/conversations', () => {
|
||||||
},
|
},
|
||||||
id2: {
|
id2: {
|
||||||
id: 'id2',
|
id: 'id2',
|
||||||
|
e164: '+18005551111',
|
||||||
activeAt: Date.now(),
|
activeAt: Date.now(),
|
||||||
name: 'B',
|
name: 'B',
|
||||||
timestamp: 20,
|
timestamp: 20,
|
||||||
|
@ -46,6 +49,7 @@ describe('state/selectors/conversations', () => {
|
||||||
type: 'direct',
|
type: 'direct',
|
||||||
isMe: false,
|
isMe: false,
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
|
title: 'B',
|
||||||
unreadCount: 1,
|
unreadCount: 1,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
typingContact: {
|
typingContact: {
|
||||||
|
@ -58,6 +62,7 @@ describe('state/selectors/conversations', () => {
|
||||||
},
|
},
|
||||||
id3: {
|
id3: {
|
||||||
id: 'id3',
|
id: 'id3',
|
||||||
|
e164: '+18005551111',
|
||||||
activeAt: Date.now(),
|
activeAt: Date.now(),
|
||||||
name: 'C',
|
name: 'C',
|
||||||
timestamp: 20,
|
timestamp: 20,
|
||||||
|
@ -68,6 +73,7 @@ describe('state/selectors/conversations', () => {
|
||||||
type: 'direct',
|
type: 'direct',
|
||||||
isMe: false,
|
isMe: false,
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
|
title: 'C',
|
||||||
unreadCount: 1,
|
unreadCount: 1,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
typingContact: {
|
typingContact: {
|
||||||
|
@ -80,6 +86,7 @@ describe('state/selectors/conversations', () => {
|
||||||
},
|
},
|
||||||
id4: {
|
id4: {
|
||||||
id: 'id4',
|
id: 'id4',
|
||||||
|
e164: '+18005551111',
|
||||||
activeAt: Date.now(),
|
activeAt: Date.now(),
|
||||||
name: 'Á',
|
name: 'Á',
|
||||||
timestamp: 20,
|
timestamp: 20,
|
||||||
|
@ -90,6 +97,7 @@ describe('state/selectors/conversations', () => {
|
||||||
type: 'direct',
|
type: 'direct',
|
||||||
isMe: false,
|
isMe: false,
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
|
title: 'A',
|
||||||
unreadCount: 1,
|
unreadCount: 1,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
typingContact: {
|
typingContact: {
|
||||||
|
@ -102,6 +110,7 @@ describe('state/selectors/conversations', () => {
|
||||||
},
|
},
|
||||||
id5: {
|
id5: {
|
||||||
id: 'id5',
|
id: 'id5',
|
||||||
|
e164: '+18005551111',
|
||||||
activeAt: Date.now(),
|
activeAt: Date.now(),
|
||||||
name: 'First!',
|
name: 'First!',
|
||||||
timestamp: 30,
|
timestamp: 30,
|
||||||
|
@ -112,6 +121,7 @@ describe('state/selectors/conversations', () => {
|
||||||
type: 'direct',
|
type: 'direct',
|
||||||
isMe: false,
|
isMe: false,
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
|
title: 'First!',
|
||||||
unreadCount: 1,
|
unreadCount: 1,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
typingContact: {
|
typingContact: {
|
||||||
|
|
17
ts/textsecure.d.ts
vendored
17
ts/textsecure.d.ts
vendored
|
@ -96,6 +96,14 @@ type StoredSignedPreKeyType = SignedPreKeyType & {
|
||||||
created_at: number;
|
created_at: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type IdentityKeyRecord = {
|
||||||
|
publicKey: ArrayBuffer;
|
||||||
|
firstUse: boolean;
|
||||||
|
timestamp: number;
|
||||||
|
verified: number;
|
||||||
|
nonblockingApproval: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type StorageProtocolType = StorageType & {
|
export type StorageProtocolType = StorageType & {
|
||||||
VerifiedStatus: {
|
VerifiedStatus: {
|
||||||
DEFAULT: number;
|
DEFAULT: number;
|
||||||
|
@ -105,6 +113,7 @@ export type StorageProtocolType = StorageType & {
|
||||||
archiveSiblingSessions: (identifier: string) => Promise<void>;
|
archiveSiblingSessions: (identifier: string) => Promise<void>;
|
||||||
removeSession: (identifier: string) => Promise<void>;
|
removeSession: (identifier: string) => Promise<void>;
|
||||||
getDeviceIds: (identifier: string) => Promise<Array<number>>;
|
getDeviceIds: (identifier: string) => Promise<Array<number>>;
|
||||||
|
getIdentityRecord: (identifier: string) => IdentityKeyRecord | undefined;
|
||||||
hydrateCaches: () => Promise<void>;
|
hydrateCaches: () => Promise<void>;
|
||||||
clearPreKeyStore: () => Promise<void>;
|
clearPreKeyStore: () => Promise<void>;
|
||||||
clearSignedPreKeysStore: () => Promise<void>;
|
clearSignedPreKeysStore: () => Promise<void>;
|
||||||
|
@ -119,13 +128,7 @@ export type StorageProtocolType = StorageType & {
|
||||||
loadSignedPreKeys: () => Promise<Array<StoredSignedPreKeyType>>;
|
loadSignedPreKeys: () => Promise<Array<StoredSignedPreKeyType>>;
|
||||||
saveIdentityWithAttributes: (
|
saveIdentityWithAttributes: (
|
||||||
number: string,
|
number: string,
|
||||||
options: {
|
options: IdentityKeyRecord
|
||||||
publicKey: ArrayBuffer;
|
|
||||||
firstUse: boolean;
|
|
||||||
timestamp: number;
|
|
||||||
verified: number;
|
|
||||||
nonblockingApproval: boolean;
|
|
||||||
}
|
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
removeSignedPreKey: (keyId: number) => Promise<void>;
|
removeSignedPreKey: (keyId: number) => Promise<void>;
|
||||||
removeAllData: () => Promise<void>;
|
removeAllData: () => Promise<void>;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// tslint:disable no-backbone-get-set-outside-model no-default-export no-unnecessary-local-variable
|
// tslint:disable no-default-export no-unnecessary-local-variable
|
||||||
|
|
||||||
import EventTarget from './EventTarget';
|
import EventTarget from './EventTarget';
|
||||||
import { WebAPIType } from './WebAPI';
|
import { WebAPIType } from './WebAPI';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// tslint:disable no-backbone-get-set-outside-model no-bitwise no-default-export
|
// tslint:disable no-bitwise no-default-export
|
||||||
|
|
||||||
import { without } from 'lodash';
|
import { without } from 'lodash';
|
||||||
import PQueue from 'p-queue';
|
import PQueue from 'p-queue';
|
||||||
|
|
|
@ -189,7 +189,6 @@ const agents: AgentCacheType = {};
|
||||||
|
|
||||||
function getContentType(response: Response) {
|
function getContentType(response: Response) {
|
||||||
if (response.headers && response.headers.get) {
|
if (response.headers && response.headers.get) {
|
||||||
// tslint:disable-next-line no-backbone-get-set-outside-model
|
|
||||||
return response.headers.get('content-type');
|
return response.headers.get('content-type');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -310,7 +309,6 @@ async function _promiseAjax(
|
||||||
let resultPromise;
|
let resultPromise;
|
||||||
if (
|
if (
|
||||||
options.responseType === 'json' &&
|
options.responseType === 'json' &&
|
||||||
// tslint:disable-next-line no-backbone-get-set-outside-model
|
|
||||||
response.headers.get('Content-Type') === 'application/json'
|
response.headers.get('Content-Type') === 'application/json'
|
||||||
) {
|
) {
|
||||||
resultPromise = response.json();
|
resultPromise = response.json();
|
||||||
|
@ -1464,7 +1462,6 @@ export function initialize({
|
||||||
throw new Error('makeProxiedRequest: Problem retrieving header value');
|
throw new Error('makeProxiedRequest: Problem retrieving header value');
|
||||||
}
|
}
|
||||||
|
|
||||||
// tslint:disable-next-line no-backbone-get-set-outside-model
|
|
||||||
const range = response.headers.get('content-range');
|
const range = response.headers.get('content-range');
|
||||||
const match = PARSE_RANGE_HEADER.exec(range);
|
const match = PARSE_RANGE_HEADER.exec(range);
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,10 @@ import { createBatcher } from './batcher';
|
||||||
import { createWaitBatcher } from './waitBatcher';
|
import { createWaitBatcher } from './waitBatcher';
|
||||||
import { deleteForEveryone } from './deleteForEveryone';
|
import { deleteForEveryone } from './deleteForEveryone';
|
||||||
import { downloadAttachment } from './downloadAttachment';
|
import { downloadAttachment } from './downloadAttachment';
|
||||||
|
import {
|
||||||
|
generateSecurityNumber,
|
||||||
|
getPlaceholder as getSafetyNumberPlaceholder,
|
||||||
|
} from './safetyNumber';
|
||||||
import { hasExpired } from './hasExpired';
|
import { hasExpired } from './hasExpired';
|
||||||
import { isFileDangerous } from './isFileDangerous';
|
import { isFileDangerous } from './isFileDangerous';
|
||||||
import { makeLookup } from './makeLookup';
|
import { makeLookup } from './makeLookup';
|
||||||
|
@ -20,6 +24,8 @@ export {
|
||||||
createWaitBatcher,
|
createWaitBatcher,
|
||||||
deleteForEveryone,
|
deleteForEveryone,
|
||||||
downloadAttachment,
|
downloadAttachment,
|
||||||
|
generateSecurityNumber,
|
||||||
|
getSafetyNumberPlaceholder,
|
||||||
GoogleChrome,
|
GoogleChrome,
|
||||||
hasExpired,
|
hasExpired,
|
||||||
isFileDangerous,
|
isFileDangerous,
|
||||||
|
|
|
@ -223,7 +223,7 @@
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "js/models/conversations.js",
|
"path": "js/models/conversations.js",
|
||||||
"line": " await wrap(",
|
"line": " await wrap(",
|
||||||
"lineNumber": 641,
|
"lineNumber": 644,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2020-06-09T20:26:46.515Z"
|
"updated": "2020-06-09T20:26:46.515Z"
|
||||||
},
|
},
|
||||||
|
@ -710,46 +710,18 @@
|
||||||
{
|
{
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/key_verification_view.js",
|
"path": "js/views/key_verification_view.js",
|
||||||
"line": " new QRCode(this.$('.qr')[0]).makeCode(",
|
"line": " this.$('.key-verification-wrapper').append(view.el);",
|
||||||
"lineNumber": 43,
|
"lineNumber": 23,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-02-14T20:02:37.507Z",
|
"updated": "2020-06-23T06:48:06.829Z"
|
||||||
"reasonDetail": "Hardcoded selector"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-append(",
|
||||||
"path": "js/views/key_verification_view.js",
|
"path": "js/views/key_verification_view.js",
|
||||||
"line": " dcodeIO.ByteBuffer.wrap(this.ourKey).toString('base64')",
|
"line": " this.$('.key-verification-wrapper').append(view.el);",
|
||||||
"lineNumber": 44,
|
"lineNumber": 23,
|
||||||
"reasonCategory": "falseMatch",
|
|
||||||
"updated": "2020-02-14T20:02:37.507Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-insertBefore(",
|
|
||||||
"path": "js/views/key_verification_view.js",
|
|
||||||
"line": " dialog.$el.insertBefore(this.el);",
|
|
||||||
"lineNumber": 86,
|
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-02-14T20:02:37.507Z",
|
"updated": "2020-06-23T06:48:06.829Z"
|
||||||
"reasonDetail": "Known DOM elements"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-$(",
|
|
||||||
"path": "js/views/key_verification_view.js",
|
|
||||||
"line": " this.$('button.verify').attr('disabled', true);",
|
|
||||||
"lineNumber": 90,
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2020-02-14T20:02:37.507Z",
|
|
||||||
"reasonDetail": "Hardcoded selector"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "jQuery-$(",
|
|
||||||
"path": "js/views/key_verification_view.js",
|
|
||||||
"line": " this.$('button.verify').removeAttr('disabled');",
|
|
||||||
"lineNumber": 121,
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2020-02-14T20:02:37.507Z",
|
|
||||||
"reasonDetail": "Hardcoded selector"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
|
@ -841,6 +813,22 @@
|
||||||
"updated": "2018-10-11T19:22:47.331Z",
|
"updated": "2018-10-11T19:22:47.331Z",
|
||||||
"reasonDetail": "Operating on already-existing DOM elements"
|
"reasonDetail": "Operating on already-existing DOM elements"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-$(",
|
||||||
|
"path": "js/views/safety_number_change_dialog_view.js",
|
||||||
|
"line": " this.$('.safety-number-change-dialog-wrapper').append(dialog.el);",
|
||||||
|
"lineNumber": 36,
|
||||||
|
"reasonCategory": "usageTrusted",
|
||||||
|
"updated": "2020-06-23T06:48:06.829Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "jQuery-append(",
|
||||||
|
"path": "js/views/safety_number_change_dialog_view.js",
|
||||||
|
"line": " this.$('.safety-number-change-dialog-wrapper').append(dialog.el);",
|
||||||
|
"lineNumber": 36,
|
||||||
|
"reasonCategory": "usageTrusted",
|
||||||
|
"updated": "2020-06-23T06:48:06.829Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/settings_view.js",
|
"path": "js/views/settings_view.js",
|
||||||
|
@ -11579,6 +11567,15 @@
|
||||||
"updated": "2020-02-14T20:02:37.507Z",
|
"updated": "2020-02-14T20:02:37.507Z",
|
||||||
"reasonDetail": "Used only to set focus"
|
"reasonDetail": "Used only to set focus"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "React-createRef",
|
||||||
|
"path": "ts/components/SafetyNumberChangeDialog.js",
|
||||||
|
"line": " const cancelButtonRef = React.createRef();",
|
||||||
|
"lineNumber": 14,
|
||||||
|
"reasonCategory": "usageTrusted",
|
||||||
|
"updated": "2020-06-23T06:48:06.829Z",
|
||||||
|
"reasonDetail": "Used to focus cancel button when dialog opens"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/SearchResults.js",
|
"path": "ts/components/SearchResults.js",
|
||||||
|
|
|
@ -13,11 +13,9 @@ export function remove() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isDone() {
|
export function isDone() {
|
||||||
// tslint:disable-next-line no-backbone-get-set-outside-model
|
|
||||||
return window.storage.get('chromiumRegistrationDone') === '';
|
return window.storage.get('chromiumRegistrationDone') === '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function everDone() {
|
export function everDone() {
|
||||||
// tslint:disable-next-line no-backbone-get-set-outside-model
|
|
||||||
return window.storage.get('chromiumRegistrationDoneEver') === '' || isDone();
|
return window.storage.get('chromiumRegistrationDoneEver') === '' || isDone();
|
||||||
}
|
}
|
||||||
|
|
58
ts/util/safetyNumber.ts
Normal file
58
ts/util/safetyNumber.ts
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import { ConversationType } from '../state/ducks/conversations';
|
||||||
|
|
||||||
|
export async function generateSecurityNumber(
|
||||||
|
ourNumber: string,
|
||||||
|
ourKey: ArrayBuffer,
|
||||||
|
theirNumber: string,
|
||||||
|
theirKey: ArrayBuffer
|
||||||
|
): Promise<string> {
|
||||||
|
return new window.libsignal.FingerprintGenerator(5200).createFor(
|
||||||
|
ourNumber,
|
||||||
|
ourKey,
|
||||||
|
theirNumber,
|
||||||
|
theirKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPlaceholder(): string {
|
||||||
|
return Array.from(Array(12))
|
||||||
|
.map(() => 'XXXXX')
|
||||||
|
.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateSecurityNumberBlock(
|
||||||
|
contact: ConversationType
|
||||||
|
): Promise<Array<string>> {
|
||||||
|
const ourNumber = window.textsecure.storage.user.getNumber();
|
||||||
|
const ourUuid = window.textsecure.storage.user.getUuid();
|
||||||
|
|
||||||
|
const us = window.textsecure.storage.protocol.getIdentityRecord(
|
||||||
|
ourUuid || ourNumber
|
||||||
|
);
|
||||||
|
const ourKey = us ? us.publicKey : null;
|
||||||
|
|
||||||
|
const them = window.textsecure.storage.protocol.getIdentityRecord(contact.id);
|
||||||
|
const theirKey = them ? them.publicKey : null;
|
||||||
|
|
||||||
|
if (!ourKey) {
|
||||||
|
throw new Error('Could not load our key');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!theirKey) {
|
||||||
|
throw new Error('Could not load their key');
|
||||||
|
}
|
||||||
|
|
||||||
|
const securityNumber = await generateSecurityNumber(
|
||||||
|
ourNumber,
|
||||||
|
ourKey,
|
||||||
|
contact.e164,
|
||||||
|
theirKey
|
||||||
|
);
|
||||||
|
|
||||||
|
const chunks = [];
|
||||||
|
for (let i = 0; i < securityNumber.length; i += 5) {
|
||||||
|
chunks.push(securityNumber.substring(i, i + 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunks;
|
||||||
|
}
|
7
ts/window.d.ts
vendored
7
ts/window.d.ts
vendored
|
@ -13,9 +13,12 @@ import * as Crypto from './Crypto';
|
||||||
import { ColorType, LocalizerType } from './types/Util';
|
import { ColorType, LocalizerType } from './types/Util';
|
||||||
import { SendOptionsType } from './textsecure/SendMessage';
|
import { SendOptionsType } from './textsecure/SendMessage';
|
||||||
|
|
||||||
|
type TaskResultType = any;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
dcodeIO: DCodeIOType;
|
dcodeIO: DCodeIOType;
|
||||||
|
getConversations: () => ConversationControllerType;
|
||||||
getExpiration: () => string;
|
getExpiration: () => string;
|
||||||
getEnvironment: () => string;
|
getEnvironment: () => string;
|
||||||
getSocketStatus: () => number;
|
getSocketStatus: () => number;
|
||||||
|
@ -83,12 +86,16 @@ export type ConversationType = {
|
||||||
getColor(): ColorType | undefined;
|
getColor(): ColorType | undefined;
|
||||||
getName(): string | undefined;
|
getName(): string | undefined;
|
||||||
getNumber(): string;
|
getNumber(): string;
|
||||||
|
getProfiles(): Promise<Array<Promise<void>>>;
|
||||||
getProfileName(): string | undefined;
|
getProfileName(): string | undefined;
|
||||||
getRecipients: () => Array<string>;
|
getRecipients: () => Array<string>;
|
||||||
getSendOptions(): SendOptionsType;
|
getSendOptions(): SendOptionsType;
|
||||||
|
getTitle(): string;
|
||||||
|
isVerified(): boolean;
|
||||||
safeGetVerified(): Promise<number>;
|
safeGetVerified(): Promise<number>;
|
||||||
getIsAddedByContact(): boolean;
|
getIsAddedByContact(): boolean;
|
||||||
addCallHistory(details: CallHistoryDetailsType): void;
|
addCallHistory(details: CallHistoryDetailsType): void;
|
||||||
|
toggleVerified(): Promise<TaskResultType>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConversationControllerType = {
|
export type ConversationControllerType = {
|
||||||
|
|
|
@ -45,6 +45,9 @@
|
||||||
// We always want 'as Type'
|
// We always want 'as Type'
|
||||||
"no-angle-bracket-type-assertion": true,
|
"no-angle-bracket-type-assertion": true,
|
||||||
|
|
||||||
|
// mostly always a false positive
|
||||||
|
"no-backbone-get-set-outside-model": false,
|
||||||
|
|
||||||
"no-consecutive-blank-lines": [true, 2],
|
"no-consecutive-blank-lines": [true, 2],
|
||||||
"object-literal-key-quotes": [true, "as-needed"],
|
"object-literal-key-quotes": [true, "as-needed"],
|
||||||
"object-literal-sort-keys": false,
|
"object-literal-sort-keys": false,
|
||||||
|
|
Loading…
Add table
Reference in a new issue