Modals redesign and SessionToast improvements (#713)

Modals redesign and SessionToast improvements
pull/737/head
Audric Ackermann 5 years ago committed by GitHub
commit ea0ab2c7cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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"
},

@ -52,6 +52,7 @@
<script type='text/x-tmpl-mustache' id='two-column'>
<div id='session-toast-container'></div>
<div id='session-confirm-container'></div>
<div class='gutter'>
<div class='network-status-container'></div>
<div class='left-pane-placeholder'></div>
@ -704,6 +705,7 @@
<script type='text/javascript' src='js/views/session_toggle_view.js'></script>
<script type='text/javascript' src='js/views/session_modal_view.js'></script>
<script type='text/javascript' src='js/views/session_dropdown_view.js'></script>
<script type='text/javascript' src='js/views/session_confirm_view.js'></script>
<script type='text/javascript' src='js/views/file_input_view.js'></script>
<script type='text/javascript' src='js/views/list_view.js'></script>
<script type='text/javascript' src='js/views/contact_list_view.js'></script>

@ -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);
},

@ -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() {

@ -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();

@ -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,

@ -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);

@ -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;
}
},
});
})();

@ -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) {

@ -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: <i>${secretWords}</i>)`;
},
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 = $('<li>').html(name);
if (window.lokiFeatureFlags.multiDeviceUnpairing) {
const link = $('<a>')
.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('<li>No paired devices</li>');
}
} 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');
},
});
})();

@ -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);
},
});

@ -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);

@ -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();
}
},
});
})();

@ -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() {

@ -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,
},

@ -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);
});
},
},

@ -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",

@ -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);
}
}
}

@ -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<Props, State> {
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 (
<SessionModal
title={this.state.title}
onOk={() => null}
onClose={this.closeDialog}
>
{this.state.view === 'default' && (
<>
<div className="spacer-lg" />
<input
type="text"
id="server-url"
placeholder={i18n('serverURL')}
defaultValue={this.state.serverURL}
/>
<div className="spacer-sm" />
{this.showError()}
<div className="session-modal__button-group">
<SessionButton
text={i18n('connect')}
onClick={() => this.showView('connecting')}
/>
<SessionButton text={i18n('cancel')} onClick={this.closeDialog} />
</div>
</>
)}
{this.state.view === 'connecting' && (
<>
<div className="session-modal__centered">
<div className="spacer-lg" />
<SessionSpinner />
<div className="spacer-lg" />
</div>
<div className="session-modal__button-group">
<SessionButton
text={i18n('cancel')}
onClick={() => this.showView('default')}
/>
</div>
</>
)}
</SessionModal>
);
}
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 && (
<>
<div className="session-label danger">{message}</div>
<div className="spacer-lg" />
</>
)}
</>
);
}
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;
}
}

@ -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<any>;
data: Array<any>;
}
export class DevicePairingDialog extends React.Component<Props, State> {
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 (
<div key={pubKey}>
<p>
{pubKeyInfo.deviceAlias}
<br />
<span className="text-subtle">Pairing Secret:</span>{' '}
{pubKeyInfo.secretWords}
</p>
{!isFinalItem ? <hr className="text-soft fullwidth" /> : null}
</div>
);
});
return (
<>
{!this.state.loading && (
<SessionModal
title={i18n('pairedDevices')}
onOk={() => null}
onClose={this.closeDialog}
>
{waitingForRequest ? (
<div className="session-modal__centered">
<h3>{i18n('waitingForDeviceToRegister')}</h3>
<small className="text-subtle">
{i18n('pairNewDevicePrompt')}
</small>
<div className="spacer-lg" />
<div id="qr">
<QRCode value={window.textsecure.storage.user.getNumber()} />
</div>
<div className="spacer-lg" />
<div className="session-modal__button-group__center">
<SessionButton
text={i18n('cancel')}
onClick={this.stopReceivingRequests}
/>
</div>
</div>
) : (
<>
{nothingPaired ? (
<div className="session-modal__centered">
<div>{i18n('noPairedDevices')}</div>
</div>
) : (
<div className="session-modal__centered">
{renderPairedDevices}
</div>
)}
<div className="spacer-lg" />
<div className="session-modal__button-group__center">
<SessionButton
text={i18n('pairNewDevice')}
onClick={this.startReceivingRequests}
/>
</div>
</>
)}
</SessionModal>
)}
</>
);
}
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();
}
}

@ -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<Props> {
}
public render() {
const i18n = this.props.i18n;
const { i18n, isRss } = this.props;
return (
<SessionModal
@ -43,12 +44,14 @@ export class UserDetailsDialog extends React.Component<Props> {
<div className="message">{this.props.pubkey}</div>
<div className="session-modal__button-group__center">
<SessionButton
text={i18n('startConversation')}
buttonType={SessionButtonType.Default}
buttonColor={SessionButtonColor.Primary}
onClick={this.onClickStartConversation}
/>
{!isRss && (
<SessionButton
text={i18n('startConversation')}
buttonType={SessionButtonType.Default}
buttonColor={SessionButtonColor.Primary}
onClick={this.onClickStartConversation}
/>
)}
</div>
</SessionModal>
);

@ -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<Props> {
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 (
<SessionModal
title={title}
onClose={() => null}
onOk={() => null}
showExitIcon={false}
showHeader={showHeader}
>
{!showHeader && <div className="spacer-lg" />}
<div className="session-modal__centered">
<span className="text-subtle">{message}</span>
</div>
<div className="spacer-lg" />
<div className="session-modal__button-group">
<SessionButton text={okText} onClick={onClickOk} />
{!hideCancel && (
<SessionButton text={cancelText} onClick={onClickClose} />
)}
</div>
</SessionModal>
);
}
}

@ -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<Props, State> {
public static defaultProps = {
showExitIcon: true,
showHeader: true,
};
constructor(props: any) {
super(props);
this.state = {
@ -32,34 +38,40 @@ export class SessionModal extends React.PureComponent<Props, State> {
}
public render() {
const { title, headerIconButtons } = this.props;
const { title, headerIconButtons, showExitIcon, showHeader } = this.props;
const { isVisible } = this.state;
return isVisible ? (
<div className={classNames('session-modal')}>
<div className="session-modal__header">
<div className="session-modal__header__close">
<SessionIconButton
iconType={SessionIconType.Exit}
iconSize={SessionIconSize.Small}
onClick={this.close}
/>
</div>
<div className="session-modal__header__title">{title}</div>
<div className="session-modal__header__icons">
{headerIconButtons
? headerIconButtons.map((iconItem: any) => {
return (
<SessionIconButton
key={iconItem.type}
iconType={iconItem.type}
iconSize={SessionIconSize.Medium}
/>
);
})
: null}
</div>
</div>
<div className={'session-modal'}>
{showHeader ? (
<>
<div className="session-modal__header">
<div className="session-modal__header__close">
{showExitIcon ? (
<SessionIconButton
iconType={SessionIconType.Exit}
iconSize={SessionIconSize.Small}
onClick={this.close}
/>
) : null}
</div>
<div className="session-modal__header__title">{title}</div>
<div className="session-modal__header__icons">
{headerIconButtons
? headerIconButtons.map((iconItem: any) => {
return (
<SessionIconButton
key={iconItem.type}
iconType={iconItem.type}
iconSize={SessionIconSize.Medium}
/>
);
})
: null}
</div>
</div>
</>
) : null}
<div className="session-modal__body">{this.props.children}</div>
</div>

@ -0,0 +1,32 @@
import React from 'react';
interface Props {
loading: boolean;
}
export class SessionSpinner extends React.Component<Props> {
public static defaultProps = {
loading: true,
};
constructor(props: any) {
super(props);
}
public render() {
const { loading } = this.props;
return (
<>
{loading ? (
<div className="session-loader">
<div />
<div />
<div />
<div />
</div>
) : null}
</>
);
}
}

3
ts/global.d.ts vendored

@ -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<T> {

@ -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==

Loading…
Cancel
Save