diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index dc6d4183d..afefeff60 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -167,7 +167,7 @@
"Only available on development modes, menu option to open up the standalone device setup sequence"
},
"connectingLoad": {
- "message": "Connecting...",
+ "message": "Connecting To Server",
"description":
"Message shown on the as a loading screen while we are connecting to something"
},
@@ -395,6 +395,9 @@
"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."
},
+ "changedSinceVerifiedTitle": {
+ "message": "Safety Number Changed"
+ },
"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.",
@@ -660,6 +663,9 @@
"unableToLoadAttachment": {
"message": "Unable to load selected attachment."
},
+ "connect": {
+ "message": "Connect"
+ },
"disconnected": {
"message": "Disconnected",
"description":
@@ -946,7 +952,7 @@
"message": "Close"
},
"pairNewDevice": {
- "message": "Pair new Device"
+ "message": "Pair New Device"
},
"devicePairingAccepted": {
"message": "Device Pairing Accepted"
@@ -957,9 +963,15 @@
"waitingForDeviceToRegister": {
"message": "Waiting for device to register..."
},
+ "pairNewDevicePrompt": {
+ "message": "Scan the QR Code on your secondary device"
+ },
"pairedDevices": {
"message": "Paired Devices"
},
+ "noPairedDevices": {
+ "message": "No paired devices"
+ },
"allowPairing": {
"message": "Allow Pairing"
},
@@ -1118,7 +1130,7 @@
"Shown on the drop-down menu for an individual message, deletes single message"
},
"deleteMessages": {
- "message": "Delete messages",
+ "message": "Delete Messages",
"description": "Menu item for deleting messages, title case."
},
"deletePublicConversationConfirmation": {
@@ -1143,7 +1155,7 @@
"Confirmation dialog text that tells the user what will happen if they leave the public channel."
},
"deleteContact": {
- "message": "Delete contact",
+ "message": "Delete Contact",
"description":
"Confirmation dialog title that asks the user if they really wish to delete the contact. Answer buttons use the strings 'ok' and 'cancel'. The deletion is permanent, i.e. it cannot be undone."
},
@@ -2015,7 +2027,7 @@
},
"banUser": {
- "message": "Ban user",
+ "message": "Ban User",
"description": "Ban user from public chat by public key."
},
@@ -2103,8 +2115,16 @@
"description":
"Button action that the user can click to connect to a new public server"
},
+ "serverUrl": {
+ "message": "Server URL",
+ "description": "Placeholder for server URL input"
+ },
+ "noServerUrl": {
+ "message": "Please enter a server URL",
+ "description": "Error message when no server url entered"
+ },
"addServerDialogTitle": {
- "message": "Connect to new public server",
+ "message": "Connect To New Public Server",
"description":
"Title for the dialog box used to connect to a new public server"
},
diff --git a/background.html b/background.html
index 0a03d5540..4b71134a2 100644
--- a/background.html
+++ b/background.html
@@ -52,6 +52,7 @@
+
diff --git a/js/background.js b/js/background.js
index 94d98147b..7d08eee07 100644
--- a/js/background.js
+++ b/js/background.js
@@ -802,12 +802,19 @@
appView.openConversation(groupId, {});
};
- // $(document).ready(() => {
- // window.settingsView = new Whisper.SessionSettingsView({
- // el: $('#settings-container'),
- // });
- // window.settingsView.render();
- // });
+ window.confirmationDialog = params => {
+ const confirmDialog = new Whisper.SessionConfirmView({
+ el: $('#session-confirm-container'),
+ title: params.title,
+ message: params.message,
+ resolve: params.resolve || undefined,
+ reject: params.reject || undefined,
+ okText: params.okText || undefined,
+ cancelText: params.cancelText || undefined,
+ hideCancel: params.hideCancel || false,
+ });
+ confirmDialog.render();
+ };
window.generateID = () =>
Math.random()
@@ -825,6 +832,7 @@
id: options.id || window.generateID(),
description: options.description || '',
type: options.type || '',
+ shouldFade: options.shouldFade,
};
// Give all toasts an ID. User may define.
@@ -1077,6 +1085,7 @@
pubkey: userPubKey,
avatarPath,
avatarColor: conversation.getColor(),
+ isRss: conversation.isRss(),
onStartConversation: () => {
Whisper.events.trigger('showConversation', userPubKey);
},
diff --git a/js/models/conversations.js b/js/models/conversations.js
index 667af34da..a427dfacd 100644
--- a/js/models/conversations.js
+++ b/js/models/conversations.js
@@ -2665,13 +2665,18 @@
},
deleteContact() {
+ const title = this.isPublic()
+ ? i18n('deletePublicChannel')
+ : i18n('deleteContact');
+
const message = this.isPublic()
? i18n('deletePublicChannelConfirmation')
: i18n('deleteContactConfirmation');
- Whisper.events.trigger('showConfirmationDialog', {
+ window.confirmationDialog({
+ title,
message,
- onOk: () => ConversationController.deleteContact(this.id),
+ resolve: () => ConversationController.deleteContact(this.id),
});
},
@@ -2721,17 +2726,23 @@
deleteMessages() {
this.resetMessageSelection();
+
+ let params;
if (this.isPublic()) {
- Whisper.events.trigger('showConfirmationDialog', {
+ params = {
+ title: i18n('deleteMessages'),
message: i18n('deletePublicConversationConfirmation'),
- onOk: () => ConversationController.deleteContact(this.id),
- });
+ resolve: () => ConversationController.deleteContact(this.id),
+ };
} else {
- Whisper.events.trigger('showConfirmationDialog', {
+ params = {
+ title: i18n('deleteMessages'),
message: i18n('deleteConversationConfirmation'),
- onOk: () => this.destroyMessages(),
- });
+ resolve: () => this.destroyMessages(),
+ };
}
+
+ window.confirmationDialog(params);
},
async destroyMessages() {
diff --git a/js/models/messages.js b/js/models/messages.js
index 8cfd7133d..e1abafad2 100644
--- a/js/models/messages.js
+++ b/js/models/messages.js
@@ -1030,9 +1030,10 @@
},
banUser() {
- window.Whisper.events.trigger('showConfirmationDialog', {
+ window.confirmationDialog({
+ title: i18n('banUser'),
message: i18n('banUserConfirm'),
- onOk: async () => {
+ resolve: async () => {
const source = this.get('source');
const conversation = this.getConversation();
diff --git a/js/modules/signal.js b/js/modules/signal.js
index f13542db1..47b6c5543 100644
--- a/js/modules/signal.js
+++ b/js/modules/signal.js
@@ -50,12 +50,20 @@ const {
} = require('../../ts/components/conversation/CreateGroupDialog');
const { EditProfileDialog } = require('../../ts/components/EditProfileDialog');
const { UserDetailsDialog } = require('../../ts/components/UserDetailsDialog');
+const {
+ DevicePairingDialog,
+} = require('../../ts/components/DevicePairingDialog');
+const { AddServerDialog } = require('../../ts/components/AddServerDialog');
+
const {
SessionSettings,
} = require('../../ts/components/session/SessionSettings');
const { SessionToast } = require('../../ts/components/session/SessionToast');
const { SessionToggle } = require('../../ts/components/session/SessionToggle');
const { SessionModal } = require('../../ts/components/session/SessionModal');
+const {
+ SessionConfirm,
+} = require('../../ts/components/session/SessionConfirm');
const {
SessionDropdown,
} = require('../../ts/components/session/SessionDropdown');
@@ -251,6 +259,8 @@ exports.setup = (options = {}) => {
CreateGroupDialog,
EditProfileDialog,
UserDetailsDialog,
+ DevicePairingDialog,
+ AddServerDialog,
SessionRegistrationView,
ConfirmDialog,
UpdateGroupDialog,
@@ -260,6 +270,7 @@ exports.setup = (options = {}) => {
SessionSettings,
SessionToast,
SessionToggle,
+ SessionConfirm,
SessionModal,
SessionDropdown,
MediaGallery,
diff --git a/js/permissions_popup_start.js b/js/permissions_popup_start.js
index f153a0bf9..69fe851d7 100644
--- a/js/permissions_popup_start.js
+++ b/js/permissions_popup_start.js
@@ -1,4 +1,4 @@
-/* global $, Whisper, i18n */
+/* global $, i18n */
$(document).on('keyup', e => {
'use strict';
@@ -8,11 +8,8 @@ $(document).on('keyup', e => {
}
});
-const $body = $(document.body);
-$body.addClass(`${window.theme}-theme`);
-
-window.view = new Whisper.ConfirmationDialogView({
- message: i18n('audioPermissionNeeded'),
+window.confirmationDialog({
+ title: i18n('audioPermissionNeeded'),
okText: i18n('allowAccess'),
resolve: () => {
'use strict';
@@ -20,7 +17,5 @@ window.view = new Whisper.ConfirmationDialogView({
window.setMediaPermissions(true);
window.closePermissionsPopup();
},
- reject: window.closePermissionsPopup,
+ onClose: window.closePermissionsPopup,
});
-
-window.view.$el.appendTo($body);
diff --git a/js/views/add_server_dialog_view.js b/js/views/add_server_dialog_view.js
index ae05bb84e..e81c895ad 100644
--- a/js/views/add_server_dialog_view.js
+++ b/js/views/add_server_dialog_view.js
@@ -1,4 +1,4 @@
-/* global Whisper, i18n, _ */
+/* global Whisper, i18n, */
// eslint-disable-next-line func-names
(function() {
@@ -7,86 +7,28 @@
window.Whisper = window.Whisper || {};
Whisper.AddServerDialogView = Whisper.View.extend({
- templateName: 'add-server-template',
- className: 'loki-dialog add-server modal',
- initialize(options = {}) {
- this.title = i18n('addServerDialogTitle');
- this.okText = options.okText || i18n('ok');
- this.cancelText = options.cancelText || i18n('cancel');
- this.$('input').focus();
+ className: 'loki-dialog add-server-dialog modal',
+ initialize() {
+ this.close = this.close.bind(this);
this.render();
},
- events: {
- keyup: 'onKeyup',
- 'click .ok': 'confirm',
- 'click .cancel': 'close',
- },
- render_attributes() {
- return {
- title: this.title,
- ok: this.okText,
- cancel: this.cancelText,
- };
- },
- confirm() {
- // Remove error if there is one
- this.showError(null);
- const serverUrl = this.$('#server-url')
- .val()
- .toLowerCase();
- // TODO: Make this not hard coded
- const channelId = 1;
- const dialog = new Whisper.ConnectingToServerDialogView({
- serverUrl,
- channelId,
- });
- const dialogDelayTimer = setTimeout(() => {
- this.el.append(dialog.el);
- }, 200);
- dialog.once('connectionResult', result => {
- clearTimeout(dialogDelayTimer);
- if (result.cancelled) {
- this.showError(null);
- return;
- }
- if (result.errorCode) {
- this.showError(result.errorCode);
- return;
- }
- window.pushToast({
- title: i18n('connectToServerSuccess'),
- type: 'success',
- id: 'connectToServerSuccess',
- });
- this.close();
+
+ render() {
+ this.dialogView = new Whisper.ReactWrapperView({
+ className: 'add-server-dialog',
+ Component: window.Signal.Components.AddServerDialog,
+ props: {
+ i18n,
+ onClose: this.close,
+ },
});
- dialog.trigger('attemptConnection');
+
+ this.$el.append(this.dialogView.el);
+ return this;
},
+
close() {
this.remove();
},
- showError(message) {
- if (_.isEmpty(message)) {
- this.$('.error').text('');
- this.$('.error').hide();
- } else {
- this.$('.error').text(`Error: ${message}`);
- this.$('.error').show();
- }
- this.$('input').focus();
- },
- onKeyup(event) {
- switch (event.key) {
- case 'Enter':
- this.confirm();
- break;
- case 'Escape':
- case 'Esc':
- this.close();
- break;
- default:
- break;
- }
- },
});
})();
diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js
index 06df077dc..17e15899b 100644
--- a/js/views/conversation_view.js
+++ b/js/views/conversation_view.js
@@ -1308,7 +1308,7 @@
},
forceSend({ contact, message }) {
- const dialog = new Whisper.ConfirmationDialogView({
+ window.confirmationDialog({
message: i18n('identityKeyErrorOnSend', [
contact.getTitle(),
contact.getTitle(),
@@ -1329,9 +1329,6 @@
message.resend(contact.id);
},
});
-
- this.$el.prepend(dialog.el);
- dialog.focusCancel();
},
showSafetyNumber(providedModel) {
@@ -1438,14 +1435,11 @@
return;
}
- const dialog = new Whisper.ConfirmationDialogView({
+ window.confirmationDialog({
message: warningMessage,
okText: i18n('delete'),
resolve: doDelete,
});
-
- this.$el.prepend(dialog.el);
- dialog.focusCancel();
},
deleteMessage(message) {
@@ -1669,7 +1663,8 @@
}
}
- const dialog = new Whisper.ConfirmationDialogView({
+ window.confirmationDialog({
+ title: i18n('changedSinceVerifiedTitle'),
message,
okText: i18n('sendAnyway'),
resolve: () => {
@@ -1679,9 +1674,6 @@
this.focusMessageFieldAndClearDisabled();
},
});
-
- this.$el.prepend(dialog.el);
- dialog.focusCancel();
},
stripQuery(text, cursorPos) {
diff --git a/js/views/device_pairing_dialog_view.js b/js/views/device_pairing_dialog_view.js
index 80d6b4f61..50ba4e9c2 100644
--- a/js/views/device_pairing_dialog_view.js
+++ b/js/views/device_pairing_dialog_view.js
@@ -1,12 +1,4 @@
-/* global
- Whisper,
- i18n,
- libloki,
- textsecure,
- ConversationController,
- $,
- QRCode,
-*/
+/* global Whisper, i18n, */
// eslint-disable-next-line func-names
(function() {
@@ -16,194 +8,27 @@
Whisper.DevicePairingDialogView = Whisper.View.extend({
className: 'loki-dialog device-pairing-dialog modal',
- templateName: 'device-pairing-dialog',
initialize() {
- this.pubKeyRequests = [];
- this.reset();
+ this.close = this.close.bind(this);
this.render();
- this.showView();
- this.qr = new QRCode(this.$('#qr')[0], {
- correctLevel: QRCode.CorrectLevel.L,
- });
- this.qr.makeCode(textsecure.storage.user.getNumber());
- },
- reset() {
- this.pubKey = null;
- this.accepted = false;
- this.isListening = false;
- this.pubKeyToUnpair = null;
- this.success = false;
- },
- events: {
- 'click #startPairing': 'startReceivingRequests',
- 'click #close': 'close',
- 'click .waitingForRequestView .cancel': 'stopReceivingRequests',
- 'click .requestReceivedView .skip': 'skipDevice',
- 'click #allowPairing': 'allowDevice',
- 'click .requestAcceptedView .ok': 'stopReceivingRequests',
- 'click .confirmUnpairView .cancel': 'stopReceivingRequests',
- 'click .confirmUnpairView .unpairDevice': 'confirmUnpairDevice',
- },
- render_attributes() {
- return {
- defaultTitle: i18n('pairedDevices'),
- waitingForRequestTitle: i18n('waitingForDeviceToRegister'),
- requestReceivedTitle: i18n('devicePairingReceived'),
- requestAcceptedTitle: i18n('devicePairingAccepted'),
- startPairingText: i18n('pairNewDevice'),
- cancelText: i18n('cancel'),
- unpairDevice: i18n('unpairDevice'),
- closeText: i18n('close'),
- skipText: i18n('skip'),
- okText: i18n('ok'),
- allowPairingText: i18n('allowPairing'),
- confirmUnpairViewTitle: i18n('confirmUnpairingTitle'),
- };
- },
- startReceivingRequests() {
- this.trigger('startReceivingRequests');
- this.isListening = true;
- this.showView();
- },
- stopReceivingRequests() {
- if (this.success) {
- const deviceAlias = this.$('#deviceAlias')[0].value.trim();
- const conv = ConversationController.get(this.pubKey);
- if (conv) {
- conv.setNickname(deviceAlias);
- }
- }
- this.trigger('stopReceivingRequests');
- this.reset();
- this.showView();
- },
- requestReceived(secondaryDevicePubKey) {
- // FIFO: push at the front of the array with unshift()
- this.pubKeyRequests.unshift(secondaryDevicePubKey);
- if (!this.pubKey) {
- this.nextPubKey();
- this.showView('requestReceived');
- }
- },
- allowDevice() {
- this.accepted = true;
- this.trigger('devicePairingRequestAccepted', this.pubKey, errors =>
- this.transmisssionCB(errors)
- );
- this.showView();
- },
- transmisssionCB(errors) {
- if (!errors) {
- this.$('.transmissionStatus').text(i18n('provideDeviceAlias'));
- this.$('#deviceAliasView').show();
- this.$('#deviceAlias').on('input', e => {
- if (e.target.value.trim()) {
- this.$('.requestAcceptedView .ok').removeAttr('disabled');
- } else {
- this.$('.requestAcceptedView .ok').attr('disabled', true);
- }
- });
- this.$('.requestAcceptedView .ok').show();
- this.$('.requestAcceptedView .ok').attr('disabled', true);
- this.success = true;
- } else {
- this.$('.transmissionStatus').text(errors);
- this.$('.requestAcceptedView .ok').show();
- }
},
- skipDevice() {
- this.trigger('devicePairingRequestRejected', this.pubKey);
- this.nextPubKey();
- this.showView();
- },
- nextPubKey() {
- // FIFO: pop at the back of the array using pop()
- this.pubKey = this.pubKeyRequests.pop();
- },
- async confirmUnpairDevice() {
- this.trigger('deviceUnpairingRequested', this.pubKeyToUnpair);
- this.reset();
- this.showView();
- },
- requestUnpairDevice(pubKey) {
- this.pubKeyToUnpair = pubKey;
- this.showView();
- },
- getPubkeyName(pubKey) {
- const secretWords = window.mnemonic.pubkey_to_secret_words(pubKey);
- const conv = ConversationController.get(pubKey);
- const deviceAlias = conv ? conv.getNickname() : 'Unnamed Device';
- return `${deviceAlias} (pairing secret: ${secretWords})`;
- },
- async showView() {
- const defaultView = this.$('.defaultView');
- const waitingForRequestView = this.$('.waitingForRequestView');
- const requestReceivedView = this.$('.requestReceivedView');
- const requestAcceptedView = this.$('.requestAcceptedView');
- const confirmUnpairView = this.$('.confirmUnpairView');
- if (this.pubKeyToUnpair) {
- defaultView.hide();
- requestReceivedView.hide();
- waitingForRequestView.hide();
- requestAcceptedView.hide();
- confirmUnpairView.show();
- const name = this.getPubkeyName(this.pubKeyToUnpair);
- this.$('.confirmUnpairView #pubkey').html(name);
- } else if (!this.isListening) {
- requestReceivedView.hide();
- waitingForRequestView.hide();
- requestAcceptedView.hide();
- confirmUnpairView.hide();
- const ourPubKey = textsecure.storage.user.getNumber();
- defaultView.show();
- const pubKeys = await libloki.storage.getSecondaryDevicesFor(ourPubKey);
- this.$('#pairedPubKeys').empty();
- if (pubKeys && pubKeys.length > 0) {
- this.$('#startPairing').attr('disabled', true);
- pubKeys.forEach(x => {
- const name = this.getPubkeyName(x);
- const li = $('
').html(name);
- if (window.lokiFeatureFlags.multiDeviceUnpairing) {
- const link = $('')
- .text('Unpair')
- .attr('href', '#');
- link.on('click', () => this.requestUnpairDevice(x));
- li.append(' - ');
- li.append(link);
- }
- this.$('#pairedPubKeys').append(li);
- });
- } else {
- this.$('#startPairing').removeAttr('disabled');
- this.$('#pairedPubKeys').append('No paired devices');
- }
- } else if (this.accepted) {
- defaultView.hide();
- requestReceivedView.hide();
- waitingForRequestView.hide();
- requestAcceptedView.show();
- } else if (this.pubKey) {
- const secretWords = window.mnemonic.pubkey_to_secret_words(this.pubKey);
- this.$('.secretWords').text(secretWords);
- requestReceivedView.show();
- waitingForRequestView.hide();
- requestAcceptedView.hide();
- defaultView.hide();
- } else {
- waitingForRequestView.show();
- requestReceivedView.hide();
- requestAcceptedView.hide();
- defaultView.hide();
- }
+ render() {
+ this.dialogView = new Whisper.ReactWrapperView({
+ className: 'device-pairing-dialog',
+ Component: window.Signal.Components.DevicePairingDialog,
+ props: {
+ i18n,
+ onClose: this.close,
+ },
+ });
+
+ this.$el.append(this.dialogView.el);
+ return this;
},
+
close() {
this.remove();
- this.qr.clear();
- if (this.pubKey && !this.accepted) {
- this.trigger('devicePairingRequestRejected', this.pubKey);
- }
- this.trigger('close');
},
});
})();
diff --git a/js/views/inbox_view.js b/js/views/inbox_view.js
index 8353f5a9a..777af7523 100644
--- a/js/views/inbox_view.js
+++ b/js/views/inbox_view.js
@@ -54,13 +54,12 @@
toast.render();
},
showConfirmationDialog({ title, message, onOk, onCancel }) {
- const dialog = new Whisper.ConfirmationDialogView({
+ window.confirmationDialog({
title,
message,
resolve: onOk,
reject: onCancel,
});
- this.el.append(dialog.el);
},
});
diff --git a/js/views/key_verification_view.js b/js/views/key_verification_view.js
index 64a7e87fc..af4965cce 100644
--- a/js/views/key_verification_view.js
+++ b/js/views/key_verification_view.js
@@ -61,16 +61,14 @@
onSafetyNumberChanged() {
this.model.getProfiles().then(this.loadKeys.bind(this));
- const dialog = new Whisper.ConfirmationDialogView({
+ window.confirmationDialog({
+ title: i18n('changedSinceVerifiedTitle'),
message: i18n('changedRightAfterVerify', [
this.model.getTitle(),
this.model.getTitle(),
]),
hideCancel: true,
});
-
- dialog.$el.insertBefore(this.el);
- dialog.focusCancel();
},
toggleVerified() {
this.$('button.verify').attr('disabled', true);
diff --git a/js/views/session_confirm_view.js b/js/views/session_confirm_view.js
new file mode 100644
index 000000000..4fc0335c8
--- /dev/null
+++ b/js/views/session_confirm_view.js
@@ -0,0 +1,54 @@
+/* global Whisper */
+
+// eslint-disable-next-line func-names
+(function() {
+ 'use strict';
+
+ window.Whisper = window.Whisper || {};
+
+ Whisper.SessionConfirmView = Whisper.View.extend({
+ initialize(options) {
+ this.props = {
+ title: options.title,
+ message: options.message,
+ onClickOk: this.ok.bind(this),
+ onClickClose: this.cancel.bind(this),
+ resolve: options.resolve,
+ reject: options.reject,
+ okText: options.okText,
+ cancelText: options.cancelText,
+ hideCancel: options.hideCancel,
+ };
+ },
+
+ render() {
+ this.$('.session-confirm-wrapper').remove();
+
+ this.confirmView = new Whisper.ReactWrapperView({
+ className: 'session-confirm-wrapper',
+ Component: window.Signal.Components.SessionConfirm,
+ props: this.props,
+ });
+
+ this.$el.append(this.confirmView.el);
+ },
+
+ ok() {
+ this.$('.session-confirm-wrapper').remove();
+ if (this.props.resolve) {
+ this.props.resolve();
+ }
+ },
+ cancel() {
+ this.$('.session-confirm-wrapper').remove();
+ if (this.props.reject) {
+ this.props.reject();
+ }
+ },
+ onKeyup(event) {
+ if (event.key === 'Escape' || event.key === 'Esc') {
+ this.cancel();
+ }
+ },
+ });
+})();
diff --git a/js/views/session_toast_view.js b/js/views/session_toast_view.js
index 3579a7f69..460862717 100644
--- a/js/views/session_toast_view.js
+++ b/js/views/session_toast_view.js
@@ -32,13 +32,18 @@
this.props.id = options.id;
this.props.description = options.description || '';
this.props.type = options.type || '';
+ this.props.shouldFade = options.shouldFade !== false;
this.toastView.update(this.props);
this.showToast();
- clearTimeout(this.timer);
- this.timer = setTimeout(this.fadeToast.bind(this), 4000);
+ if (this.timer) {
+ clearTimeout(this.timer);
+ }
+ if (this.props.shouldFade) {
+ this.timer = setTimeout(this.fadeToast.bind(this), 4000);
+ }
},
showToast() {
diff --git a/js/views/user_details_dialog_view.js b/js/views/user_details_dialog_view.js
index 7638639e4..98c164691 100644
--- a/js/views/user_details_dialog_view.js
+++ b/js/views/user_details_dialog_view.js
@@ -13,6 +13,7 @@
avatarPath,
avatarColor,
pubkey,
+ isRss,
onOk,
onStartConversation,
}) {
@@ -20,6 +21,7 @@
this.profileName = profileName;
this.pubkey = pubkey;
+ this.isRss = isRss;
this.avatarPath = avatarPath;
this.avatarColor = avatarColor;
this.onOk = onOk;
@@ -38,6 +40,7 @@
onStartConversation: this.onStartConversation,
profileName: this.profileName,
pubkey: this.pubkey,
+ isRss: this.isRss,
avatarPath: this.avatarPath,
i18n,
},
diff --git a/js/views/whisper_view.js b/js/views/whisper_view.js
index 4ac7c6f7e..121277475 100644
--- a/js/views/whisper_view.js
+++ b/js/views/whisper_view.js
@@ -53,13 +53,12 @@
},
confirm(message, okText) {
return new Promise((resolve, reject) => {
- const dialog = new Whisper.ConfirmationDialogView({
- message,
+ window.confirmationDialog({
+ title: message,
okText,
resolve,
reject,
});
- this.$el.append(dialog.el);
});
},
},
diff --git a/package.json b/package.json
index 2dd44aa85..a2305a59a 100644
--- a/package.json
+++ b/package.json
@@ -101,10 +101,12 @@
"pify": "3.0.0",
"protobufjs": "6.8.6",
"proxy-agent": "3.0.3",
+ "qrcode": "^1.4.4",
"react": "16.8.3",
"react-contextmenu": "2.11.0",
"react-dom": "16.8.3",
"react-portal": "^4.2.0",
+ "react-qrcode": "^0.2.0",
"react-redux": "6.0.1",
"react-virtualized": "9.21.0",
"read-last-lines": "1.3.0",
diff --git a/stylesheets/_session.scss b/stylesheets/_session.scss
index 5628d0c6f..228286ead 100644
--- a/stylesheets/_session.scss
+++ b/stylesheets/_session.scss
@@ -74,6 +74,7 @@ $session-color-light-grey: #a0a0a0;
$session-shadow-opacity: 0.15;
$session-overlay-opacity: 0.3;
+$session-margin-xs: 5px;
$session-margin-sm: 10px;
$session-margin-md: 15px;
$session-margin-lg: 20px;
@@ -92,6 +93,18 @@ div.spacer-lg {
color: rgba($color, 0.6);
}
+.text-subtle {
+ opacity: 0.6;
+}
+
+.text-soft {
+ opacity: 0.4;
+}
+
+.fullwidth {
+ width: 100%;
+}
+
$session-transition-duration: 0.25s;
$session-icon-size-sm: 15px;
@@ -251,6 +264,30 @@ $session_message-container-border-radius: 5px;
}
}
+.session-label {
+ color: $session-color-white;
+ padding: $session-margin-sm;
+ width: 100%;
+ border-radius: 2px;
+ text-align: center;
+
+ &.primary {
+ background-color: $session-color-primary;
+ }
+ &.secondary {
+ background-color: $session-color-secondary;
+ }
+ &.success {
+ background-color: $session-color-success;
+ }
+ &.danger {
+ background-color: $session-color-danger;
+ }
+ &.warning {
+ background-color: $session-color-warning;
+ }
+}
+
@mixin set-icon-margin($size) {
margin: $size / 3;
}
@@ -546,8 +583,7 @@ label {
}
&__body {
- padding: $session-margin-lg;
-
+ padding: 0px $session-margin-lg $session-margin-lg $session-margin-lg;
font-family: 'Wasa';
line-height: 16px;
font-size: 13px;
@@ -557,15 +593,27 @@ label {
}
}
+ &__centered {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+
&__button-group {
display: flex;
justify-content: flex-end;
+ .session-button {
+ margin-left: $session-margin-sm;
+ }
+
&__center {
- align-items: center;
+ display: flex;
+ justify-content: center;
}
+
.session-button {
- margin-left: $session-margin-sm;
+ margin: 0 $session-margin-xs;
}
}
}
@@ -690,3 +738,60 @@ label {
margin-top: 50px;
margin-left: 75px;
}
+
+.session-loader {
+ display: inline-block;
+ position: relative;
+ width: 80px;
+ height: 80px;
+
+ div {
+ position: absolute;
+ top: 33px;
+ width: 13px;
+ height: 13px;
+ border-radius: 50%;
+ background: $session-color-green;
+ animation-timing-function: cubic-bezier(0, 1, 1, 0);
+ }
+ div:nth-child(1) {
+ left: 8px;
+ animation: session-loader1 0.6s infinite;
+ }
+ div:nth-child(2) {
+ left: 8px;
+ animation: session-loader2 0.6s infinite;
+ }
+ div:nth-child(3) {
+ left: 32px;
+ animation: session-loader2 0.6s infinite;
+ }
+ div:nth-child(4) {
+ left: 56px;
+ animation: session-loader3 0.6s infinite;
+ }
+ @keyframes session-loader1 {
+ 0% {
+ transform: scale(0);
+ }
+ 100% {
+ transform: scale(1);
+ }
+ }
+ @keyframes session-loader3 {
+ 0% {
+ transform: scale(1);
+ }
+ 100% {
+ transform: scale(0);
+ }
+ }
+ @keyframes session-loader2 {
+ 0% {
+ transform: translate(0, 0);
+ }
+ 100% {
+ transform: translate(24px, 0);
+ }
+ }
+}
diff --git a/ts/components/AddServerDialog.tsx b/ts/components/AddServerDialog.tsx
new file mode 100644
index 000000000..b6373455f
--- /dev/null
+++ b/ts/components/AddServerDialog.tsx
@@ -0,0 +1,256 @@
+import React from 'react';
+
+import { SessionModal } from './session/SessionModal';
+import { SessionButton } from './session/SessionButton';
+import { SessionSpinner } from './session/SessionSpinner';
+
+interface Props {
+ i18n: any;
+ onClose: any;
+}
+
+interface State {
+ title: string;
+ error: string | null;
+ connecting: boolean;
+ success: boolean;
+ view: 'connecting' | 'default';
+ serverURL: string;
+}
+
+export class AddServerDialog extends React.Component {
+ constructor(props: any) {
+ super(props);
+
+ this.state = {
+ title: this.props.i18n('addServerDialogTitle'),
+ error: null,
+ connecting: false,
+ success: false,
+ view: 'default',
+ serverURL: '',
+ };
+
+ this.showError = this.showError.bind(this);
+ this.showView = this.showView.bind(this);
+ this.attemptConnection = this.attemptConnection.bind(this);
+
+ this.closeDialog = this.closeDialog.bind(this);
+ this.onKeyUp = this.onKeyUp.bind(this);
+ }
+
+ public render() {
+ const { i18n } = this.props;
+
+ return (
+ null}
+ onClose={this.closeDialog}
+ >
+ {this.state.view === 'default' && (
+ <>
+
+
+
+
+
+ {this.showError()}
+
+
+ this.showView('connecting')}
+ />
+
+
+
+ >
+ )}
+
+ {this.state.view === 'connecting' && (
+ <>
+
+
+
+ this.showView('default')}
+ />
+
+ >
+ )}
+
+ );
+ }
+
+ private showView(view: 'default' | 'connecting', error?: string) {
+ const { i18n } = this.props;
+
+ const isDefaultView = view === 'default';
+ const isConnectingView = view === 'connecting';
+
+ if (isDefaultView) {
+ this.setState({
+ title: i18n('addServerDialogTitle'),
+ error: error || null,
+ view: 'default',
+ connecting: false,
+ success: false,
+ });
+
+ return true;
+ }
+
+ if (isConnectingView) {
+ // TODO: Make this not hard coded
+ const channelId = 1;
+ const serverURL = String(
+ $('.session-modal #server-url').val()
+ ).toLowerCase();
+
+ const serverURLExists = serverURL.length > 0;
+
+ if (!serverURLExists) {
+ this.setState({
+ error: i18n('noServerURL'),
+ view: 'default',
+ });
+
+ return false;
+ }
+
+ this.setState({
+ title: i18n('connectingLoad'),
+ serverURL: serverURL,
+ view: 'connecting',
+ connecting: true,
+ error: null,
+ });
+
+ const connectionResult = this.attemptConnection(serverURL, channelId);
+
+ // Give 5s maximum for promise to revole. Else, throw error.
+ const maxConnectionDuration = 5000;
+ const connectionTimeout = setTimeout(() => {
+ if (!this.state.success) {
+ this.showView('default', i18n('connectToServerFail'));
+
+ return;
+ }
+ }, maxConnectionDuration);
+
+ connectionResult
+ .then(() => {
+ clearTimeout(connectionTimeout);
+
+ if (this.state.connecting) {
+ this.setState({
+ success: true,
+ });
+ window.pushToast({
+ title: i18n('connectToServerSuccess'),
+ id: 'connectToServerSuccess',
+ type: 'success',
+ });
+ this.closeDialog();
+ }
+ })
+ .catch((connectionError: string) => {
+ clearTimeout(connectionTimeout);
+ this.showView('default', connectionError);
+
+ return false;
+ });
+ }
+
+ return true;
+ }
+
+ private showError() {
+ const message = this.state.error;
+
+ return (
+ <>
+ {message && (
+ <>
+ {message}
+
+ >
+ )}
+ >
+ );
+ }
+
+ private onKeyUp(event: any) {
+ switch (event.key) {
+ case 'Enter':
+ if (this.state.view === 'default') {
+ this.showView('connecting');
+ }
+ break;
+ case 'Esc':
+ case 'Escape':
+ this.closeDialog();
+ break;
+ default:
+ }
+ }
+
+ private closeDialog() {
+ window.removeEventListener('keyup', this.onKeyUp);
+ this.props.onClose();
+ }
+
+ private async attemptConnection(serverURL: string, channelId: number) {
+ const { i18n } = this.props;
+
+ const rawserverURL = serverURL
+ .replace(/^https?:\/\//i, '')
+ .replace(/[/\\]+$/i, '');
+ const sslserverURL = `https://${rawserverURL}`;
+ const conversationId = `publicChat:${channelId}@${rawserverURL}`;
+
+ const conversationExists = window.ConversationController.get(
+ conversationId
+ );
+ if (conversationExists) {
+ // We are already a member of this public chat
+ return new Promise((_resolve, reject) => {
+ reject(i18n('publicChatExists'));
+ });
+ }
+
+ const serverAPI = await window.lokiPublicChatAPI.findOrCreateServer(
+ sslserverURL
+ );
+ if (!serverAPI) {
+ // Url incorrect or server not compatible
+ return new Promise((_resolve, reject) => {
+ reject(i18n('connectToServerFail'));
+ });
+ }
+
+ const conversation = await window.ConversationController.getOrCreateAndWait(
+ conversationId,
+ 'group'
+ );
+
+ await serverAPI.findOrCreateChannel(channelId, conversationId);
+ await conversation.setPublicSource(sslserverURL, channelId);
+ await conversation.setFriendRequestStatus(
+ window.friends.friendRequestStatusEnum.friends
+ );
+
+ return conversation;
+ }
+}
diff --git a/ts/components/DevicePairingDialog.tsx b/ts/components/DevicePairingDialog.tsx
new file mode 100644
index 000000000..e855c7374
--- /dev/null
+++ b/ts/components/DevicePairingDialog.tsx
@@ -0,0 +1,273 @@
+import React from 'react';
+import { QRCode } from 'react-qrcode';
+
+import { SessionModal } from './session/SessionModal';
+import { SessionButton } from './session/SessionButton';
+
+interface Props {
+ i18n: any;
+ onClose: any;
+ pubKeyToUnpair: string | null;
+ pubKey: string | null;
+}
+
+interface State {
+ currentPubKey: string | null;
+ accepted: boolean;
+ isListening: boolean;
+ success: boolean;
+ loading: boolean;
+ view:
+ | 'default'
+ | 'waitingForRequest'
+ | 'requestReceived'
+ | 'requestAccepted'
+ | 'confirmUnpair';
+ pubKeyRequests: Array;
+ data: Array;
+}
+
+export class DevicePairingDialog extends React.Component {
+ constructor(props: any) {
+ super(props);
+
+ this.closeDialog = this.closeDialog.bind(this);
+ this.onKeyUp = this.onKeyUp.bind(this);
+ this.startReceivingRequests = this.startReceivingRequests.bind(this);
+ this.stopReceivingRequests = this.stopReceivingRequests.bind(this);
+ this.getPubkeyName = this.getPubkeyName.bind(this);
+
+ this.state = {
+ currentPubKey: this.props.pubKey,
+ accepted: false,
+ isListening: false,
+ success: false,
+ loading: true,
+ view: 'default',
+ pubKeyRequests: [],
+ data: [],
+ };
+ }
+
+ public componentDidMount() {
+ this.getSecondaryDevices();
+ }
+
+ public render() {
+ const { i18n } = this.props;
+
+ const waitingForRequest = this.state.view === 'waitingForRequest';
+ const nothingPaired = this.state.data.length === 0;
+
+ const renderPairedDevices = this.state.data.map((pubKey: any) => {
+ const pubKeyInfo = this.getPubkeyName(pubKey);
+ const isFinalItem =
+ this.state.data[this.state.data.length - 1] === pubKey;
+
+ return (
+
+
+ {pubKeyInfo.deviceAlias}
+
+ Pairing Secret:{' '}
+ {pubKeyInfo.secretWords}
+
+ {!isFinalItem ?
: null}
+
+ );
+ });
+
+ return (
+ <>
+ {!this.state.loading && (
+ null}
+ onClose={this.closeDialog}
+ >
+ {waitingForRequest ? (
+
+
{i18n('waitingForDeviceToRegister')}
+
+ {i18n('pairNewDevicePrompt')}
+
+
+
+
+
+
+
+
+
+
+
+
+ ) : (
+ <>
+ {nothingPaired ? (
+
+
{i18n('noPairedDevices')}
+
+ ) : (
+
+ {renderPairedDevices}
+
+ )}
+
+
+
+
+
+ >
+ )}
+
+ )}
+ >
+ );
+ }
+
+ private showView(
+ view?:
+ | 'default'
+ | 'waitingForRequest'
+ | 'requestReceived'
+ | 'requestAccepted'
+ | 'confirmUnpair'
+ ) {
+ if (!view) {
+ this.setState({
+ view: 'default',
+ });
+
+ return;
+ }
+
+ if (view === 'waitingForRequest') {
+ this.setState({
+ view,
+ isListening: true,
+ });
+
+ return;
+ }
+ this.setState({ view });
+ }
+
+ private getSecondaryDevices() {
+ const secondaryDevices = window.libloki.storage
+ .getSecondaryDevicesFor(this.state.currentPubKey)
+ .then(() => {
+ this.setState({
+ data: secondaryDevices,
+ loading: false,
+ });
+ });
+ }
+
+ private startReceivingRequests() {
+ this.showView('waitingForRequest');
+ }
+
+ private getPubkeyName(pubKey: string | null) {
+ if (!pubKey) {
+ return {};
+ }
+
+ const secretWords = window.mnemonic.pubkey_to_secret_words(pubKey);
+ const conv = window.ConversationController.get(this.state.currentPubKey);
+ const deviceAlias = conv ? conv.getNickname() : 'Unnamed Device';
+
+ return { deviceAlias, secretWords };
+ }
+
+ private stopReceivingRequests() {
+ if (this.state.success) {
+ const aliasKey = 'deviceAlias';
+ const deviceAlias = this.getPubkeyName(this.state.currentPubKey)[
+ aliasKey
+ ];
+
+ const conv = window.ConversationController.get(this.state.currentPubKey);
+ if (conv) {
+ conv.setNickname(deviceAlias);
+ }
+ }
+
+ this.showView();
+ }
+
+ private requestReceived(secondaryDevicePubKey: string | EventHandlerNonNull) {
+ // FIFO: push at the front of the array with unshift()
+ this.state.pubKeyRequests.unshift(secondaryDevicePubKey);
+ if (!this.state.currentPubKey) {
+ this.nextPubKey();
+
+ this.showView('requestReceived');
+ }
+ }
+
+ private allowDevice() {
+ this.setState({
+ accepted: true,
+ });
+ window.Whisper.trigger(
+ 'devicePairingRequestAccepted',
+ this.state.currentPubKey,
+ (errors: any) => {
+ this.transmisssionCB(errors);
+
+ return true;
+ }
+ );
+ this.showView();
+ }
+
+ private transmisssionCB(errors: any) {
+ if (!errors) {
+ this.setState({
+ success: true,
+ });
+ } else {
+ return;
+ }
+ }
+
+ private skipDevice() {
+ window.Whisper.trigger(
+ 'devicePairingRequestRejected',
+ this.state.currentPubKey
+ );
+ this.nextPubKey();
+ this.showView();
+ }
+
+ private nextPubKey() {
+ // FIFO: pop at the back of the array using pop()
+ const pubKeyRequests = this.state.pubKeyRequests;
+ this.setState({
+ currentPubKey: pubKeyRequests.pop(),
+ });
+ }
+
+ private onKeyUp(event: any) {
+ switch (event.key) {
+ case 'Esc':
+ case 'Escape':
+ this.closeDialog();
+ break;
+ default:
+ }
+ }
+
+ private closeDialog() {
+ window.removeEventListener('keyup', this.onKeyUp);
+ this.stopReceivingRequests();
+ this.props.onClose();
+ }
+}
diff --git a/ts/components/UserDetailsDialog.tsx b/ts/components/UserDetailsDialog.tsx
index 99ca613bb..7a1ea4856 100644
--- a/ts/components/UserDetailsDialog.tsx
+++ b/ts/components/UserDetailsDialog.tsx
@@ -10,6 +10,7 @@ import {
interface Props {
i18n: any;
+ isRss: boolean;
profileName: string;
avatarPath: string;
avatarColor: string;
@@ -29,7 +30,7 @@ export class UserDetailsDialog extends React.Component {
}
public render() {
- const i18n = this.props.i18n;
+ const { i18n, isRss } = this.props;
return (
{
{this.props.pubkey}
-
+ {!isRss && (
+
+ )}
);
diff --git a/ts/components/session/SessionConfirm.tsx b/ts/components/session/SessionConfirm.tsx
new file mode 100644
index 000000000..b473e6d34
--- /dev/null
+++ b/ts/components/session/SessionConfirm.tsx
@@ -0,0 +1,60 @@
+import React from 'react';
+import { SessionModal } from './SessionModal';
+import { SessionButton } from './SessionButton';
+
+interface Props {
+ message: string;
+ title: string;
+ onOk?: any;
+ onClose?: any;
+ onClickOk: any;
+ onClickClose: any;
+ okText?: string;
+ cancelText?: string;
+ hideCancel: boolean;
+}
+
+export class SessionConfirm extends React.Component {
+ public static defaultProps = {
+ title: '',
+ hideCancel: false,
+ };
+
+ constructor(props: any) {
+ super(props);
+ }
+
+ public render() {
+ const { title, message, onClickOk, onClickClose, hideCancel } = this.props;
+
+ const okText = this.props.okText || window.i18n('ok');
+ const cancelText = this.props.cancelText || window.i18n('cancel');
+ const showHeader = !!this.props.title;
+
+ return (
+ null}
+ onOk={() => null}
+ showExitIcon={false}
+ showHeader={showHeader}
+ >
+ {!showHeader && }
+
+
+ {message}
+
+
+
+
+
+
+
+ {!hideCancel && (
+
+ )}
+
+
+ );
+ }
+}
diff --git a/ts/components/session/SessionModal.tsx b/ts/components/session/SessionModal.tsx
index 9977046d7..b77d0aba3 100644
--- a/ts/components/session/SessionModal.tsx
+++ b/ts/components/session/SessionModal.tsx
@@ -1,5 +1,4 @@
import React from 'react';
-import classNames from 'classnames';
import { SessionIconButton, SessionIconSize, SessionIconType } from './icon/';
@@ -7,6 +6,8 @@ interface Props {
title: string;
onClose: any;
onOk: any;
+ showExitIcon?: boolean;
+ showHeader?: boolean;
//Maximum of two icons in header
headerIconButtons?: Array<{
type: SessionIconType;
@@ -19,6 +20,11 @@ interface State {
}
export class SessionModal extends React.PureComponent {
+ public static defaultProps = {
+ showExitIcon: true,
+ showHeader: true,
+ };
+
constructor(props: any) {
super(props);
this.state = {
@@ -32,34 +38,40 @@ export class SessionModal extends React.PureComponent {
}
public render() {
- const { title, headerIconButtons } = this.props;
+ const { title, headerIconButtons, showExitIcon, showHeader } = this.props;
const { isVisible } = this.state;
return isVisible ? (
-
-
-
-
-
-
{title}
-
- {headerIconButtons
- ? headerIconButtons.map((iconItem: any) => {
- return (
-
- );
- })
- : null}
-
-
+
+ {showHeader ? (
+ <>
+
+
+ {showExitIcon ? (
+
+ ) : null}
+
+
{title}
+
+ {headerIconButtons
+ ? headerIconButtons.map((iconItem: any) => {
+ return (
+
+ );
+ })
+ : null}
+
+
+ >
+ ) : null}
{this.props.children}
diff --git a/ts/components/session/SessionSpinner.tsx b/ts/components/session/SessionSpinner.tsx
new file mode 100644
index 000000000..dd33e0688
--- /dev/null
+++ b/ts/components/session/SessionSpinner.tsx
@@ -0,0 +1,32 @@
+import React from 'react';
+
+interface Props {
+ loading: boolean;
+}
+
+export class SessionSpinner extends React.Component
{
+ public static defaultProps = {
+ loading: true,
+ };
+
+ constructor(props: any) {
+ super(props);
+ }
+
+ public render() {
+ const { loading } = this.props;
+
+ return (
+ <>
+ {loading ? (
+
+ ) : null}
+ >
+ );
+ }
+}
diff --git a/ts/global.d.ts b/ts/global.d.ts
index 3a85cc274..9e1d3dbc4 100644
--- a/ts/global.d.ts
+++ b/ts/global.d.ts
@@ -4,6 +4,7 @@ interface Window {
passwordUtil: any;
dcodeIO: any;
libsignal: any;
+ libloki: any;
displayNameRegex: any;
Signal: any;
Whisper: any;
@@ -12,7 +13,9 @@ interface Window {
textsecure: any;
Session: any;
i18n: any;
+ friends: any;
generateID: any;
+ pushToast: any;
}
interface Promise {
diff --git a/yarn.lock b/yarn.lock
index 75bf08394..9a1347020 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -139,9 +139,9 @@
integrity sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==
"@types/dompurify@^2.0.0":
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-2.0.0.tgz#9616caa5bf2569aea2e4889d4f929d968c081b40"
- integrity sha512-g/ilp+Bo6Ljy60i5LnjkGw00X7EIoFjoPGlxqZhV8TJ9fWEzXheioU1O+U/UzCzUA7pUDy/JNMytTQDJctpUHg==
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-2.0.1.tgz#0bf3a9f8ee21d81adb20b8c374ab034d6a74dbf7"
+ integrity sha512-OQ16dECrRv/I//woKkVUxyVGYR94W3qp3Wy//B63awHVe3h/1/URFqP5a/V2m4k01DEvWs1+z7FWW3xfM1lH3Q==
dependencies:
"@types/trusted-types" "*"
@@ -1243,6 +1243,19 @@ buble@^0.19.3:
os-homedir "^1.0.1"
vlq "^1.0.0"
+buffer-alloc-unsafe@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0"
+ integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==
+
+buffer-alloc@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec"
+ integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==
+ dependencies:
+ buffer-alloc-unsafe "^1.1.0"
+ buffer-fill "^1.0.0"
+
buffer-crc32@0.2.13, buffer-crc32@^0.2.1:
version "0.2.13"
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
@@ -1252,10 +1265,20 @@ buffer-equal@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-0.0.1.tgz#91bc74b11ea405bc916bc6aa908faafa5b4aac4b"
+buffer-fill@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
+ integrity sha1-+PeLdniYiO858gXNY39o5wISKyw=
+
buffer-from@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.0.0.tgz#4cb8832d23612589b0406e9e2956c17f06fdf531"
+buffer-from@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
+ integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
+
buffer-indexof@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c"
@@ -1272,6 +1295,14 @@ buffer@^4.3.0:
ieee754 "^1.1.4"
isarray "^1.0.0"
+buffer@^5.4.3:
+ version "5.4.3"
+ resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.4.3.tgz#3fbc9c69eb713d323e3fc1a895eee0710c072115"
+ integrity sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A==
+ dependencies:
+ base64-js "^1.0.2"
+ ieee754 "^1.1.4"
+
buffers@~0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb"
@@ -2522,6 +2553,11 @@ diffie-hellman@^5.0.0:
miller-rabin "^4.0.0"
randombytes "^2.0.0"
+dijkstrajs@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.1.tgz#d3cd81221e3ea40742cfcde556d4e99e98ddc71b"
+ integrity sha1-082BIh4+pAdCz83lVtTpnpjdxxs=
+
dir-glob@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.0.0.tgz#0b205d2b6aef98238ca286598a8204d29d0a0034"
@@ -4982,6 +5018,11 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+isarray@^2.0.1:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
+ integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
+
isbinaryfile@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.2.tgz#bfc45642da645681c610cca831022e30af426488"
@@ -6981,6 +7022,11 @@ pngjs@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.0.1.tgz#b15086ac1ac47298c8fd3f9cdf364fa9879c4db6"
+pngjs@^3.3.0:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f"
+ integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==
+
portfinder@^1.0.9:
version "1.0.13"
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.13.tgz#bb32ecd87c27104ae6ee44b5a3ccbf0ebb1aede9"
@@ -7474,6 +7520,19 @@ q@^1.1.2, q@~1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/q/-/q-1.5.0.tgz#dd01bac9d06d30e6f219aecb8253ee9ebdc308f1"
+qrcode@^1.4.4:
+ version "1.4.4"
+ resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.4.4.tgz#f0c43568a7e7510a55efc3b88d9602f71963ea83"
+ integrity sha512-oLzEC5+NKFou9P0bMj5+v6Z40evexeE29Z9cummZXZ9QXyMr3lphkURzxjXgPJC5azpxcshoDWV1xE46z+/c3Q==
+ dependencies:
+ buffer "^5.4.3"
+ buffer-alloc "^1.2.0"
+ buffer-from "^1.1.1"
+ dijkstrajs "^1.0.1"
+ isarray "^2.0.1"
+ pngjs "^3.3.0"
+ yargs "^13.2.4"
+
qs@5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-5.2.0.tgz#a9f31142af468cb72b25b30136ba2456834916be"
@@ -7699,6 +7758,11 @@ react-portal@^4.2.0:
dependencies:
prop-types "^15.5.8"
+react-qrcode@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/react-qrcode/-/react-qrcode-0.2.0.tgz#a05cf2ae5ac57c3a9751e512132a821ed60533f9"
+ integrity sha512-3JzSzkTUUMb26sbq5/u75zw9l3gQ1BLvdCAYgRnAZ1wGJj1Su94pzv4g/XfzaJEj6h8Y0H9mYX4djmKBGZQHSQ==
+
react-redux@6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-6.0.1.tgz#0d423e2c1cb10ada87293d47e7de7c329623ba4d"
@@ -10374,7 +10438,7 @@ yargs@^10.0.3:
y18n "^3.2.1"
yargs-parser "^8.0.0"
-yargs@^13.3.0:
+yargs@^13.2.4, yargs@^13.3.0:
version "13.3.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83"
integrity sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==