Replaces ConfirmationModal with ConfirmationDialog

This commit is contained in:
Josh Perez 2021-04-27 12:29:59 -07:00 committed by GitHub
parent c9d74654bf
commit e75bba1c52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 456 additions and 737 deletions

View File

@ -29,7 +29,9 @@ const {
AttachmentList,
} = require('../../ts/components/conversation/AttachmentList');
const { CaptionEditor } = require('../../ts/components/CaptionEditor');
const { ConfirmationModal } = require('../../ts/components/ConfirmationModal');
const {
ConfirmationDialog,
} = require('../../ts/components/ConfirmationDialog');
const {
ContactDetail,
} = require('../../ts/components/conversation/ContactDetail');
@ -317,7 +319,7 @@ exports.setup = (options = {}) => {
const Components = {
AttachmentList,
CaptionEditor,
ConfirmationModal,
ConfirmationDialog,
ContactDetail,
ContactListItem,
ContactModal,

View File

@ -9,7 +9,7 @@ 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 { ConfirmationDialog } = require('./ts/components/ConfirmationDialog');
const { makeGetter, makeSetter } = require('./preload_utils');
const {
getEnvironment,
@ -32,7 +32,7 @@ window.forCalling = config.forCalling === 'true';
window.forCamera = config.forCamera === 'true';
window.Signal = {
Components: {
ConfirmationModal,
ConfirmationDialog,
},
};

View File

@ -7673,278 +7673,6 @@ button.module-image__border-overlay:focus {
}
}
// Module: SafetyNumberChangeDialog
.module-sfn-dialog__title {
@include font-body-1-bold;
text-align: center;
@include dark-theme {
color: $color-white;
}
}
.module-sfn-dialog__message {
@include font-body-2;
text-align: center;
@include light-theme {
color: $color-gray-60;
}
@include dark-theme {
color: $color-gray-25;
}
}
.module-sfn-dialog__contacts {
list-style-type: none;
max-height: 300px;
overflow-y: scroll;
padding: 0;
}
.module-sfn-dialog__contact {
align-items: center;
display: flex;
flex-direction: row;
margin-bottom: 16px;
&--wrapper {
flex-grow: 1;
margin-left: 12px;
}
&--name {
@include font-body-1-bold;
@include dark-theme {
color: $color-white;
}
}
&--number {
@include light-theme {
color: $color-gray-60;
}
@include dark-theme {
color: $color-gray-25;
}
}
&--view {
@include font-body-1-bold;
background: inherit;
border: none;
cursor: pointer;
margin-right: 2px;
outline: none;
padding: 8px 14px;
@include keyboard-mode {
&:focus {
box-shadow: 0px 0px 0px 2px $ultramarine-ui-light;
}
}
@include light-theme {
color: $ultramarine-ui-light;
}
@include dark-theme {
color: $ultramarine-ui-dark;
}
}
}
.module-sfn-dialog__actions {
border-top: 1px solid $color-gray-05;
display: flex;
justify-content: flex-end;
margin-left: -16px;
margin-right: -16px;
margin-top: -14px;
padding-left: 16px;
padding-right: 16px;
padding-top: 16px;
&--cancel {
@include font-body-1-bold;
border: none;
border-radius: 4px;
outline: none;
padding: 7px 14px;
@include mouse-mode {
&:hover {
background: $color-gray-15;
}
}
@include keyboard-mode {
&:focus {
box-shadow: 0px 0px 0px 2px $ultramarine-ui-light;
}
}
@include light-theme {
background-color: $color-gray-05;
color: $ultramarine-ui-light;
}
@include dark-theme {
background-color: $color-gray-75;
color: $ultramarine-ui-dark;
}
}
&--confirm {
@include font-body-1-bold;
background: $ultramarine-ui-light;
border: none;
border-radius: 4px;
color: $color-white;
margin-left: 12px;
outline: none;
padding: 7px 14px;
@include mouse-mode {
&:hover {
background: $ultramarine-brand-dark;
}
}
@include keyboard-mode {
&:focus {
box-shadow: 0px 0px 0px 2px $ultramarine-brand-dark;
}
}
}
}
/* Safety Number verification */
.module-safety-number {
&__icon {
height: 1.25em;
width: 1.25em;
vertical-align: text-bottom;
display: inline-block;
}
&__verification-label {
margin: 10px 0;
}
&__icon--verified {
display: inline-block;
height: 1.25em;
margin-right: 4px;
vertical-align: text-bottom;
width: 1.25em;
@include light-theme {
-webkit-mask: url('../images/icons/v2/check-24.svg') no-repeat center;
-webkit-mask-size: 100%;
background-color: #121212;
}
@include dark-theme {
-webkit-mask: url('../images/icons/v2/check-24.svg') no-repeat center;
-webkit-mask-size: 100%;
background-color: #f6f6f6;
}
}
&__icon--shield {
display: inline-block;
height: 1.25em;
margin-right: 4px;
vertical-align: text-bottom;
width: 1.25em;
@include light-theme {
-webkit-mask: url('../images/icons/v2/safety-number-outline-24.svg')
no-repeat center;
-webkit-mask-size: 100%;
background-color: #121212;
}
@include dark-theme {
-webkit-mask: url('../images/icons/v2/safety-number-solid-24.svg')
no-repeat center;
-webkit-mask-size: 100%;
background-color: #f6f6f6;
}
}
&__verify-container {
text-align: center;
}
&__button--verify {
border-radius: 5px;
font-weight: bold;
margin: 0;
outline: none;
padding: 10px;
}
&__number {
background: #f6f6f6;
border-radius: 5px;
border: solid 1px #dedede;
font-family: monospace;
margin: 20px auto 20px auto;
padding: 10px;
text-align: center;
width: 16em;
@include dark-theme {
background: #1b1b1b;
border: solid 1px #848484;
color: #f6f6f6;
}
}
&__verification-status {
margin: 30px 0 10px;
text-align: center;
}
&__close-button {
display: flex;
justify-content: flex-end;
button {
background: inherit;
border: none;
cursor: pointer;
padding: 0;
@include keyboard-mode {
&:focus {
border: 1px solid $ultramarine-ui-light;
}
}
span {
display: inline-block;
height: 24px;
width: 24px;
@include light-theme {
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-60);
}
@include dark-theme {
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-05);
}
}
}
}
}
// Module: StickerPicker
.module-sticker-picker {
@ -8873,112 +8601,6 @@ button.module-image__border-overlay:focus {
}
}
// Module: confirmation dialog
.module-confirmation-dialog {
&__overlay {
background: $color-black-alpha-40;
position: fixed;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
// THIS Z-INDEX IS OVER NINE THOUSAND. OVER NINE THOUSAND?! THAT CAN'T BE!
z-index: 9001;
}
&__container {
width: 360px;
padding: 12px 16px;
border-radius: 8px;
@include popper-shadow();
@include light-theme() {
background: $color-white;
color: $color-gray-90;
}
@include dark-theme() {
background: $color-gray-80;
color: $color-gray-05;
}
&__title {
@include font-body-1-bold;
}
&__content {
@include font-body-1;
}
&__buttons {
margin-top: 22px;
display: flex;
flex-direction: row;
justify-content: flex-end;
&__button {
margin-left: 4px;
border-radius: 17px;
height: 34px;
padding: 5px 12px;
display: flex;
justify-content: center;
align-items: center;
@include font-body-1-bold;
@include mouse-mode {
outline: none;
}
@include light-theme() {
background: $color-white;
color: $color-gray-60;
border: 1px solid $color-gray-60;
}
@include dark-theme() {
background: $color-gray-75;
color: $color-gray-25;
border: 1px solid $color-gray-25;
}
&--negative {
@include light-theme() {
border: none;
background: $color-accent-red;
color: $color-white;
}
@include dark-theme() {
border: none;
background: $color-accent-red;
color: $color-white;
}
}
&--affirmative {
@include light-theme() {
border: none;
background: $color-accent-green;
color: $color-white;
}
@include dark-theme() {
border: none;
background: $color-accent-green;
color: $color-white;
}
}
}
}
}
}
.module-left-pane-dialog {
background: $color-accent-green;
color: $color-white;

View File

@ -88,5 +88,9 @@
display: flex;
justify-content: flex-end;
margin-top: 16px;
.module-Button {
margin-left: 8px;
}
}
}

