Move confirmation_dialog_view to ts and React

* Moves confirmation_dialog_view to ts and React

* showConfirmationDialog API
This commit is contained in:
Josh Perez 2021-01-04 13:47:14 -05:00 committed by Scott Nonnenberg
parent 031a1fcc3d
commit 2529e208c1
16 changed files with 154 additions and 254 deletions

View File

@ -114,18 +114,6 @@
<button class='finish' tabIndex='1'><span class='icon'></span></button>
</script>
<script type='text/x-tmpl-mustache' id='confirmation-dialog'>
<div class="content">
<div class='message'>{{ message }}</div>
<div class='buttons'>
<button class='ok' tabindex='2'>{{ ok }}</button>
{{ #showCancel }}
<button class='cancel' tabindex='1'>{{ cancel }}</button>
{{ /showCancel }}
</div>
</div>
</script>
<script type='text/x-tmpl-mustache' id='safety-number-change-dialog'>
<div class='safety-number-change-dialog-wrapper'></div>
</script>
@ -361,7 +349,7 @@
<script type='text/javascript' src='js/views/recorder_view.js'></script>
<script type='text/javascript' src='ts/views/conversation_view.js'></script>
<script type='text/javascript' src='js/views/inbox_view.js'></script>
<script type='text/javascript' src='js/views/confirmation_dialog_view.js'></script>
<script type='text/javascript' src='ts/shims/showConfirmationDialog.js'></script>
<script type='text/javascript' src='js/views/identicon_svg_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>

View File

@ -1,7 +1,7 @@
// Copyright 2018-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* global $, Whisper, i18n */
/* global $, i18n */
$(document).on('keydown', e => {
if (e.keyCode === 27) {
@ -35,7 +35,8 @@ if (window.forCalling) {
message = i18n('audioPermissionNeeded');
}
window.view = new Whisper.ConfirmationDialogView({
window.showConfirmationDialog({
confirmStyle: 'affirmative',
message,
okText: i18n('allowAccess'),
resolve: () => {
@ -48,5 +49,3 @@ window.view = new Whisper.ConfirmationDialogView({
},
reject: window.closePermissionsPopup,
});
window.view.$el.appendTo($body);

View File

@ -1,79 +0,0 @@
// Copyright 2015-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* global Backbone, Whisper, i18n */
// eslint-disable-next-line func-names
(function () {
window.Whisper = window.Whisper || {};
Whisper.ConfirmationDialogView = Whisper.View.extend({
className: 'confirmation-dialog modal',
templateName: 'confirmation-dialog',
initialize(options) {
this.previousFocus = document.activeElement;
this.message = options.message;
this.hideCancel = options.hideCancel;
this.resolve = options.resolve;
this.okText = options.okText || i18n('ok');
this.reject = options.reject;
this.cancelText = options.cancelText || i18n('cancel');
if (Whisper.activeConfirmationView) {
Whisper.activeConfirmationView.remove();
Whisper.activeConfirmationView = null;
}
Whisper.activeConfirmationView = this;
this.render();
},
events: {
keydown: 'onKeydown',
'click .ok': 'ok',
'click .cancel': 'cancel',
},
remove() {
if (this.previousFocus && this.previousFocus.focus) {
this.previousFocus.focus();
}
Backbone.View.prototype.remove.call(this);
},
render_attributes() {
return {
message: this.message,
showCancel: !this.hideCancel,
cancel: this.cancelText,
ok: this.okText,
};
},
ok() {
this.remove();
if (this.resolve) {
this.resolve();
}
},
cancel() {
this.remove();
if (this.reject) {
this.reject(new Error('User clicked cancel button'));
}
},
onKeydown(event) {
if (event.key === 'Escape' || event.key === 'Esc') {
this.cancel();
event.preventDefault();
event.stopPropagation();
}
},
focusCancel() {
// We delay this call because we might be called inside click handlers
// which would set focus to themselves afterwards!
setTimeout(() => this.$('.cancel').focus(), 1);
},
});
})();

View File

@ -54,14 +54,12 @@
},
confirm(message, okText) {
return new Promise((resolve, reject) => {
const dialog = new Whisper.ConfirmationDialogView({
window.showConfirmationDialog({
message,
okText,
resolve,
reject,
});
this.$el.append(dialog.el);
dialog.focusCancel();
});
},
},

View File

@ -23,20 +23,9 @@
</head>
<body class='permissions-popup'>
</body>
<script type='text/x-tmpl-mustache' id='confirmation-dialog'>
<div class="content">
<div class='message'>{{ message }}</div>
<div class='buttons'>
{{ #showCancel }}
<button class='cancel' tabindex='2'>{{ cancel }}</button>
{{ /showCancel }}
<button class='ok' tabindex='1'>{{ ok }}</button>
</div>
</div>
</script>
<script type='text/javascript' src='js/components.js'></script>
<script type='text/javascript' src='ts/backboneJquery.js'></script>
<script type='text/javascript' src='js/views/whisper_view.js'></script>
<script type='text/javascript' src='js/views/confirmation_dialog_view.js'></script>
<script type='text/javascript' src='ts/shims/showConfirmationDialog.js'></script>
<script type='text/javascript' src='js/permissions_popup_start.js'></script>
</html>

View File

@ -3,9 +3,13 @@
/* global window */
window.React = require('react');
window.ReactDOM = require('react-dom');
const { ipcRenderer, remote } = require('electron');
const url = require('url');
const i18n = require('./js/modules/i18n');
const { ConfirmationModal } = require('./ts/components/ConfirmationModal');
const { makeGetter, makeSetter } = require('./preload_utils');
const { nativeTheme } = remote.require('electron');
@ -20,6 +24,11 @@ window.theme = config.theme;
window.i18n = i18n.setup(locale, localeMessages);
window.forCalling = config.forCalling === 'true';
window.forCamera = config.forCamera === 'true';
window.Signal = {
Components: {
ConfirmationModal,
},
};
function setSystemTheme() {
window.systemTheme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light';

View File

@ -292,79 +292,10 @@
cursor: pointer;
}
.confirmation-dialog {
.content {
max-width: 350px;
margin: 100px auto;
padding: 1em;
border-radius: 5px;
overflow: auto;
@include light-theme {
background: $color-white;
box-shadow: 0px 0px 15px 0px $color-black-alpha-20;
}
@include dark-theme {
background: $color-black;
color: $color-gray-02;
box-shadow: 0px 0px 15px 0px $color-white-alpha-20;
}
.buttons {
margin-top: 10px;
button {
float: right;
margin-left: 10px;
padding: 5px 8px;
border-radius: 5px;
outline: none;
@include keyboard-mode {
&:focus {
outline: -webkit-focus-ring-color auto 5px;
}
}
@include light-theme {
background-color: $color-gray-02;
border: 1px solid $color-gray-15;
}
@include dark-theme {
background-color: $color-gray-90;
border: 1px solid $color-gray-45;
color: $color-gray-02;
}
&:hover {
@include light-theme {
background-color: $color-gray-15;
border-color: $color-gray-25;
}
@include dark-theme {
background-color: $color-gray-75;
border-color: $color-gray-45;
}
}
}
}
}
}
.permissions-popup,
.debug-log-window {
.modal {
background-color: transparent;
padding: 0;
}
.confirmation-dialog .content {
box-shadow: 0px 0px 0px 0px;
max-width: 1000px;
margin: 0;
margin-left: auto;
margin-right: auto;
margin-top: 15px;
}
}

View File

@ -8308,7 +8308,8 @@ button.module-image__border-overlay:focus {
display: flex;
justify-content: center;
align-items: center;
z-index: 5;
// THIS Z-INDEX IS OVER NINE THOUSAND. OVER NINE THOUSAND?! THAT CAN'T BE!
z-index: 9001;
}
&__container {

View File

@ -125,16 +125,8 @@
<button class='close'><span class='icon'></span></button>
</script>
<script type='text/x-tmpl-mustache' id='confirmation-dialog'>
<div class="content">
<div class='message'>{{ message }}</div>
<div class='buttons'>
{{ #showCancel }}
<button class='cancel' tabindex='2'>{{ cancel }}</button>
{{ /showCancel }}
<button class='ok' tabindex='1'>{{ ok }}</button>
</div>
</div>
<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'>

View File

@ -241,15 +241,14 @@ type WhatIsThis = import('./window.d').WhatIsThis;
try {
await new Promise((resolve, reject) => {
const dialog = new window.Whisper.ConfirmationDialogView({
window.showConfirmationDialog({
cancelText: window.i18n('quit'),
confirmStyle: 'negative',
message: window.i18n('deleteOldIndexedDBData'),
okText: window.i18n('deleteOldData'),
cancelText: window.i18n('quit'),
resolve,
reject,
reject: () => reject(),
resolve: () => resolve(),
});
document.body.append(dialog.el);
dialog.focusCancel();
});
} catch (error) {
window.log.info(

View File

@ -12,9 +12,8 @@ import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages);
storiesOf('Components/ConfirmationDialog', module).add(
'ConfirmationDialog',
() => {
storiesOf('Components/ConfirmationDialog', module)
.add('ConfirmationDialog', () => {
return (
<ConfirmationDialog
i18n={i18n}
@ -36,5 +35,23 @@ storiesOf('Components/ConfirmationDialog', module).add(
{text('Child text', 'asdf blip')}
</ConfirmationDialog>
);
}
);
})
.add('Custom cancel text', () => {
return (
<ConfirmationDialog
cancelText="Nah"
i18n={i18n}
onClose={action('onClose')}
title={text('Title', 'Foo bar banana baz?')}
actions={[
{
text: 'Maybe',
style: 'affirmative',
action: action('affirmative'),
},
]}
>
{text('Child text', 'asdf blip')}
</ConfirmationDialog>
);
});

View File

@ -12,11 +12,12 @@ export type ActionSpec = {
};
export type OwnProps = {
readonly i18n: LocalizerType;
readonly children: React.ReactNode;
readonly title?: string | React.ReactNode;
readonly actions: Array<ActionSpec>;
readonly cancelText?: string;
readonly children?: React.ReactNode;
readonly i18n: LocalizerType;
readonly onClose: () => unknown;
readonly title?: string | React.ReactNode;
};
export type Props = OwnProps;
@ -28,7 +29,7 @@ function focusRef(el: HTMLElement | null) {
}
export const ConfirmationDialog = React.memo(
({ i18n, onClose, children, title, actions }: Props) => {
({ i18n, onClose, cancelText, children, title, actions }: Props) => {
React.useEffect(() => {
const handler = ({ key }: KeyboardEvent) => {
if (key === 'Escape') {
@ -81,7 +82,7 @@ export const ConfirmationDialog = React.memo(
ref={focusRef}
className="module-confirmation-dialog__container__buttons__button"
>
{i18n('confirmation-dialog--Cancel')}
{cancelText || i18n('confirmation-dialog--Cancel')}
</button>
{actions.map((action, i) => (
<button

View File

@ -0,0 +1,77 @@
// Copyright 2015-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
// This file is here temporarily while we're switching off of Backbone into
// React. In the future, and in React-land, please just import and use
// ConfirmationModal directly. This is the thin API layer to bridge the gap
// while we convert things over. Please delete this file once all usages are
// ported over. Note: this file cannot have any imports/exports since it is
// being included in a <script /> tag.
type ConfirmationDialogViewProps = {
cancelText?: string;
confirmStyle?: 'affirmative' | 'negative';
message: string;
okText: string;
reject?: () => void;
resolve: () => void;
};
let confirmationDialogViewNode: HTMLElement | null = null;
let confirmationDialogPreviousFocus: HTMLElement | null = null;
function removeConfirmationDialog() {
if (!confirmationDialogViewNode) {
return;
}
window.ReactDOM.unmountComponentAtNode(confirmationDialogViewNode);
document.body.removeChild(confirmationDialogViewNode);
if (
confirmationDialogPreviousFocus &&
typeof confirmationDialogPreviousFocus.focus === 'function'
) {
confirmationDialogPreviousFocus.focus();
}
confirmationDialogViewNode = null;
}
function showConfirmationDialog(options: ConfirmationDialogViewProps) {
if (confirmationDialogViewNode) {
removeConfirmationDialog();
}
confirmationDialogViewNode = document.createElement('div');
document.body.appendChild(confirmationDialogViewNode);
confirmationDialogPreviousFocus = document.activeElement as HTMLElement;
window.ReactDOM.render(
// eslint-disable-next-line react/react-in-jsx-scope, react/jsx-no-undef
<window.Signal.Components.ConfirmationModal
actions={[
{
action: () => {
removeConfirmationDialog();
options.resolve();
},
style: options.confirmStyle,
text: options.okText || window.i18n('ok'),
},
]}
cancelText={options.cancelText || window.i18n('cancel')}
i18n={window.i18n}
onClose={() => {
removeConfirmationDialog();
if (options.reject) {
options.reject();
}
}}
title={options.message}
/>,
confirmationDialogViewNode
);
}
window.showConfirmationDialog = showConfirmationDialog;

View File

@ -286,15 +286,6 @@
"updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-appendTo(",
"path": "js/permissions_popup_start.js",
"line": "window.view.$el.appendTo($body);",
"lineNumber": 52,
"reasonCategory": "usageTrusted",
"updated": "2020-06-02T21:51:34.813Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
},
{
"rule": "jQuery-$(",
"path": "js/settings_start.js",
@ -357,15 +348,6 @@
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
},
{
"rule": "jQuery-$(",
"path": "js/views/confirmation_dialog_view.js",
"line": " setTimeout(() => this.$('.cancel').focus(), 1);",
"lineNumber": 76,
"reasonCategory": "usageTrusted",
"updated": "2019-12-07T02:04:56.987Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-append(",
"path": "js/views/contact_list_view.js",
@ -1359,20 +1341,11 @@
"updated": "2018-09-15T00:38:04.183Z",
"reasonDetail": "Value set came directly from Mustache tempating engine"
},
{
"rule": "jQuery-append(",
"path": "js/views/whisper_view.js",
"line": " this.$el.append(dialog.el);",
"lineNumber": 63,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
},
{
"rule": "jQuery-$(",
"path": "js/views/whisper_view.js",
"line": " $('script[type=\"text/x-tmpl-mustache\"]').each((i, el) => {",
"lineNumber": 72,
"lineNumber": 70,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "Protected from arbitrary input"
@ -1381,7 +1354,7 @@
"rule": "jQuery-$(",
"path": "js/views/whisper_view.js",
"line": " const $el = $(el);",
"lineNumber": 73,
"lineNumber": 71,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "Protected from arbitrary input"
@ -1390,7 +1363,7 @@
"rule": "jQuery-html(",
"path": "js/views/whisper_view.js",
"line": " templates[id] = $el.html();",
"lineNumber": 75,
"lineNumber": 73,
"reasonCategory": "usageTrusted",
"updated": "2018-09-15T00:38:04.183Z",
"reasonDetail": "Getting the value, not setting it"

View File

@ -2034,7 +2034,8 @@ Whisper.ConversationView = Whisper.View.extend({
this.$('.microphone').hide();
},
handleAudioConfirm(blob: any, lostFocus: any) {
const dialog = new Whisper.ConfirmationDialogView({
window.showConfirmationDialog({
confirmStyle: 'negative',
cancelText: window.i18n('discard'),
message: lostFocus
? window.i18n('voiceRecordingInterruptedBlur')
@ -2044,9 +2045,6 @@ Whisper.ConversationView = Whisper.View.extend({
await this.handleAudioCapture(blob);
},
});
this.$el.prepend(dialog.el);
dialog.focusCancel();
},
async handleAudioCapture(blob: any) {
if (this.hasFiles()) {
@ -2346,7 +2344,8 @@ Whisper.ConversationView = Whisper.View.extend({
throw new Error(`forceSend: Did not find message for id ${messageId}`);
}
const dialog = new Whisper.ConfirmationDialogView({
window.showConfirmationDialog({
confirmStyle: 'negative',
message: window.i18n('identityKeyErrorOnSend', {
name1: contact.getTitle(),
name2: contact.getTitle(),
@ -2367,9 +2366,6 @@ Whisper.ConversationView = Whisper.View.extend({
message.resend(contact.getSendTarget());
},
});
this.$el.prepend(dialog.el);
dialog.focusCancel();
},
showSafetyNumber(id: any) {
@ -2513,7 +2509,8 @@ Whisper.ConversationView = Whisper.View.extend({
);
}
const dialog = new Whisper.ConfirmationDialogView({
window.showConfirmationDialog({
confirmStyle: 'negative',
message: window.i18n('deleteWarning'),
okText: window.i18n('delete'),
resolve: () => {
@ -2530,9 +2527,6 @@ Whisper.ConversationView = Whisper.View.extend({
this.resetPanel();
},
});
this.$el.prepend(dialog.el);
dialog.focusCancel();
},
deleteMessageForEveryone(messageId: string) {
@ -2543,7 +2537,8 @@ Whisper.ConversationView = Whisper.View.extend({
);
}
const dialog = new Whisper.ConfirmationDialogView({
window.showConfirmationDialog({
confirmStyle: 'negative',
message: window.i18n('deleteForEveryoneWarning'),
okText: window.i18n('delete'),
resolve: async () => {
@ -2551,9 +2546,6 @@ Whisper.ConversationView = Whisper.View.extend({
this.resetPanel();
},
});
this.$el.prepend(dialog.el);
dialog.focusCancel();
},
showStickerPackPreview(packId: any, packKey: any) {

15
ts/window.d.ts vendored
View File

@ -64,6 +64,7 @@ import { MessageModel } from './models/messages';
import { ConversationModel } from './models/conversations';
import { combineNames } from './util';
import { BatcherType } from './util/batcher';
import { ConfirmationModal } from './components/ConfirmationModal';
import { ErrorModal } from './components/ErrorModal';
import { ProgressModal } from './components/ProgressModal';
import { ContactModal } from './components/conversation/ContactModal';
@ -74,6 +75,17 @@ type TaskResultType = any;
export type WhatIsThis = any;
// Synced with the type in ts/shims/showConfirmationDialog
// we are duplicating it here because that file cannot import/export.
type ConfirmationDialogViewProps = {
cancelText?: string;
confirmStyle?: 'affirmative' | 'negative';
message: string;
okText: string;
reject?: () => void;
resolve: () => void;
};
declare global {
interface Window {
_: typeof Underscore;
@ -160,6 +172,7 @@ declare global {
setAutoHideMenuBar: (value: WhatIsThis) => void;
setBadgeCount: (count: number) => void;
setMenuBarVisibility: (value: WhatIsThis) => void;
showConfirmationDialog: (options: ConfirmationDialogViewProps) => void;
showKeyboardShortcuts: () => void;
storage: {
addBlockedGroup: (group: string) => void;
@ -397,6 +410,7 @@ declare global {
Components: {
AttachmentList: any;
CaptionEditor: any;
ConfirmationModal: typeof ConfirmationModal;
ContactDetail: any;
ErrorModal: typeof ErrorModal;
ContactModal: typeof ContactModal;
@ -600,7 +614,6 @@ export type WhisperType = {
MessageType: MessageModel;
GroupMemberConversation: WhatIsThis;
KeyChangeListener: WhatIsThis;
ConfirmationDialogView: WhatIsThis;
ClearDataView: WhatIsThis;
ReactWrapperView: WhatIsThis;
activeConfirmationView: WhatIsThis;