View File

@ -0,0 +1,78 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
.module-SafetyNumberChangeDialog {
&__message {
@include font-body-2;
text-align: center;
@include light-theme {
color: $color-gray-60;
}
@include dark-theme {
color: $color-gray-25;
}
}
&__contacts {
list-style-type: none;
max-height: 300px;
overflow-y: scroll;
padding: 0;
}
&__contact {
align-items: center;
display: flex;
flex-direction: row;
margin-bottom: 16px;
&--wrapper {
flex-grow: 1;
margin-left: 12px;
}
&--name {
@include font-body-1-bold;
@include dark-theme {
color: $color-white;
}
}
&--number {
@include light-theme {
color: $color-gray-60;
}
@include dark-theme {
color: $color-gray-25;
}
}
&--view {
@include font-body-1-bold;
background: inherit;
border: none;
cursor: pointer;
margin-right: 2px;
outline: none;
padding: 8px 14px;
@include keyboard-mode {
&:focus {
box-shadow: 0px 0px 0px 2px $ultramarine-ui-light;
}
}
@include light-theme {
color: $ultramarine-ui-light;
}
@include dark-theme {
color: $ultramarine-ui-dark;
}
}
}
}

View File

@ -0,0 +1,122 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
.module-SafetyNumberViewer {
&__icon {
height: 1.25em;
width: 1.25em;
vertical-align: text-bottom;
display: inline-block;
}
&__verification-label {
margin: 10px 0;
}
&__icon--verified {
display: inline-block;
height: 1.25em;
margin-right: 4px;
vertical-align: text-bottom;
width: 1.25em;
@include light-theme {
-webkit-mask: url('../images/icons/v2/check-24.svg') no-repeat center;
-webkit-mask-size: 100%;
background-color: #121212;
}
@include dark-theme {
-webkit-mask: url('../images/icons/v2/check-24.svg') no-repeat center;
-webkit-mask-size: 100%;
background-color: #f6f6f6;
}
}
&__icon--shield {
display: inline-block;
height: 1.25em;
margin-right: 4px;
vertical-align: text-bottom;
width: 1.25em;
@include light-theme {
-webkit-mask: url('../images/icons/v2/safety-number-outline-24.svg')
no-repeat center;
-webkit-mask-size: 100%;
background-color: #121212;
}
@include dark-theme {
-webkit-mask: url('../images/icons/v2/safety-number-solid-24.svg')
no-repeat center;
-webkit-mask-size: 100%;
background-color: #f6f6f6;
}
}
&__verify-container {
text-align: center;
}
&__button--verify {
border-radius: 5px;
font-weight: bold;
margin: 0;
outline: none;
padding: 10px;
}
&__number {
background: #f6f6f6;
border-radius: 5px;
border: solid 1px #dedede;
font-family: monospace;
margin: 20px auto 20px auto;
padding: 10px;
text-align: center;
width: 16em;
@include dark-theme {
background: #1b1b1b;
border: solid 1px #848484;
color: #f6f6f6;
}
}
&__verification-status {
margin: 30px 0 10px;
text-align: center;
}
&__close-button {
display: flex;
justify-content: flex-end;
button {
background: inherit;
border: none;
cursor: pointer;
padding: 0;
@include keyboard-mode {
&:focus {
border: 1px solid $ultramarine-ui-light;
}
}
span {
display: inline-block;
height: 24px;
width: 24px;
@include light-theme {
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-60);
}
@include dark-theme {
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-05);
}
}
}
}
}

View File

@ -38,5 +38,7 @@
@import './components/GroupTitleInput.scss';
@import './components/MessageAudio.scss';
@import './components/Modal.scss';
@import './components/SafetyNumberChangeDialog.scss';
@import './components/SafetyNumberViewer.scss';
@import './components/SearchResultsLoadingFakeHeader.scss';
@import './components/SearchResultsLoadingFakeRow.scss';

View File

@ -3,7 +3,7 @@
import * as React from 'react';
import { ConfirmationModal } from './ConfirmationModal';
import { Modal } from './Modal';
import { LocalizerType } from '../types/Util';
import {
AudioDevice,
@ -135,12 +135,7 @@ export const CallingDeviceSelection = ({
: undefined;
return (
<ConfirmationModal
actions={[]}
i18n={i18n}
theme={Theme.Dark}
onClose={toggleSettings}
>
<Modal i18n={i18n} theme={Theme.Dark} onClose={toggleSettings}>
<div className="module-calling-device-selection">
<button
type="button"
@ -210,6 +205,6 @@ export const CallingDeviceSelection = ({
{renderAudioOptions(availableSpeakers, i18n, selectedSpeaker)}
</select>
</div>
</ConfirmationModal>
</Modal>
);
};

View File

@ -2,8 +2,10 @@
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import classNames from 'classnames';
import { Button, ButtonVariant } from './Button';
import { LocalizerType } from '../types/Util';
import { Modal } from './Modal';
import { Theme } from '../util/theme';
export type ActionSpec = {
text: string;
@ -12,12 +14,14 @@ export type ActionSpec = {
};
export type OwnProps = {
readonly actions: Array<ActionSpec>;
readonly actions?: Array<ActionSpec>;
readonly cancelText?: string;
readonly children?: React.ReactNode;
readonly i18n: LocalizerType;
readonly onCancel?: () => unknown;
readonly onClose: () => unknown;
readonly title?: string | React.ReactNode;
readonly theme?: Theme;
};
export type Props = OwnProps;
@ -28,85 +32,77 @@ function focusRef(el: HTMLElement | null) {
}
}
// TODO: This should use <Modal>. See DESKTOP-1038.
export const ConfirmationDialog = React.memo(
({ i18n, onClose, cancelText, children, title, actions }: Props) => {
React.useEffect(() => {
const handler = ({ key }: KeyboardEvent) => {
if (key === 'Escape') {
onClose();
}
};
document.addEventListener('keydown', handler);
function getButtonVariant(
buttonStyle?: 'affirmative' | 'negative'
): ButtonVariant {
if (buttonStyle === 'affirmative') {
return ButtonVariant.Primary;
}
return () => {
document.removeEventListener('keydown', handler);
};
}, [onClose]);
if (buttonStyle === 'negative') {
return ButtonVariant.Destructive;
}
return ButtonVariant.Secondary;
}
export const ConfirmationDialog = React.memo(
({
actions = [],
cancelText,
children,
i18n,
onCancel,
onClose,
theme,
title,
}: Props) => {
const cancelAndClose = React.useCallback(() => {
if (onCancel) {
onCancel();
}
onClose();
}, [onCancel, onClose]);
const handleCancel = React.useCallback(
(e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
onClose();
cancelAndClose();
}
},
[onClose]
[cancelAndClose]
);
const handleAction = React.useCallback(
(e: React.MouseEvent<HTMLButtonElement>) => {
if (e.currentTarget.dataset.action) {
const actionIndex = parseInt(e.currentTarget.dataset.action, 10);
const { action } = actions[actionIndex];
action();
}
onClose();
},
[onClose, actions]
);
const hasActions = Boolean(actions.length);
return (
<div className="module-confirmation-dialog__container">
{title ? (
<h1 className="module-confirmation-dialog__container__title">
{title}
</h1>
) : null}
<div className="module-confirmation-dialog__container__content">
{children}
</div>
{actions.length > 0 && (
<div className="module-confirmation-dialog__container__buttons">
<button
type="button"
onClick={handleCancel}
ref={focusRef}
className="module-confirmation-dialog__container__buttons__button"
<Modal i18n={i18n} onClose={cancelAndClose} title={title} theme={theme}>
{children}
<Modal.Footer>
<Button
onClick={handleCancel}
ref={focusRef}
variant={
hasActions ? ButtonVariant.Secondary : ButtonVariant.Primary
}
>
{cancelText || i18n('confirmation-dialog--Cancel')}
</Button>
{actions.map((action, i) => (
<Button
key={action.text}
onClick={() => {
action.action();
onClose();
}}
data-action={i}
variant={getButtonVariant(action.style)}
>
{cancelText || i18n('confirmation-dialog--Cancel')}
</button>
{actions.map((action, i) => (
<button
type="button"
key={action.text}
onClick={handleAction}
data-action={i}
className={classNames(
'module-confirmation-dialog__container__buttons__button',
action.style === 'affirmative'
? 'module-confirmation-dialog__container__buttons__button--affirmative'
: null,
action.style === 'negative'
? 'module-confirmation-dialog__container__buttons__button--negative'
: null
)}
>
{action.text}
</button>
))}
</div>
)}
</div>
{action.text}
</Button>
))}
</Modal.Footer>
</Modal>
);
}
);

View File

@ -1,90 +0,0 @@
// Copyright 2019-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import classNames from 'classnames';
import { createPortal } from 'react-dom';
import {
ConfirmationDialog,
Props as ConfirmationDialogProps,
} from './ConfirmationDialog';
import { LocalizerType } from '../types/Util';
import { Theme, themeClassName } from '../util/theme';
export type OwnProps = {
readonly i18n: LocalizerType;
readonly onClose: () => unknown;
readonly theme?: Theme;
};
export type Props = OwnProps & ConfirmationDialogProps;
export const ConfirmationModal = React.memo(
({ i18n, onClose, theme, children, ...rest }: Props) => {
const [root, setRoot] = React.useState<HTMLElement | null>(null);
React.useEffect(() => {
const div = document.createElement('div');
document.body.appendChild(div);
setRoot(div);
return () => {
document.body.removeChild(div);
setRoot(null);
};
}, []);
React.useEffect(() => {
const handler = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose();
event.preventDefault();
event.stopPropagation();
}
};
document.addEventListener('keydown', handler);
return () => {
document.removeEventListener('keydown', handler);
};
}, [onClose]);
const handleCancel = React.useCallback(
(e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
onClose();
}
},
[onClose]
);
const handleKeyCancel = React.useCallback(
(e: React.KeyboardEvent) => {
if (e.target === e.currentTarget && e.keyCode === 27) {
onClose();
}
},
[onClose]
);
return root
? createPortal(
<div
role="presentation"
className={classNames(
'module-confirmation-dialog__overlay',
theme ? themeClassName(theme) : undefined
)}
onClick={handleCancel}
onKeyUp={handleKeyCancel}
>
<ConfirmationDialog i18n={i18n} {...rest} onClose={onClose}>
{children}
</ConfirmationDialog>
</div>,
root
)
: null;
}
);

View File

@ -4,7 +4,8 @@
import * as React from 'react';
import { LocalizerType } from '../types/Util';
import { ConfirmationModal } from './ConfirmationModal';
import { Modal } from './Modal';
import { Button, ButtonVariant } from './Button';
export type PropsType = {
buttonText?: string;
@ -21,30 +22,29 @@ function focusRef(el: HTMLElement | null) {
}
}
// TODO: This should use <Modal>. See DESKTOP-1038.
export const ErrorModal = (props: PropsType): JSX.Element => {
const { buttonText, description, i18n, onClose, title } = props;
return (
<ConfirmationModal
actions={[]}
title={title || i18n('ErrorModal--title')}
<Modal
i18n={i18n}
onClose={onClose}
title={title || i18n('ErrorModal--title')}
>
<div className="module-error-modal__description">
{description || i18n('ErrorModal--description')}
</div>
<div className="module-error-modal__button-container">
<button
type="button"
className="module-confirmation-dialog__container__buttons__button"
onClick={onClose}
ref={focusRef}
>
{buttonText || i18n('Confirmation--confirm')}
</button>
</div>
</ConfirmationModal>
<>
<div className="module-error-modal__description">
{description || i18n('ErrorModal--description')}
</div>
<Modal.Footer>
<Button
onClick={onClose}
ref={focusRef}
variant={ButtonVariant.Secondary}
>
{buttonText || i18n('Confirmation--confirm')}
</Button>
</Modal.Footer>
</>
</Modal>
);
};

View File

@ -18,7 +18,7 @@ import {
import { LocalizerType } from '../types/Util';
import { CallBackgroundBlur } from './CallBackgroundBlur';
import { Avatar, AvatarSize } from './Avatar';
import { ConfirmationModal } from './ConfirmationModal';
import { ConfirmationDialog } from './ConfirmationDialog';
import { Intl } from './Intl';
import { ContactName } from './conversation/ContactName';
import { useIntersectionObserver } from '../util/hooks';
@ -200,7 +200,8 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
return (
<>
{showBlockInfo && (
<ConfirmationModal
<ConfirmationDialog
cancelText={i18n('ok')}
i18n={i18n}
onClose={() => {
setShowBlockInfo(false);
@ -221,18 +222,9 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
/>
</div>
}
actions={[
{
text: i18n('ok'),
action: () => {
setShowBlockInfo(false);
},
style: 'affirmative',
},
]}
>
{i18n('calling__block-info')}
</ConfirmationModal>
</ConfirmationDialog>
)}
<div

View File

@ -7,6 +7,7 @@ import { noop } from 'lodash';
import { LocalizerType } from '../types/Util';
import { ModalHost } from './ModalHost';
import { Theme } from '../util/theme';
type PropsType = {
children: ReactNode;
@ -14,6 +15,7 @@ type PropsType = {
i18n: LocalizerType;
onClose?: () => void;
title?: ReactNode;
theme?: Theme;
};
export function Modal({
@ -22,13 +24,14 @@ export function Modal({
i18n,
onClose = noop,
title,
theme,
}: Readonly<PropsType>): ReactElement {
const [scrolled, setScrolled] = useState(false);
const hasHeader = Boolean(hasXButton || title);
return (
<ModalHost onClose={onClose}>
<ModalHost onClose={onClose} theme={theme}>
<div
className={classNames(
'module-Modal',

View File

@ -1,65 +1,73 @@
// Copyright 2019-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import React, { useEffect } from 'react';
import classNames from 'classnames';
import { createPortal } from 'react-dom';
import { Theme, themeClassName } from '../util/theme';
export type PropsType = {
readonly onClose: () => unknown;
readonly children: React.ReactElement;
readonly theme?: Theme;
};
export const ModalHost = React.memo(({ onClose, children }: PropsType) => {
const [root, setRoot] = React.useState<HTMLElement | null>(null);
export const ModalHost = React.memo(
({ onClose, children, theme }: PropsType) => {
const [root, setRoot] = React.useState<HTMLElement | null>(null);
React.useEffect(() => {
const div = document.createElement('div');
document.body.appendChild(div);
setRoot(div);
useEffect(() => {
const div = document.createElement('div');
document.body.appendChild(div);
setRoot(div);
return () => {
document.body.removeChild(div);
setRoot(null);
};
}, []);
return () => {
document.body.removeChild(div);
setRoot(null);
};
}, []);
React.useEffect(() => {
const handler = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose();
useEffect(() => {
const handler = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose();
event.preventDefault();
event.stopPropagation();
}
};
document.addEventListener('keydown', handler);
event.preventDefault();
event.stopPropagation();
}
};
document.addEventListener('keydown', handler);
return () => {
document.removeEventListener('keydown', handler);
};
}, [onClose]);
return () => {
document.removeEventListener('keydown', handler);
};
}, [onClose]);
// This makes it easier to write dialogs to be hosted here; they won't have to worry
// as much about preventing propagation of mouse events.
const handleCancel = React.useCallback(
(e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
onClose();
}
},
[onClose]
);
// This makes it easier to write dialogs to be hosted here; they won't have to worry
// as much about preventing propagation of mouse events.
const handleCancel = React.useCallback(
(e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
onClose();
}
},
[onClose]
);
return root
? createPortal(
<div
role="presentation"
className="module-modal-host__overlay"
onClick={handleCancel}
>
{children}
</div>,
root
)
: null;
});
return root
? createPortal(
<div
role="presentation"
className={classNames(
'module-modal-host__overlay',
theme ? themeClassName(theme) : undefined
)}
onClick={handleCancel}
>
{children}
</div>,
root
)
: null;
}
);

View File

@ -13,6 +13,7 @@ import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages);
const contactWithAllData = {
id: 'abc',
avatarPath: undefined,
color: 'signal-blue',
profileName: '-*Smartest Dude*-',
@ -22,6 +23,7 @@ const contactWithAllData = {
} as ConversationType;
const contactWithJustProfile = {
id: 'def',
avatarPath: undefined,
color: 'signal-blue',
title: '-*Smartest Dude*-',
@ -31,6 +33,7 @@ const contactWithJustProfile = {
} as ConversationType;
const contactWithJustNumber = {
id: 'xyz',
avatarPath: undefined,
color: 'signal-blue',
profileName: undefined,

View File

@ -2,10 +2,12 @@
// SPDX-License-Identifier: AGPL-3.0-only
import * as React from 'react';
import { noop } from 'lodash';
import { Avatar } from './Avatar';
import { ConfirmationModal } from './ConfirmationModal';
import { ConfirmationDialog } from './ConfirmationDialog';
import { InContactsIcon } from './InContactsIcon';
import { Modal } from './Modal';
import { ConversationType } from '../state/ducks/conversations';
import { LocalizerType } from '../types/Util';
@ -24,18 +26,17 @@ export type Props = {
readonly renderSafetyNumber: (props: SafetyNumberProps) => JSX.Element;
};
type SafetyDialogContentProps = Props & {
readonly onView: (contact: ConversationType) => void;
};
const SafetyDialogContents = ({
export const SafetyNumberChangeDialog = ({
confirmText,
contacts,
i18n,
onCancel,
onConfirm,
onView,
}: SafetyDialogContentProps): JSX.Element => {
renderSafetyNumber,
}: Props): JSX.Element => {
const [selectedContact, setSelectedContact] = React.useState<
ConversationType | undefined
>(undefined);
const cancelButtonRef = React.createRef<HTMLButtonElement>();
React.useEffect(() => {
@ -44,20 +45,46 @@ const SafetyDialogContents = ({
}
}, [cancelButtonRef, contacts]);
const onClose = selectedContact
? () => {
setSelectedContact(undefined);
}
: onCancel;
if (selectedContact) {
return (
<Modal i18n={i18n}>
{renderSafetyNumber({ contactID: selectedContact.id, onClose })}
</Modal>
);
}
return (
<>
<h1 className="module-sfn-dialog__title">
{i18n('safetyNumberChanges')}
</h1>
<div className="module-sfn-dialog__message">
<ConfirmationDialog
actions={[
{
action: onConfirm,
text: confirmText || i18n('sendMessageToContact'),
style: 'affirmative',
},
]}
i18n={i18n}
onCancel={onClose}
onClose={noop}
title={i18n('safetyNumberChanges')}
>
<div className="module-SafetyNumberChangeDialog__message">
{i18n('changedVerificationWarning')}
</div>
<ul className="module-sfn-dialog__contacts">
<ul className="module-SafetyNumberChangeDialog__contacts">
{contacts.map((contact: ConversationType) => {
const shouldShowNumber = Boolean(contact.name || contact.profileName);
return (
<li className="module-sfn-dialog__contact" key={contact.id}>
<li
className="module-SafetyNumberChangeDialog__contact"
key={contact.id}
>
<Avatar
avatarPath={contact.avatarPath}
color={contact.color}
@ -69,8 +96,8 @@ const SafetyDialogContents = ({
title={contact.title}
size={52}
/>
<div className="module-sfn-dialog__contact--wrapper">
<div className="module-sfn-dialog__contact--name">
<div className="module-SafetyNumberChangeDialog__contact--wrapper">
<div className="module-SafetyNumberChangeDialog__contact--name">
{contact.title}
{contact.name ? (
<span>
@ -80,15 +107,15 @@ const SafetyDialogContents = ({
) : null}
</div>
{shouldShowNumber ? (
<div className="module-sfn-dialog__contact--number">
<div className="module-SafetyNumberChangeDialog__contact--number">
{contact.phoneNumber}
</div>
) : null}
</div>
<button
className="module-sfn-dialog__contact--view"
className="module-SafetyNumberChangeDialog__contact--view"
onClick={() => {
onView(contact);
setSelectedContact(contact);
}}
tabIndex={0}
type="button"
@ -99,52 +126,6 @@ const SafetyDialogContents = ({
);
})}
</ul>
<div className="module-sfn-dialog__actions">
<button
className="module-sfn-dialog__actions--cancel"
onClick={onCancel}
ref={cancelButtonRef}
tabIndex={0}
type="button"
>
{i18n('cancel')}
</button>
<button
className="module-sfn-dialog__actions--confirm"
onClick={onConfirm}
tabIndex={0}
type="button"
>
{confirmText || i18n('sendMessageToContact')}
</button>
</div>
</>
);
};
export const SafetyNumberChangeDialog = (props: Props): JSX.Element => {
const { i18n, onCancel, renderSafetyNumber } = props;
const [contact, setViewSafetyNumber] = React.useState<
ConversationType | undefined
>(undefined);
const onClose = contact
? () => {
setViewSafetyNumber(undefined);
}
: onCancel;
return (
<ConfirmationModal actions={[]} i18n={i18n} onClose={onClose}>
{contact && renderSafetyNumber({ contactID: contact.id, onClose })}
{!contact && (
<SafetyDialogContents
{...props}
onView={selectedContact => {
setViewSafetyNumber(selectedContact);
}}
/>
)}
</ConfirmationModal>
</ConfirmationDialog>
);
};

View File

@ -41,8 +41,8 @@ export const SafetyNumberViewer = ({
if (!contact.phoneNumber) {
return (
<div className="module-safety-number">
<div className="module-safety-number__verify-container">
<div className="module-SafetyNumberViewer">
<div className="module-SafetyNumberViewer__verify-container">
{i18n('cannotGenerateSafetyNumber')}
</div>
</div>
@ -54,7 +54,7 @@ export const SafetyNumberViewer = ({
showNumber && contact.phoneNumber ? ` · ${contact.phoneNumber}` : '';
const name = `${contact.title}${numberFragment}`;
const boldName = (
<span className="module-safety-number__bold-name">{name}</span>
<span className="module-SafetyNumberViewer__bold-name">{name}</span>
);
const { isVerified } = contact;
@ -65,15 +65,15 @@ export const SafetyNumberViewer = ({
const verifyButtonText = isVerified ? i18n('unverify') : i18n('verify');
return (
<div className="module-safety-number">
<div className="module-SafetyNumberViewer">
{onClose && (
<div className="module-safety-number__close-button">
<div className="module-SafetyNumberViewer__close-button">
<button onClick={onClose} tabIndex={0} type="button">
<span />
</button>
</div>
)}
<div className="module-safety-number__verification-label">
<div className="module-SafetyNumberViewer__verification-label">
<Intl
i18n={i18n}
id={safetyNumberChangedKey}
@ -83,21 +83,21 @@ export const SafetyNumberViewer = ({
}}
/>
</div>
<div className="module-safety-number__number">
<div className="module-SafetyNumberViewer__number">
{safetyNumber || getPlaceholder()}
</div>
<Intl i18n={i18n} id="verifyHelp" components={[boldName]} />
<div className="module-safety-number__verification-status">
<div className="module-SafetyNumberViewer__verification-status">
{isVerified ? (
<span className="module-safety-number__icon--verified" />
<span className="module-SafetyNumberViewer__icon--verified" />
) : (
<span className="module-safety-number__icon--shield" />
<span className="module-SafetyNumberViewer__icon--shield" />
)}
<Intl i18n={i18n} id={verifiedStatusKey} components={[boldName]} />
</div>
<div className="module-safety-number__verify-container">
<div className="module-SafetyNumberViewer__verify-container">
<button
className="module-safety-number__button--verify"
className="module-SafetyNumberViewer__button--verify"
disabled={verificationDisabled}
onClick={() => {
toggleVerified(contact);

View File

@ -3,7 +3,7 @@
import * as React from 'react';
import { ContactName, PropsType as ContactNameProps } from './ContactName';
import { ConfirmationModal } from '../ConfirmationModal';
import { ConfirmationDialog } from '../ConfirmationDialog';
import { Intl } from '../Intl';
import { LocalizerType } from '../../types/Util';
@ -42,7 +42,7 @@ export const MessageRequestActionsConfirmation = ({
}: Props): JSX.Element | null => {
if (state === MessageRequestState.blocking) {
return (
<ConfirmationModal
<ConfirmationDialog
i18n={i18n}
onClose={() => {
onChangeState(MessageRequestState.default);
@ -77,13 +77,13 @@ export const MessageRequestActionsConfirmation = ({
]}
>
{i18n(`MessageRequests--block-${conversationType}-confirm-body`)}
</ConfirmationModal>
</ConfirmationDialog>
);
}
if (state === MessageRequestState.unblocking) {
return (
<ConfirmationModal
<ConfirmationDialog
i18n={i18n}
onClose={() => {
onChangeState(MessageRequestState.default);
@ -113,13 +113,13 @@ export const MessageRequestActionsConfirmation = ({
]}
>
{i18n(`MessageRequests--unblock-${conversationType}-confirm-body`)}
</ConfirmationModal>
</ConfirmationDialog>
);
}
if (state === MessageRequestState.deleting) {
return (
<ConfirmationModal
<ConfirmationDialog
i18n={i18n}
onClose={() => {
onChangeState(MessageRequestState.default);
@ -149,7 +149,7 @@ export const MessageRequestActionsConfirmation = ({
]}
>
{i18n(`MessageRequests--delete-${conversationType}-confirm-body`)}
</ConfirmationModal>
</ConfirmationDialog>
);
}

View File

@ -5,7 +5,7 @@ import React from 'react';
import classNames from 'classnames';
import { LocalizerType } from '../../../types/Util';
import { ConfirmationModal } from '../../ConfirmationModal';
import { ConfirmationDialog } from '../../ConfirmationDialog';
import { Tooltip, TooltipPlacement } from '../../Tooltip';
import { PanelRow } from './PanelRow';
@ -88,7 +88,7 @@ export const ConversationDetailsActions: React.ComponentType<Props> = ({
</PanelSection>
{confirmingLeave && (
<ConfirmationModal
<ConfirmationDialog
actions={[
{
text: i18n(
@ -103,11 +103,11 @@ export const ConversationDetailsActions: React.ComponentType<Props> = ({
title={i18n('ConversationDetailsActions--leave-group-modal-title')}
>
{i18n('ConversationDetailsActions--leave-group-modal-content')}
</ConfirmationModal>
</ConfirmationDialog>
)}
{confirmingBlock && (
<ConfirmationModal
<ConfirmationDialog
actions={[
{
text: i18n(
@ -124,7 +124,7 @@ export const ConversationDetailsActions: React.ComponentType<Props> = ({
])}
>
{i18n('ConversationDetailsActions--block-group-modal-content')}
</ConfirmationModal>
</ConfirmationDialog>
)}
</>
);

View File

@ -8,7 +8,7 @@ import _ from 'lodash';
import { ConversationType } from '../../../state/ducks/conversations';
import { LocalizerType } from '../../../types/Util';
import { Avatar } from '../../Avatar';
import { ConfirmationModal } from '../../ConfirmationModal';
import { ConfirmationDialog } from '../../ConfirmationDialog';
import { PanelSection } from './PanelSection';
import { PanelRow } from './PanelRow';
import { ConversationDetailsIcon } from './ConversationDetailsIcon';
@ -206,7 +206,7 @@ function MembershipActionConfirmation({
}
return (
<ConfirmationModal
<ConfirmationDialog
actions={[
{
action: modalAction,
@ -223,7 +223,7 @@ function MembershipActionConfirmation({
ourConversationId,
stagedMemberships,
})}
</ConfirmationModal>
</ConfirmationDialog>
);
}

View File

@ -3,7 +3,7 @@
import * as React from 'react';
import { StickerPackInstallButton } from './StickerPackInstallButton';
import { ConfirmationModal } from '../ConfirmationModal';
import { ConfirmationDialog } from '../ConfirmationDialog';
import { LocalizerType } from '../../types/Util';
import { StickerPackType } from '../../state/ducks/stickers';
@ -91,7 +91,7 @@ export const StickerManagerPackRow = React.memo(
return (
<>
{uninstalling ? (
<ConfirmationModal
<ConfirmationDialog
i18n={i18n}
onClose={clearUninstalling}
actions={[
@ -103,7 +103,7 @@ export const StickerManagerPackRow = React.memo(
]}
>
{i18n('stickers--StickerManager--UninstallWarning')}
</ConfirmationModal>
</ConfirmationDialog>
) : null}
<div
tabIndex={0}

View File

@ -140,7 +140,7 @@ export const StickerPreviewModal = React.memo((props: Props) => {
}
uninstallStickerPack(pack.id, pack.key);
setConfirmingUninstall(false);
// onClose is called by the confirmation modal
// onClose is called by <ConfirmationDialog />
}, [uninstallStickerPack, setConfirmingUninstall, pack]);
React.useEffect(() => {

View File

@ -3,7 +3,7 @@
// 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
// ConfirmationDialog 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.
@ -49,11 +49,10 @@ function showConfirmationDialog(options: ConfirmationDialogViewProps) {
window.ReactDOM.render(
// eslint-disable-next-line react/react-in-jsx-scope, react/jsx-no-undef
<window.Signal.Components.ConfirmationModal
<window.Signal.Components.ConfirmationDialog
actions={[
{
action: () => {
removeConfirmationDialog();
options.resolve();
},
style: options.confirmStyle,
@ -62,11 +61,13 @@ function showConfirmationDialog(options: ConfirmationDialogViewProps) {
]}
cancelText={options.cancelText || window.i18n('cancel')}
i18n={window.i18n}
onCancel={() => {
if (options.reject) {
options.reject(new Error('showConfirmationDialog: onCancel called'));
}
}}
onClose={() => {
removeConfirmationDialog();
if (options.reject) {
options.reject(new Error('showConfirmationDialog: onClose called'));
}
}}
title={options.message}
/>,

View File

@ -16433,7 +16433,7 @@
"rule": "React-createRef",
"path": "ts/components/SafetyNumberChangeDialog.js",
"line": " const cancelButtonRef = React.createRef();",
"lineNumber": 30,
"lineNumber": 33,
"reasonCategory": "usageTrusted",
"updated": "2020-06-23T06:48:06.829Z",
"reasonDetail": "Used to focus cancel button when dialog opens"
@ -17055,4 +17055,4 @@
"updated": "2021-01-08T15:46:32.143Z",
"reasonDetail": "Doesn't manipulate the DOM. This is just a function."
}
]
]

4
ts/window.d.ts vendored
View File

@ -81,7 +81,7 @@ import { combineNames } from './util';
import { BatcherType } from './util/batcher';
import { AttachmentList } from './components/conversation/AttachmentList';
import { CaptionEditor } from './components/CaptionEditor';
import { ConfirmationModal } from './components/ConfirmationModal';
import { ConfirmationDialog } from './components/ConfirmationDialog';
import { ContactDetail } from './components/conversation/ContactDetail';
import { ContactModal } from './components/conversation/ContactModal';
import { ErrorModal } from './components/ErrorModal';
@ -460,7 +460,7 @@ declare global {
Components: {
AttachmentList: typeof AttachmentList;
CaptionEditor: typeof CaptionEditor;
ConfirmationModal: typeof ConfirmationModal;
ConfirmationDialog: typeof ConfirmationDialog;
ContactDetail: typeof ContactDetail;
ContactModal: typeof ContactModal;
ErrorModal: typeof ErrorModal;