Refactored and cleaned up Modal and friends

This commit is contained in:
Alvaro 2022-09-29 16:40:09 -06:00 committed by GitHub
parent f64426fbe0
commit 00a720faa9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 853 additions and 787 deletions

View File

@ -3,6 +3,9 @@
// Using BEM syntax explained here: https://csswizardry.com/2013/01/mindbemding-getting-your-head-round-bem-syntax/
// CAUTION: these styles are often overridden by other components
// if you make changes to these, you must check EVERY component that uses <Modal.../>
.module-title-bar-drag-area {
-webkit-app-region: drag;
height: var(--title-bar-drag-area-height);

View File

@ -1,12 +1,6 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
div.AddUserToAnotherGroupModal__body {
padding-left: 0;
padding-bottom: 0;
padding-right: 0;
}
.AddUserToAnotherGroupModal {
&__main-body {
display: flex;

View File

@ -9,8 +9,7 @@
user-select: none;
// We use this selector for specificity.
&.module-Modal {
&__width-container {
max-width: 420px;
}

View File

@ -4,8 +4,7 @@
.BadgeSustainerInstructionsDialog {
user-select: none;
// We use this selector for specificity.
&.module-Modal {
&__width-container {
max-width: 420px;
}

View File

@ -2,27 +2,23 @@
// SPDX-License-Identifier: AGPL-3.0-only
.module-CallingSelectPresentingSourcesModal {
// specificity
&.module-Modal {
&__width-container {
max-width: 665px;
position: relative;
padding-bottom: 48px;
}
&__button-footer {
// there's no module-class-name on the footer,
// so we have to reference it using the generic selector
.module-Modal__button-footer {
background-color: $color-gray-95;
bottom: 0;
margin-left: -16px;
margin-top: 0;
padding: 16px;
position: absolute;
width: 100%;
}
&__sources {
margin-bottom: 20px;
margin-left: -6px;
margin-right: -6px;
margin-bottom: 34px;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
&:last-child {
margin-bottom: 0;
@ -38,9 +34,6 @@
border-radius: 4px;
border: 1px solid $color-gray-60;
margin-bottom: 14px;
margin-left: 6px;
margin-right: 6px;
overflow: hidden;
padding: 8px;
text-align: center;

View File

@ -71,8 +71,4 @@
height: 24px;
width: 24px;
}
&__modal__body {
overflow-x: hidden !important;
}
}

View File

@ -7,6 +7,7 @@
flex-direction: column;
justify-content: center;
margin-top: 4px;
margin-bottom: 16px;
&__name {
@include font-title-2;
@ -143,8 +144,3 @@
}
}
}
.module-Modal.ContactModal__modal .ContactModal__modal__body {
padding-left: 0;
padding-right: 0;
}

View File

@ -4,6 +4,7 @@
.module-Modal {
@include popper-shadow();
border-radius: 8px;
overflow: hidden;
// We need this to be a number not divisible by 5 so that if we have sticky
// buttons the bottom doesn't bleed through by 1px.
max-height: 89vh;
@ -23,9 +24,7 @@
align-items: center;
display: flex;
justify-content: space-between;
margin-bottom: 1em;
padding: 16px 16px 0 16px;
position: sticky;
padding: 16px 16px 1em 16px;
&--with-back-button .module-Modal__title {
text-align: center;
@ -132,16 +131,20 @@
@include scrollbar;
@include font-body-1;
margin: 0;
padding: 16px;
overflow-y: overlay;
overflow-x: auto;
}
&--padded {
.module-Modal__body {
padding: 16px;
}
}
&--has-header {
.module-Modal__body {
padding-top: 0;
border-top: 1px solid transparent;
// If there's a header, just the body scrolls
overflow-y: overlay;
overflow-x: auto;
&--scrolled {
@include light-theme {
@ -155,65 +158,22 @@
}
}
&--no-header {
// If there's no header, the whole thing scrolls
overflow-y: overlay;
overflow-x: auto;
}
&__button-footer {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
margin-top: 8px;
align-items: center;
padding: 1em 16px 16px 16px;
gap: 8px;
.module-Button {
margin-left: 8px;
margin-top: 8px;
}
&--one-button-per-line {
flex-direction: column;
align-items: flex-end;
}
.module-Modal--sticky-buttons & {
bottom: 0;
display: flex;
justify-content: flex-end;
padding: 16px 0;
position: sticky;
right: 0;
width: 100%;
z-index: $z-index-above-popup;
@include light-theme() {
background: $color-white;
}
@include dark-theme() {
background: $color-gray-80;
}
}
}
&--sticky-buttons {
.module-Modal__body {
padding-bottom: 0;
}
position: relative;
.module-Modal__body--overflow {
.module-Modal__button-footer {
@include light-theme {
border-top: 1px solid $color-gray-05;
}
@include dark-theme {
border-top: 1px solid $color-gray-80;
}
}
}
}
// Overrides for a modal with important message
@ -251,6 +211,7 @@
margin-top: 27px;
flex-grow: 0;
flex-shrink: 0;
padding: 0 12px 4px 12px;
.module-Button {
flex-grow: 1;

View File

@ -2,6 +2,27 @@
// SPDX-License-Identifier: AGPL-3.0-only
.SendStoryModal {
&__body {
// force
.module-Modal & {
padding-bottom: 0;
}
}
// don't re-layout buttons on wrap,
// since we have things beyond same-sized-rectangles in the footer
.module-Modal__button-footer {
&--one-button-per-line {
flex-direction: row;
align-items: center;
}
}
&__item--contact-or-conversation {
height: 52px;
padding: 0 6px;
}
&__top-bar {
align-items: center;
display: flex;
@ -85,7 +106,6 @@
justify-content: space-between;
margin: 8px 0;
user-select: none;
width: 100%;
}
&__info {
@ -164,8 +184,9 @@
&__selected-lists {
@include font-body-2;
color: $color-gray-15;
max-width: 280px;
padding-right: 16px;
user-select: none;
flex: 1;
}
&__ok {
@ -212,11 +233,3 @@
}
}
}
.module-Modal--sticky-buttons .SendStoryModal__button-footer {
align-items: center;
justify-content: space-between;
padding-top: 0;
padding-left: 16px;
padding-right: 16px;
}

View File

@ -2,9 +2,13 @@
// SPDX-License-Identifier: AGPL-3.0-only
.StoriesSettingsModal {
&__modal__body {
display: flex;
flex-direction: column;
}
&__conversation-list {
.module-conversation-list,
.module-conversation-list__item--contact-or-conversation {
.module-conversation-list {
padding-left: 0;
padding-right: 0;
}
@ -194,20 +198,6 @@
overflow: hidden;
}
&__search {
&__container {
margin-left: 0;
margin-right: 0;
}
}
&__tags {
margin: 0 -4px;
// Override .module-ContactPills
padding: 0;
}
&__name-story-avatar-container {
align-items: center;
display: flex;

View File

@ -148,6 +148,7 @@ export const AddUserToAnotherGroupModal = ({
onClose={toggleAddUserToAnotherGroupModal}
title={i18n('AddUserToAnotherGroupModal__title')}
moduleClassName="AddUserToAnotherGroupModal"
padded={false}
>
<div className="AddUserToAnotherGroupModal__main-body">
<SearchInput

View File

@ -21,10 +21,15 @@ export const Alert: FunctionComponent<PropsType> = ({
onClose,
title,
}) => (
<Modal modalName="Alert" i18n={i18n} onClose={onClose} title={title}>
{body}
<Modal.ButtonFooter>
<Modal
modalName="Alert"
i18n={i18n}
onClose={onClose}
title={title}
modalFooter={
<Button onClick={onClose}>{i18n('Confirmation--confirm')}</Button>
</Modal.ButtonFooter>
}
>
{body}
</Modal>
);

View File

@ -58,6 +58,7 @@ type PropsType = {
}
| {
type: 'submit';
form?: string;
}
) &
(
@ -117,12 +118,14 @@ export const Button = React.forwardRef<HTMLButtonElement, PropsType>(
let onClick: undefined | MouseEventHandler<HTMLButtonElement>;
let type: 'button' | 'submit';
let form;
if ('onClick' in props) {
({ onClick } = props);
type = 'button';
} else {
onClick = undefined;
({ type } = props);
({ form } = props);
}
const sizeClassName = SIZE_CLASS_NAMES.get(size);
@ -143,6 +146,7 @@ export const Button = React.forwardRef<HTMLButtonElement, PropsType>(
)}
disabled={disabled}
onClick={onClick}
form={form}
ref={ref}
style={style}
tabIndex={tabIndex}

View File

@ -140,6 +140,7 @@ export const CallingDeviceSelection = ({
i18n={i18n}
theme={Theme.Dark}
onClose={toggleSettings}
padded={false}
>
<div className="module-calling-device-selection">
<button

View File

@ -82,6 +82,20 @@ export const CallingSelectPresentingSourcesModal = ({
source => source.isScreen
);
const footer = (
<>
<Button onClick={() => setPresenting()} variant={ButtonVariant.Secondary}>
{i18n('cancel')}
</Button>
<Button
disabled={!sourceToPresent}
onClick={() => setPresenting(sourceToPresent)}
>
{i18n('calling__SelectPresentingSourcesModal--confirm')}
</Button>
</>
);
return (
<Modal
modalName="CallingSelectPresentingSourcesModal"
@ -93,6 +107,7 @@ export const CallingSelectPresentingSourcesModal = ({
}}
theme={Theme.Dark}
title={i18n('calling__SelectPresentingSourcesModal--title')}
modalFooter={footer}
>
<div className="module-CallingSelectPresentingSourcesModal__title">
{i18n('calling__SelectPresentingSourcesModal--entireScreen')}
@ -120,20 +135,6 @@ export const CallingSelectPresentingSourcesModal = ({
/>
))}
</div>
<Modal.ButtonFooter moduleClassName="module-CallingSelectPresentingSourcesModal">
<Button
onClick={() => setPresenting()}
variant={ButtonVariant.Secondary}
>
{i18n('cancel')}
</Button>
<Button
disabled={!sourceToPresent}
onClick={() => setPresenting(sourceToPresent)}
>
{i18n('calling__SelectPresentingSourcesModal--confirm')}
</Button>
</Modal.ButtonFooter>
</Modal>
);
};

View File

@ -34,6 +34,16 @@ export function CaptchaDialog(props: Readonly<PropsType>): JSX.Element {
};
if (isClosing && !isPending) {
const footer = (
<>
<Button onClick={onCancelClick} variant={ButtonVariant.Secondary}>
{i18n('cancel')}
</Button>
<Button onClick={onSkipClick} variant={ButtonVariant.Destructive}>
{i18n('CaptchaDialog--can_close__skip-verification')}
</Button>
</>
);
return (
<Modal
modalName="CaptchaDialog"
@ -42,18 +52,11 @@ export function CaptchaDialog(props: Readonly<PropsType>): JSX.Element {
title={i18n('CaptchaDialog--can-close__title')}
onClose={() => setIsClosing(false)}
key="skip"
modalFooter={footer}
>
<section>
<p>{i18n('CaptchaDialog--can-close__body')}</p>
</section>
<Modal.ButtonFooter>
<Button onClick={onCancelClick} variant={ButtonVariant.Secondary}>
{i18n('cancel')}
</Button>
<Button onClick={onSkipClick} variant={ButtonVariant.Destructive}>
{i18n('CaptchaDialog--can_close__skip-verification')}
</Button>
</Modal.ButtonFooter>
</Modal>
);
}
@ -71,6 +74,21 @@ export function CaptchaDialog(props: Readonly<PropsType>): JSX.Element {
}
};
const footer = (
<Button
disabled={isPending}
onClick={onContinueClick}
ref={updateButtonRef}
variant={ButtonVariant.Primary}
>
{isPending ? (
<Spinner size="22px" svgSize="small" direction="on-captcha" />
) : (
'Continue'
)}
</Button>
);
return (
<Modal
modalName="CaptchaDialog.pending"
@ -80,25 +98,12 @@ export function CaptchaDialog(props: Readonly<PropsType>): JSX.Element {
hasXButton
onClose={() => setIsClosing(true)}
key="primary"
modalFooter={footer}
>
<section>
<p>{i18n('CaptchaDialog__first-paragraph')}</p>
<p>{i18n('CaptchaDialog__second-paragraph')}</p>
</section>
<Modal.ButtonFooter>
<Button
disabled={isPending}
onClick={onContinueClick}
ref={updateButtonRef}
variant={ButtonVariant.Primary}
>
{isPending ? (
<Spinner size="22px" svgSize="small" direction="on-captcha" />
) : (
'Continue'
)}
</Button>
</Modal.ButtonFooter>
</Modal>
);
}

View File

@ -7,7 +7,7 @@ import { animated } from '@react-spring/web';
import { Button, ButtonVariant } from './Button';
import type { LocalizerType } from '../types/Util';
import { ModalHost } from './ModalHost';
import { Modal, ModalWindow } from './Modal';
import { ModalPage } from './Modal';
import type { Theme } from '../util/theme';
import { useAnimated } from '../hooks/useAnimated';
@ -96,6 +96,34 @@ export const ConfirmationDialog = React.memo(
const hasActions = Boolean(actions.length);
const footer = (
<>
<Button
onClick={handleCancel}
ref={focusRef}
variant={
cancelButtonVariant ||
(hasActions ? ButtonVariant.Secondary : ButtonVariant.Primary)
}
>
{cancelText || i18n('confirmation-dialog--Cancel')}
</Button>
{actions.map((action, i) => (
<Button
key={action.text}
onClick={() => {
action.action();
close();
}}
data-action={i}
variant={getButtonVariant(action.style)}
>
{action.text}
</Button>
))}
</>
);
const modalName = `ConfirmationDialog.${dialogName}`;
return (
@ -108,41 +136,17 @@ export const ConfirmationDialog = React.memo(
theme={theme}
>
<animated.div style={modalStyles}>
<ModalWindow
<ModalPage
modalName={modalName}
hasXButton={hasXButton}
i18n={i18n}
moduleClassName={moduleClassName}
onClose={cancelAndClose}
title={title}
modalFooter={footer}
>
{children}
<Modal.ButtonFooter>
<Button
onClick={handleCancel}
ref={focusRef}
variant={
cancelButtonVariant ||
(hasActions ? ButtonVariant.Secondary : ButtonVariant.Primary)
}
>
{cancelText || i18n('confirmation-dialog--Cancel')}
</Button>
{actions.map((action, i) => (
<Button
key={action.text}
onClick={() => {
action.action();
close();
}}
data-action={i}
variant={getButtonVariant(action.style)}
>
{action.text}
</Button>
))}
</Modal.ButtonFooter>
</ModalWindow>
</ModalPage>
</animated.div>
</ModalHost>
);

View File

@ -33,6 +33,30 @@ export function CrashReportDialog(props: Readonly<PropsType>): JSX.Element {
uploadCrashReports();
};
const footer = (
<>
<Button
disabled={isPending}
onClick={onEraseClick}
variant={ButtonVariant.Secondary}
>
{i18n('CrashReportDialog__erase')}
</Button>
<Button
disabled={isPending}
onClick={onSubmitClick}
ref={button => button?.focus()}
variant={ButtonVariant.Primary}
>
{isPending ? (
<Spinner size="22px" svgSize="small" />
) : (
i18n('CrashReportDialog__submit')
)}
</Button>
</>
);
return (
<Modal
modalName="CrashReportDialog"
@ -41,29 +65,9 @@ export function CrashReportDialog(props: Readonly<PropsType>): JSX.Element {
title={i18n('CrashReportDialog__title')}
hasXButton
onClose={eraseCrashReports}
modalFooter={footer}
>
<section>{i18n('CrashReportDialog__body')}</section>
<Modal.ButtonFooter>
<Button
disabled={isPending}
onClick={onEraseClick}
variant={ButtonVariant.Secondary}
>
{i18n('CrashReportDialog__erase')}
</Button>
<Button
disabled={isPending}
onClick={onSubmitClick}
ref={button => button?.focus()}
variant={ButtonVariant.Primary}
>
{isPending ? (
<Spinner size="22px" svgSize="small" />
) : (
i18n('CrashReportDialog__submit')
)}
</Button>
</Modal.ButtonFooter>
</Modal>
);
}

View File

@ -104,6 +104,38 @@ export function CustomizingPreferredReactionsModal({
);
const canSave = !isSaving && hasChanged;
const footer = (
<>
<Button
disabled={!canReset}
onClick={() => {
resetDraftEmoji();
}}
onKeyDown={event => {
if (event.key === 'Enter' || event.key === 'Space') {
resetDraftEmoji();
}
}}
variant={ButtonVariant.SecondaryAffirmative}
>
{i18n('reset')}
</Button>
<Button
disabled={!canSave}
onClick={() => {
savePreferredReactions();
}}
onKeyDown={event => {
if (event.key === 'Enter' || event.key === 'Space') {
savePreferredReactions();
}
}}
>
{i18n('save')}
</Button>
</>
);
return (
<Modal
modalName="CustomizingPreferredReactionsModal"
@ -114,6 +146,7 @@ export function CustomizingPreferredReactionsModal({
cancelCustomizePreferredReactionsModal();
}}
title={i18n('CustomizingPreferredReactions__title')}
modalFooter={footer}
>
<div className="module-CustomizingPreferredReactionsModal__small-emoji-picker-wrapper">
<ReactionPickerPicker
@ -163,35 +196,6 @@ export function CustomizingPreferredReactionsModal({
/>
</div>
)}
<Modal.ButtonFooter>
<Button
disabled={!canReset}
onClick={() => {
resetDraftEmoji();
}}
onKeyDown={event => {
if (event.key === 'Enter' || event.key === 'Space') {
resetDraftEmoji();
}
}}
variant={ButtonVariant.SecondaryAffirmative}
>
{i18n('reset')}
</Button>
<Button
disabled={!canSave}
onClick={() => {
savePreferredReactions();
}}
onKeyDown={event => {
if (event.key === 'Enter' || event.key === 'Space') {
savePreferredReactions();
}
}}
>
{i18n('save')}
</Button>
</Modal.ButtonFooter>
</Modal>
);
}

View File

@ -25,27 +25,23 @@ function focusRef(el: HTMLElement | null) {
export const ErrorModal = (props: PropsType): JSX.Element => {
const { buttonText, description, i18n, onClose, title } = props;
const footer = (
<Button onClick={onClose} ref={focusRef} variant={ButtonVariant.Secondary}>
{buttonText || i18n('Confirmation--confirm')}
</Button>
);
return (
<Modal
modalName="ErrorModal"
i18n={i18n}
onClose={onClose}
title={title || i18n('ErrorModal--title')}
modalFooter={footer}
>
<>
<div className="module-error-modal__description">
{description || i18n('ErrorModal--description')}
</div>
<Modal.ButtonFooter>
<Button
onClick={onClose}
ref={focusRef}
variant={ButtonVariant.Secondary}
>
{buttonText || i18n('Confirmation--confirm')}
</Button>
</Modal.ButtonFooter>
</>
<div className="module-error-modal__description">
{description || i18n('ErrorModal--description')}
</div>
</Modal>
);
};

View File

@ -46,14 +46,15 @@ BareBonesLong.story = {
};
export const BareBonesLongWithButton = (): JSX.Element => (
<Modal modalName="test" i18n={i18n}>
<Modal
modalName="test"
i18n={i18n}
modalFooter={<Button onClick={noop}>Okay</Button>}
>
<p>{LOREM_IPSUM}</p>
<p>{LOREM_IPSUM}</p>
<p>{LOREM_IPSUM}</p>
<p>{LOREM_IPSUM}</p>
<Modal.ButtonFooter>
<Button onClick={noop}>Okay</Button>
</Modal.ButtonFooter>
</Modal>
);
@ -68,11 +69,9 @@ export const TitleXButtonBodyAndButtonFooter = (): JSX.Element => (
title="Hello world"
onClose={onClose}
hasXButton
modalFooter={<Button onClick={noop}>Okay</Button>}
>
{LOREM_IPSUM}
<Modal.ButtonFooter>
<Button onClick={noop}>Okay</Button>
</Modal.ButtonFooter>
</Modal>
);
@ -81,21 +80,27 @@ TitleXButtonBodyAndButtonFooter.story = {
};
export const LotsOfButtonsInTheFooter = (): JSX.Element => (
<Modal modalName="test" i18n={i18n} onClose={onClose}>
<Modal
modalName="test"
i18n={i18n}
onClose={onClose}
modalFooter={
<>
<Button onClick={noop}>Okay X</Button>
<Button onClick={noop}>Okay</Button>
<Button onClick={noop}>Okay</Button>
<Button onClick={noop}>
This is a button with a fairly large amount of text
</Button>
<Button onClick={noop}>Okay</Button>
<Button onClick={noop}>
This is a button with a fairly large amount of text
</Button>
<Button onClick={noop}>Okay</Button>
</>
}
>
Hello world!
<Modal.ButtonFooter>
<Button onClick={noop}>Okay</Button>
<Button onClick={noop}>Okay</Button>
<Button onClick={noop}>Okay</Button>
<Button onClick={noop}>
This is a button with a fairly large amount of text
</Button>
<Button onClick={noop}>Okay</Button>
<Button onClick={noop}>
This is a button with a fairly large amount of text
</Button>
<Button onClick={noop}>Okay</Button>
</Modal.ButtonFooter>
</Modal>
);
@ -123,14 +128,17 @@ LongBodyWithTitle.story = {
};
export const LongBodyWithTitleAndButton = (): JSX.Element => (
<Modal modalName="test" i18n={i18n} title="Hello world" onClose={onClose}>
<Modal
modalName="test"
i18n={i18n}
title="Hello world"
onClose={onClose}
modalFooter={<Button onClick={noop}>Okay</Button>}
>
<p>{LOREM_IPSUM}</p>
<p>{LOREM_IPSUM}</p>
<p>{LOREM_IPSUM}</p>
<p>{LOREM_IPSUM}</p>
<Modal.ButtonFooter>
<Button onClick={noop}>Okay</Button>
</Modal.ButtonFooter>
</Modal>
);
@ -160,19 +168,20 @@ LongBodyWithLongTitleAndXButton.story = {
export const WithStickyButtonsLongBody = (): JSX.Element => (
<Modal
modalName="test"
hasStickyButtons
hasXButton
i18n={i18n}
onClose={onClose}
modalFooter={
<>
<Button onClick={noop}>Okay</Button>
<Button onClick={noop}>Okay</Button>
</>
}
>
<p>{LOREM_IPSUM}</p>
<p>{LOREM_IPSUM}</p>
<p>{LOREM_IPSUM}</p>
<p>{LOREM_IPSUM}</p>
<Modal.ButtonFooter>
<Button onClick={noop}>Okay</Button>
<Button onClick={noop}>Okay</Button>
</Modal.ButtonFooter>
</Modal>
);
@ -183,16 +192,17 @@ WithStickyButtonsLongBody.story = {
export const WithStickyButtonsShortBody = (): JSX.Element => (
<Modal
modalName="test"
hasStickyButtons
hasXButton
i18n={i18n}
onClose={onClose}
modalFooter={
<>
<Button onClick={noop}>Okay</Button>
<Button onClick={noop}>Okay</Button>
</>
}
>
<p>{LOREM_IPSUM.slice(0, 140)}</p>
<Modal.ButtonFooter>
<Button onClick={noop}>Okay</Button>
<Button onClick={noop}>Okay</Button>
</Modal.ButtonFooter>
</Modal>
);
@ -203,25 +213,26 @@ WithStickyButtonsShortBody.story = {
export const StickyFooterLotsOfButtons = (): JSX.Element => (
<Modal
modalName="test"
hasStickyButtons
i18n={i18n}
onClose={onClose}
title="OK"
modalFooter={
<>
<Button onClick={noop}>Okay</Button>
<Button onClick={noop}>Okay</Button>
<Button onClick={noop}>Okay</Button>
<Button onClick={noop}>
This is a button with a fairly large amount of text
</Button>
<Button onClick={noop}>Okay</Button>
<Button onClick={noop}>
This is a button with a fairly large amount of text
</Button>
<Button onClick={noop}>Okay</Button>
</>
}
>
<p>{LOREM_IPSUM}</p>
<Modal.ButtonFooter>
<Button onClick={noop}>Okay</Button>
<Button onClick={noop}>Okay</Button>
<Button onClick={noop}>Okay</Button>
<Button onClick={noop}>
This is a button with a fairly large amount of text
</Button>
<Button onClick={noop}>Okay</Button>
<Button onClick={noop}>
This is a button with a fairly large amount of text
</Button>
<Button onClick={noop}>Okay</Button>
</Modal.ButtonFooter>
</Modal>
);

View File

@ -20,7 +20,6 @@ import { useRefMerger } from '../hooks/useRefMerger';
type PropsType = {
children: ReactNode;
modalName: string;
hasStickyButtons?: boolean;
hasXButton?: boolean;
i18n: LocalizerType;
modalFooter?: JSX.Element;
@ -29,9 +28,10 @@ type PropsType = {
onClose?: () => void;
title?: ReactNode;
useFocusTrap?: boolean;
padded?: boolean;
};
type ModalPropsType = PropsType & {
export type ModalPropsType = PropsType & {
noMouseClose?: boolean;
theme?: Theme;
};
@ -41,7 +41,6 @@ const BASE_CLASS_NAME = 'module-Modal';
export function Modal({
children,
modalName,
hasStickyButtons,
hasXButton,
i18n,
modalFooter,
@ -52,6 +51,7 @@ export function Modal({
theme,
title,
useFocusTrap,
padded = true,
}: Readonly<ModalPropsType>): ReactElement {
const { close, modalStyles, overlayStyles } = useAnimated(onClose, {
getFrom: () => ({ opacity: 0, transform: 'translateY(48px)' }),
@ -72,9 +72,8 @@ export function Modal({
useFocusTrap={useFocusTrap}
>
<animated.div style={modalStyles}>
<ModalWindow
<ModalPage
modalName={modalName}
hasStickyButtons={hasStickyButtons}
hasXButton={hasXButton}
i18n={i18n}
modalFooter={modalFooter}
@ -82,25 +81,46 @@ export function Modal({
onBackButtonClick={onBackButtonClick}
onClose={close}
title={title}
padded={padded}
>
{children}
</ModalWindow>
</ModalPage>
</animated.div>
</ModalHost>
);
}
export function ModalWindow({
type ModalPageProps = Readonly<{
// should be the one provided by PagedModal
onClose: () => void;
}> &
Omit<Readonly<PropsType>, 'onClose'>;
/**
* Represents a single instance (or page) of a modal window.
*
* It should not be used by itself, either wrap it with PagedModal,
* render it in a component that has PagedModal as an ancestor, or
* use Modal instead.
*
* It does not provide open/close animation.
*
* NOTE: When used in conjunction with PagedModal (almost always the case):
* onClose" handler should be the one provided by the parent PagedModal,
* not one that has any logic. If you have some logic to execute when the
* modal closes, pass it to PagedModal.
*/
export function ModalPage({
children,
hasStickyButtons,
hasXButton,
i18n,
modalFooter,
moduleClassName,
onBackButtonClick,
onClose = noop,
onClose,
title,
}: Readonly<PropsType>): JSX.Element {
padded = true,
}: ModalPageProps): JSX.Element {
const modalRef = useRef<HTMLDivElement | null>(null);
const refMerger = useRefMerger();
@ -131,7 +151,7 @@ export function ModalWindow({
className={classNames(
getClassName(''),
getClassName(hasHeader ? '--has-header' : '--no-header'),
hasStickyButtons && getClassName('--sticky-buttons')
padded && getClassName('--padded')
)}
ref={modalRef}
onClick={event => {
@ -200,7 +220,7 @@ export function ModalWindow({
</div>
)}
</Measure>
{modalFooter}
{modalFooter && <Modal.ButtonFooter>{modalFooter}</Modal.ButtonFooter>}
</div>
</>
);
@ -208,17 +228,12 @@ export function ModalWindow({
Modal.ButtonFooter = function ButtonFooter({
children,
moduleClassName,
}: Readonly<{
children: ReactNode;
moduleClassName?: string;
}>): ReactElement {
const [ref, hasWrapped] = useHasWrapped<HTMLDivElement>();
const className = getClassNamesFor(
BASE_CLASS_NAME,
moduleClassName
)('__button-footer');
const className = getClassNamesFor(BASE_CLASS_NAME)('__button-footer');
return (
<div
@ -232,3 +247,55 @@ Modal.ButtonFooter = function ButtonFooter({
</div>
);
};
type PagedModalProps = Readonly<{
modalName: string;
children: RenderModalPage;
moduleClassName?: string;
onClose?: () => void;
useFocusTrap?: boolean;
noMouseClose?: boolean;
theme?: Theme;
}>;
/**
* Provides modal animation and click to close functionality to a
* ModalPage descendant.
*
* Useful when we want to swap between different ModalPages (possibly
* rendered by different components) without triggering an open/close
* transition animation.
*/
export function PagedModal({
modalName,
children,
moduleClassName,
noMouseClose,
onClose = noop,
theme,
useFocusTrap,
}: PagedModalProps): ReactElement {
const { close, modalStyles, overlayStyles } = useAnimated(onClose, {
getFrom: () => ({ opacity: 0, transform: 'translateY(48px)' }),
getTo: isOpen =>
isOpen
? { opacity: 1, transform: 'translateY(0px)' }
: { opacity: 0, transform: 'translateY(48px)' },
});
return (
<ModalHost
modalName={modalName}
moduleClassName={moduleClassName}
noMouseClose={noMouseClose}
onClose={close}
overlayStyles={overlayStyles}
theme={theme}
useFocusTrap={useFocusTrap}
>
<animated.div style={modalStyles}>{children(close)}</animated.div>
</ModalHost>
);
}
export type RenderModalPage = (onClose: () => void) => JSX.Element;

View File

@ -24,6 +24,26 @@ export const NeedsScreenRecordingPermissionsModal = ({
openSystemPreferencesAction,
toggleScreenRecordingPermissionsDialog,
}: PropsType): JSX.Element => {
const footer = (
<>
<Button
onClick={toggleScreenRecordingPermissionsDialog}
ref={focusRef}
variant={ButtonVariant.Secondary}
>
{i18n('calling__presenting--permission-cancel')}
</Button>
<Button
onClick={() => {
openSystemPreferencesAction();
toggleScreenRecordingPermissionsDialog();
}}
variant={ButtonVariant.Primary}
>
{i18n('calling__presenting--permission-open')}
</Button>
</>
);
return (
<Modal
modalName="NeedsScreenRecordingPermissionsModal"
@ -31,6 +51,7 @@ export const NeedsScreenRecordingPermissionsModal = ({
title={i18n('calling__presenting--permission-title')}
theme={Theme.Dark}
onClose={toggleScreenRecordingPermissionsDialog}
modalFooter={footer}
>
<p>{i18n('calling__presenting--macos-permission-description')}</p>
<ol style={{ paddingLeft: 16 }}>
@ -38,24 +59,6 @@ export const NeedsScreenRecordingPermissionsModal = ({
<li>{i18n('calling__presenting--permission-instruction-step2')}</li>
<li>{i18n('calling__presenting--permission-instruction-step3')}</li>
</ol>
<Modal.ButtonFooter>
<Button
onClick={toggleScreenRecordingPermissionsDialog}
ref={focusRef}
variant={ButtonVariant.Secondary}
>
{i18n('calling__presenting--permission-cancel')}
</Button>
<Button
onClick={() => {
openSystemPreferencesAction();
toggleScreenRecordingPermissionsDialog();
}}
variant={ButtonVariant.Primary}
>
{i18n('calling__presenting--permission-open')}
</Button>
</Modal.ButtonFooter>
</Modal>
);
};

View File

@ -61,7 +61,6 @@ export const ProfileEditorModal = ({
<>
<Modal
modalName="ProfileEditorModal"
hasStickyButtons
hasXButton
i18n={i18n}
onClose={toggleProfileEditor}

View File

@ -19,13 +19,14 @@ import { Checkbox } from './Checkbox';
import { ConfirmationDialog } from './ConfirmationDialog';
import { ContextMenu } from './ContextMenu';
import {
DistributionListSettings,
EditDistributionList,
DistributionListSettingsModal,
EditDistributionListModal,
EditMyStoriesPrivacy,
Page as StoriesSettingsPage,
} from './StoriesSettingsModal';
import { MY_STORIES_ID, getStoryDistributionListName } from '../types/Stories';
import { Modal } from './Modal';
import type { RenderModalPage, ModalPropsType } from './Modal';
import { PagedModal, ModalPage } from './Modal';
import { StoryDistributionListName } from './StoryDistributionListName';
import { Theme } from '../util/theme';
import { isNotNil } from '../util/isNotNil';
@ -254,58 +255,113 @@ export const SendStoryModal = ({
Array<UUIDStringType>
>(initialMyStoriesMemberUuids);
let content: JSX.Element;
if (page === Page.SetMyStoriesPrivacy) {
content = (
<EditMyStoriesPrivacy
hasDisclaimerAbove
i18n={i18n}
learnMore="SendStoryModal__privacy-disclaimer"
myStories={stagedMyStories}
onClickExclude={() => {
let nextSelectedContacts = stagedMyStories.members;
let selectedNames: string | undefined;
if (page === Page.ChooseGroups) {
selectedNames = chosenGroupNames.join(', ');
} else {
selectedNames = selectedStoryNames
.map(listName => getStoryDistributionListName(i18n, listName, listName))
.join(', ');
}
if (!stagedMyStories.isBlockList) {
const modalCommonProps: Pick<ModalPropsType, 'hasXButton' | 'i18n'> = {
hasXButton: true,
i18n,
};
let modal: RenderModalPage;
if (page === Page.SetMyStoriesPrivacy) {
const footer = (
<>
<div />
<div>
<Button
onClick={() => setPage(Page.SendStory)}
variant={ButtonVariant.Secondary}
>
{i18n('cancel')}
</Button>
<Button
onClick={() => {
if (stagedMyStories.isBlockList) {
if (stagedMyStories.members.length) {
onHideMyStoriesFrom(stagedMyStoriesMemberUuids);
} else {
setMyStoriesToAllSignalConnections();
}
} else {
onViewersUpdated(MY_STORIES_ID, stagedMyStoriesMemberUuids);
}
setSelectedContacts([]);
setPage(Page.SendStory);
}}
variant={ButtonVariant.Primary}
>
{i18n('save')}
</Button>
</div>
</>
);
modal = handleClose => (
<ModalPage
modalName="SendStoryModal__my-stories-privacy"
title={i18n('SendStoryModal__my-stories-privacy')}
modalFooter={footer}
onClose={handleClose}
{...modalCommonProps}
>
<EditMyStoriesPrivacy
hasDisclaimerAbove
i18n={i18n}
learnMore="SendStoryModal__privacy-disclaimer"
myStories={stagedMyStories}
onClickExclude={() => {
let nextSelectedContacts = stagedMyStories.members;
if (!stagedMyStories.isBlockList) {
setStagedMyStories(myStories => ({
...myStories,
isBlockList: true,
members: [],
}));
nextSelectedContacts = [];
}
setSelectedContacts(nextSelectedContacts);
setPage(Page.HideStoryFrom);
}}
onClickOnlyShareWith={() => {
if (!stagedMyStories.isBlockList) {
setSelectedContacts(stagedMyStories.members);
} else {
setStagedMyStories(myStories => ({
...myStories,
isBlockList: false,
members: [],
}));
}
setPage(Page.AddViewer);
}}
setSelectedContacts={setSelectedContacts}
setMyStoriesToAllSignalConnections={() => {
setStagedMyStories(myStories => ({
...myStories,
isBlockList: true,
members: [],
}));
nextSelectedContacts = [];
}
setSelectedContacts(nextSelectedContacts);
setPage(Page.HideStoryFrom);
}}
onClickOnlyShareWith={() => {
if (!stagedMyStories.isBlockList) {
setSelectedContacts(stagedMyStories.members);
} else {
setStagedMyStories(myStories => ({
...myStories,
isBlockList: false,
members: [],
}));
}
setPage(Page.AddViewer);
}}
setSelectedContacts={setSelectedContacts}
setMyStoriesToAllSignalConnections={() => {
setStagedMyStories(myStories => ({
...myStories,
isBlockList: true,
members: [],
}));
setSelectedContacts([]);
}}
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
/>
setSelectedContacts([]);
}}
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
/>
</ModalPage>
);
} else if (page === Page.EditingDistributionList && listToEdit) {
content = (
<DistributionListSettings
modal = handleClose => (
<DistributionListSettingsModal
getPreferredBadge={getPreferredBadge}
i18n={i18n}
listToEdit={listToEdit}
@ -316,6 +372,8 @@ export const SendStoryModal = ({
setPage={setPage}
setSelectedContacts={setSelectedContacts}
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
onBackButtonClick={() => setListIdToEdit(undefined)}
onClose={handleClose}
/>
);
} else if (
@ -324,8 +382,8 @@ export const SendStoryModal = ({
page === Page.AddViewer ||
page === Page.HideStoryFrom
) {
content = (
<EditDistributionList
modal = handleClose => (
<EditDistributionListModal
candidateConversations={candidateConversations}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
@ -350,13 +408,60 @@ export const SendStoryModal = ({
}
}}
page={page}
onClose={handleClose}
onBackButtonClick={() => {
if (listIdToEdit) {
if (
page === Page.AddViewer ||
page === Page.HideStoryFrom ||
page === Page.ChooseViewers
) {
setPage(Page.EditingDistributionList);
} else {
setListIdToEdit(undefined);
}
} else if (page === Page.HideStoryFrom || page === Page.AddViewer) {
setSelectedContacts([]);
setStagedMyStories(initialMyStories);
setStagedMyStoriesMemberUuids(initialMyStoriesMemberUuids);
setPage(Page.SetMyStoriesPrivacy);
} else if (page === Page.ChooseViewers) {
setSelectedContacts([]);
setPage(Page.SendStory);
} else if (page === Page.NameStory) {
setPage(Page.ChooseViewers);
}
}}
selectedContacts={selectedContacts}
setSelectedContacts={setSelectedContacts}
/>
);
} else if (page === Page.ChooseGroups) {
content = (
const footer = (
<>
<div className="SendStoryModal__selected-lists">{selectedNames}</div>
<button
aria-label={i18n('SendStoryModal__ok')}
className="SendStoryModal__ok"
disabled={!chosenGroupIds.size}
onClick={() => {
toggleGroupsForStorySend(Array.from(chosenGroupIds));
setChosenGroupIds(new Set());
setPage(Page.SendStory);
}}
type="button"
/>
</>
);
modal = handleClose => (
<ModalPage
modalName="SendStoryModal__choose-groups"
title={i18n('SendStoryModal__choose-groups')}
modalFooter={footer}
onClose={handleClose}
{...modalCommonProps}
>
<SearchInput
disabled={groupConversations.length === 0}
i18n={i18n}
@ -429,11 +534,32 @@ export const SendStoryModal = ({
{i18n('noContactsFound')}
</div>
)}
</>
</ModalPage>
);
} else {
content = (
const footer = (
<>
<div className="SendStoryModal__selected-lists">{selectedNames}</div>
<button
aria-label={i18n('SendStoryModal__send')}
className="SendStoryModal__send"
disabled={!selectedListIds.size && !selectedGroupIds.size}
onClick={() => {
onSend(Array.from(selectedListIds), Array.from(selectedGroupIds));
}}
type="button"
/>
</>
);
modal = handleClose => (
<ModalPage
modalName="SendStoryModal__title"
title={i18n('SendStoryModal__title')}
moduleClassName="SendStoryModal"
modalFooter={footer}
onClose={handleClose}
{...modalCommonProps}
>
<div className="SendStoryModal__top-bar">
{i18n('stories')}
<ContextMenu
@ -649,159 +775,19 @@ export const SendStoryModal = ({
)}
</Checkbox>
))}
</>
);
}
let modalTitle: string;
if (page === Page.SetMyStoriesPrivacy) {
modalTitle = i18n('SendStoryModal__my-stories-privacy');
} else if (page === Page.HideStoryFrom) {
modalTitle = i18n('StoriesSettings__hide-story');
} else if (page === Page.ChooseGroups) {
modalTitle = i18n('SendStoryModal__choose-groups');
} else if (page === Page.NameStory) {
modalTitle = i18n('StoriesSettings__name-story');
} else if (page === Page.ChooseViewers || page === Page.AddViewer) {
modalTitle = i18n('StoriesSettings__choose-viewers');
} else {
modalTitle = i18n('SendStoryModal__title');
}
let selectedNames: string | undefined;
if (page === Page.ChooseGroups) {
selectedNames = chosenGroupNames.join(', ');
} else {
selectedNames = selectedStoryNames
.map(listName => getStoryDistributionListName(i18n, listName, listName))
.join(', ');
}
const hasBackButton = page !== Page.SendStory;
let modalFooter: JSX.Element | undefined;
if (
page === Page.SendStory ||
page === Page.ChooseGroups ||
page === Page.SetMyStoriesPrivacy
) {
modalFooter = (
<Modal.ButtonFooter moduleClassName="SendStoryModal">
{page !== Page.SetMyStoriesPrivacy && (
<div className="SendStoryModal__selected-lists">{selectedNames}</div>
)}
{page === Page.ChooseGroups && (
<button
aria-label={i18n('SendStoryModal__ok')}
className="SendStoryModal__ok"
disabled={!chosenGroupIds.size}
onClick={() => {
toggleGroupsForStorySend(Array.from(chosenGroupIds));
setChosenGroupIds(new Set());
setPage(Page.SendStory);
}}
type="button"
/>
)}
{page === Page.SendStory && (
<button
aria-label={i18n('SendStoryModal__send')}
className="SendStoryModal__send"
disabled={!selectedListIds.size && !selectedGroupIds.size}
onClick={() => {
onSend(Array.from(selectedListIds), Array.from(selectedGroupIds));
}}
type="button"
/>
)}
{page === Page.SetMyStoriesPrivacy && (
<>
<div />
<div>
<Button
onClick={() => setPage(Page.SendStory)}
variant={ButtonVariant.Secondary}
>
{i18n('cancel')}
</Button>
<Button
onClick={() => {
if (stagedMyStories.isBlockList) {
if (stagedMyStories.members.length) {
onHideMyStoriesFrom(stagedMyStoriesMemberUuids);
} else {
setMyStoriesToAllSignalConnections();
}
} else {
onViewersUpdated(MY_STORIES_ID, stagedMyStoriesMemberUuids);
}
setSelectedContacts([]);
setPage(Page.SendStory);
}}
variant={ButtonVariant.Primary}
>
{i18n('save')}
</Button>
</div>
</>
)}
</Modal.ButtonFooter>
</ModalPage>
);
}
return (
<>
<Modal
<PagedModal
modalName="SendStoryModal"
hasStickyButtons
hasXButton
i18n={i18n}
modalFooter={modalFooter}
onBackButtonClick={
hasBackButton
? () => {
if (listIdToEdit) {
if (
page === Page.AddViewer ||
page === Page.HideStoryFrom ||
page === Page.ChooseViewers
) {
setPage(Page.EditingDistributionList);
} else {
setListIdToEdit(undefined);
}
} else if (page === Page.SetMyStoriesPrivacy) {
setSelectedContacts([]);
setStagedMyStories(initialMyStories);
setStagedMyStoriesMemberUuids(initialMyStoriesMemberUuids);
setPage(Page.SendStory);
} else if (
page === Page.HideStoryFrom ||
page === Page.AddViewer
) {
setSelectedContacts([]);
setStagedMyStories(initialMyStories);
setStagedMyStoriesMemberUuids(initialMyStoriesMemberUuids);
setPage(Page.SetMyStoriesPrivacy);
} else if (page === Page.ChooseGroups) {
setChosenGroupIds(new Set());
setPage(Page.SendStory);
} else if (page === Page.ChooseViewers) {
setSelectedContacts([]);
setPage(Page.SendStory);
} else if (page === Page.NameStory) {
setPage(Page.ChooseViewers);
}
}
: undefined
}
onClose={onClose}
title={modalTitle}
theme={Theme.Dark}
onClose={onClose}
>
{content}
</Modal>
{modal}
</PagedModal>
{confirmRemoveGroupId && (
<ConfirmationDialog
dialogName="SendStoryModal.confirmRemoveGroupId"

View File

@ -12,6 +12,7 @@ import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
import type { Row } from './ConversationList';
import type { StoryDistributionListWithMembersDataType } from '../types/Stories';
import type { UUIDStringType } from '../types/UUID';
import type { RenderModalPage, ModalPropsType } from './Modal';
import { Avatar, AvatarSize } from './Avatar';
import { Button, ButtonVariant } from './Button';
import { Checkbox } from './Checkbox';
@ -22,7 +23,7 @@ import { ConversationList, RowType } from './ConversationList';
import { Input } from './Input';
import { Intl } from './Intl';
import { MY_STORIES_ID, getStoryDistributionListName } from '../types/Stories';
import { Modal } from './Modal';
import { PagedModal, ModalPage } from './Modal';
import { SearchInput } from './SearchInput';
import { StoryDistributionListName } from './StoryDistributionListName';
import { Theme } from '../util/theme';
@ -80,6 +81,12 @@ function filterConversations(
).filter(conversation => conversation.uuid);
}
const modalCommonProps: Pick<ModalPropsType, 'hasXButton' | 'moduleClassName'> =
{
hasXButton: true,
moduleClassName: 'StoriesSettingsModal__modal',
};
export const StoriesSettingsModal = ({
candidateConversations,
distributionLists,
@ -120,18 +127,34 @@ export const StoriesSettingsModal = ({
string | undefined
>();
let content: JSX.Element | null;
let modal: RenderModalPage | null;
if (page !== Page.DistributionLists) {
content = (
<EditDistributionList
const isChoosingViewers =
page === Page.ChooseViewers || page === Page.AddViewer;
modal = onClose => (
<EditDistributionListModal
candidateConversations={candidateConversations}
getPreferredBadge={getPreferredBadge}
i18n={i18n}
page={page}
onClose={onClose}
onCreateList={(name, uuids) => {
onDistributionListCreated(name, uuids);
resetChooseViewersScreen();
}}
onBackButtonClick={() => {
if (page === Page.HideStoryFrom) {
resetChooseViewersScreen();
} else if (page === Page.NameStory) {
setPage(Page.ChooseViewers);
} else if (isChoosingViewers) {
resetChooseViewersScreen();
} else if (listToEdit) {
setListToEditId(undefined);
}
}}
onViewersUpdated={uuids => {
if (listToEditId && page === Page.AddViewer) {
onViewersUpdated(listToEditId, uuids);
@ -147,14 +170,14 @@ export const StoriesSettingsModal = ({
resetChooseViewersScreen();
}
}}
page={page}
selectedContacts={selectedContacts}
setSelectedContacts={setSelectedContacts}
/>
);
} else if (listToEdit) {
content = (
<DistributionListSettings
modal = onClose => (
<DistributionListSettingsModal
key="settings-modal"
getPreferredBadge={getPreferredBadge}
i18n={i18n}
listToEdit={listToEdit}
@ -165,6 +188,8 @@ export const StoriesSettingsModal = ({
setPage={setPage}
setSelectedContacts={setSelectedContacts}
toggleSignalConnectionsModal={toggleSignalConnectionsModal}
onBackButtonClick={() => setListToEditId(undefined)}
onClose={onClose}
/>
);
} else {
@ -172,8 +197,14 @@ export const StoriesSettingsModal = ({
list => list.id !== MY_STORIES_ID
);
content = (
<>
modal = onClose => (
<ModalPage
modalName="StoriesSettingsModal__list"
i18n={i18n}
onClose={onClose}
title={i18n('StoriesSettings__title')}
{...modalCommonProps}
>
<button
className="StoriesSettingsModal__list"
onClick={() => {
@ -244,61 +275,19 @@ export const StoriesSettingsModal = ({
</span>
</button>
))}
</>
</ModalPage>
);
}
const isChoosingViewers =
page === Page.ChooseViewers || page === Page.AddViewer;
let modalTitle: string = i18n('StoriesSettings__title');
if (page === Page.HideStoryFrom) {
modalTitle = i18n('StoriesSettings__hide-story');
} else if (page === Page.NameStory) {
modalTitle = i18n('StoriesSettings__name-story');
} else if (isChoosingViewers) {
modalTitle = i18n('StoriesSettings__choose-viewers');
} else if (listToEdit) {
modalTitle = getStoryDistributionListName(
i18n,
listToEdit.id,
listToEdit.name
);
}
const hasBackButton = page !== Page.DistributionLists || listToEdit;
const hasStickyButtons =
isChoosingViewers || page === Page.NameStory || page === Page.HideStoryFrom;
return (
<>
<Modal
<PagedModal
modalName="StoriesSettingsModal"
hasStickyButtons={hasStickyButtons}
hasXButton
i18n={i18n}
moduleClassName="StoriesSettingsModal__modal"
onBackButtonClick={
hasBackButton
? () => {
if (page === Page.HideStoryFrom) {
resetChooseViewersScreen();
} else if (page === Page.NameStory) {
setPage(Page.ChooseViewers);
} else if (isChoosingViewers) {
resetChooseViewersScreen();
} else if (listToEdit) {
setListToEditId(undefined);
}
}
: undefined
}
onClose={hideStoriesSettings}
theme={Theme.Dark}
title={modalTitle}
onClose={hideStoriesSettings}
>
{content}
</Modal>
{modal}
</PagedModal>
{confirmDeleteListId && (
<ConfirmationDialog
dialogName="StoriesSettings.deleteList"
@ -324,12 +313,14 @@ export const StoriesSettingsModal = ({
);
};
type DistributionListSettingsPropsType = {
type DistributionListSettingsModalPropsType = {
i18n: LocalizerType;
listToEdit: StoryDistributionListWithMembersDataType;
setConfirmDeleteListId: (id: string) => unknown;
setPage: (page: Page) => unknown;
setSelectedContacts: (contacts: Array<ConversationType>) => unknown;
onBackButtonClick: (() => void) | undefined;
onClose: () => void;
} & Pick<
PropsType,
| 'getPreferredBadge'
@ -339,18 +330,20 @@ type DistributionListSettingsPropsType = {
| 'toggleSignalConnectionsModal'
>;
export const DistributionListSettings = ({
export const DistributionListSettingsModal = ({
getPreferredBadge,
i18n,
listToEdit,
onRemoveMember,
onRepliesNReactionsChanged,
onBackButtonClick,
onClose,
setConfirmDeleteListId,
setMyStoriesToAllSignalConnections,
setPage,
setSelectedContacts,
toggleSignalConnectionsModal,
}: DistributionListSettingsPropsType): JSX.Element => {
}: DistributionListSettingsModalPropsType): JSX.Element => {
const [confirmRemoveMember, setConfirmRemoveMember] = useState<
| undefined
| {
@ -362,8 +355,21 @@ export const DistributionListSettings = ({
const isMyStories = listToEdit.id === MY_STORIES_ID;
const modalTitle = getStoryDistributionListName(
i18n,
listToEdit.id,
listToEdit.name
);
return (
<>
<ModalPage
modalName="DistributionListSettingsModal"
i18n={i18n}
onBackButtonClick={onBackButtonClick}
onClose={onClose}
title={modalTitle}
{...modalCommonProps}
>
{!isMyStories && (
<>
<div className="StoriesSettingsModal__list StoriesSettingsModal__list--no-pointer">
@ -521,7 +527,7 @@ export const DistributionListSettings = ({
{i18n('StoriesSettings__remove--body')}
</ConfirmationDialog>
)}
</>
</ModalPage>
);
};
@ -630,24 +636,37 @@ export const EditMyStoriesPrivacy = ({
);
};
type EditDistributionListPropsType = {
type EditDistributionListModalPropsType = {
onCreateList: (name: string, viewerUuids: Array<UUIDStringType>) => unknown;
onViewersUpdated: (viewerUuids: Array<UUIDStringType>) => unknown;
page: Page;
page:
| Page.AddViewer
| Page.ChooseViewers
| Page.HideStoryFrom
| Page.NameStory;
selectedContacts: Array<ConversationType>;
onClose: () => unknown;
setSelectedContacts: (contacts: Array<ConversationType>) => unknown;
onBackButtonClick: () => void;
} & Pick<PropsType, 'candidateConversations' | 'getPreferredBadge' | 'i18n'>;
export const EditDistributionList = ({
/**
*
* @param param0
* @returns
*/
export const EditDistributionListModal = ({
candidateConversations,
getPreferredBadge,
i18n,
onCreateList,
onViewersUpdated,
page,
onClose,
selectedContacts,
setSelectedContacts,
}: EditDistributionListPropsType): JSX.Element | null => {
onBackButtonClick,
}: EditDistributionListModalPropsType): JSX.Element => {
const [storyName, setStoryName] = useState('');
const [searchTerm, setSearchTerm] = useState('');
@ -668,18 +687,6 @@ export const EditDistributionList = ({
};
}, [candidateConversations, normalizedSearchTerm, setFilteredConversations]);
const isEditingDistributionList =
page === Page.AddViewer ||
page === Page.ChooseViewers ||
page === Page.NameStory ||
page === Page.HideStoryFrom;
useEffect(() => {
if (!isEditingDistributionList) {
setSearchTerm('');
}
}, [isEditingDistributionList]);
const contactLookup = useMemo(() => {
const map = new Map();
candidateConversations.forEach(contact => {
@ -720,8 +727,29 @@ export const EditDistributionList = ({
page === Page.ChooseViewers || page === Page.AddViewer;
if (page === Page.NameStory) {
const footer = (
<Button
disabled={!storyName}
onClick={() => {
onCreateList(storyName, Array.from(selectedConversationUuids));
setStoryName('');
}}
variant={ButtonVariant.Primary}
>
{i18n('done')}
</Button>
);
return (
<>
<ModalPage
modalName="StoriesSettings__name-story"
title={i18n('StoriesSettings__name-story')}
modalFooter={footer}
i18n={i18n}
onBackButtonClick={onBackButtonClick}
onClose={onClose}
{...modalCommonProps}
>
<div className="StoriesSettingsModal__name-story-avatar-container">
<div className="StoriesSettingsModal__list__avatar--private StoriesSettingsModal__list__avatar--private--large" />
</div>
@ -762,143 +790,137 @@ export const EditDistributionList = ({
</span>
</div>
))}
<Modal.ButtonFooter>
<Button
disabled={!storyName}
onClick={() => {
onCreateList(storyName, Array.from(selectedConversationUuids));
setStoryName('');
}}
variant={ButtonVariant.Primary}
>
{i18n('done')}
</Button>
</Modal.ButtonFooter>
</>
</ModalPage>
);
}
if (
page === Page.AddViewer ||
page === Page.ChooseViewers ||
page === Page.HideStoryFrom
) {
const rowCount = filteredConversations.length;
const getRow = (index: number): undefined | Row => {
const contact = filteredConversations[index];
if (!contact || !contact.uuid) {
return undefined;
}
const rowCount = filteredConversations.length;
const getRow = (index: number): undefined | Row => {
const contact = filteredConversations[index];
if (!contact || !contact.uuid) {
return undefined;
}
const isSelected = selectedConversationUuids.has(UUID.cast(contact.uuid));
const isSelected = selectedConversationUuids.has(UUID.cast(contact.uuid));
return {
type: RowType.ContactCheckbox,
contact,
isChecked: isSelected,
};
return {
type: RowType.ContactCheckbox,
contact,
isChecked: isSelected,
};
};
return (
<>
<SearchInput
disabled={candidateConversations.length === 0}
i18n={i18n}
placeholder={i18n('contactSearchPlaceholder')}
moduleClassName="StoriesSettingsModal__search"
onChange={event => {
setSearchTerm(event.target.value);
}}
value={searchTerm}
/>
{selectedContacts.length ? (
<ContactPills moduleClassName="StoriesSettingsModal__tags">
{selectedContacts.map(contact => (
<ContactPill
key={contact.id}
acceptedMessageRequest={contact.acceptedMessageRequest}
avatarPath={contact.avatarPath}
color={contact.color}
firstName={contact.systemGivenName ?? contact.firstName}
i18n={i18n}
id={contact.id}
isMe={contact.isMe}
phoneNumber={contact.phoneNumber}
profileName={contact.profileName}
sharedGroupNames={contact.sharedGroupNames}
title={contact.title}
onClickRemove={() => toggleSelectedConversation(contact.id)}
/>
))}
</ContactPills>
) : undefined}
{candidateConversations.length ? (
<Measure bounds>
{({ contentRect, measureRef }: MeasuredComponentProps) => (
<div
className="StoriesSettingsModal__conversation-list"
ref={measureRef}
>
<ConversationList
dimensions={contentRect.bounds}
getPreferredBadge={getPreferredBadge}
getRow={getRow}
i18n={i18n}
onClickArchiveButton={shouldNeverBeCalled}
onClickContactCheckbox={(conversationId: string) => {
toggleSelectedConversation(conversationId);
}}
lookupConversationWithoutUuid={asyncShouldNeverBeCalled}
showConversation={shouldNeverBeCalled}
showUserNotFoundModal={shouldNeverBeCalled}
setIsFetchingUUID={shouldNeverBeCalled}
onSelectConversation={shouldNeverBeCalled}
renderMessageSearchResult={() => {
shouldNeverBeCalled();
return <div />;
}}
rowCount={rowCount}
shouldRecomputeRowHeights={false}
showChooseGroupMembers={shouldNeverBeCalled}
theme={ThemeType.dark}
/>
</div>
)}
</Measure>
) : (
<div className="module-ForwardMessageModal__no-candidate-contacts">
{i18n('noContactsFound')}
</div>
)}
{isChoosingViewers && (
<Modal.ButtonFooter>
<Button
disabled={selectedContacts.length === 0}
onClick={() => {
onViewersUpdated(Array.from(selectedConversationUuids));
}}
variant={ButtonVariant.Primary}
>
{page === Page.AddViewer ? i18n('done') : i18n('next2')}
</Button>
</Modal.ButtonFooter>
)}
{page === Page.HideStoryFrom && (
<Modal.ButtonFooter>
<Button
disabled={selectedContacts.length === 0}
onClick={() => {
onViewersUpdated(Array.from(selectedConversationUuids));
}}
variant={ButtonVariant.Primary}
>
{i18n('update')}
</Button>
</Modal.ButtonFooter>
)}
</>
let footer: JSX.Element | undefined;
if (isChoosingViewers) {
footer = (
<Button
disabled={selectedContacts.length === 0}
onClick={() => {
onViewersUpdated(Array.from(selectedConversationUuids));
}}
variant={ButtonVariant.Primary}
>
{page === Page.AddViewer ? i18n('done') : i18n('next2')}
</Button>
);
} else if (page === Page.HideStoryFrom) {
footer = (
<Button
disabled={selectedContacts.length === 0}
onClick={() => {
onViewersUpdated(Array.from(selectedConversationUuids));
}}
variant={ButtonVariant.Primary}
>
{i18n('update')}
</Button>
);
}
return null;
return (
<ModalPage
modalName={`EditDistributionListModal__${page}`}
i18n={i18n}
modalFooter={footer}
onBackButtonClick={onBackButtonClick}
onClose={onClose}
title={
page === Page.HideStoryFrom
? i18n('StoriesSettings__hide-story')
: i18n('StoriesSettings__choose-viewers')
}
padded={page !== Page.ChooseViewers && page !== Page.AddViewer}
{...modalCommonProps}
>
<SearchInput
disabled={candidateConversations.length === 0}
i18n={i18n}
placeholder={i18n('contactSearchPlaceholder')}
moduleClassName="StoriesSettingsModal__search"
onChange={event => {
setSearchTerm(event.target.value);
}}
value={searchTerm}
/>
{selectedContacts.length ? (
<ContactPills moduleClassName="StoriesSettingsModal__tags">
{selectedContacts.map(contact => (
<ContactPill
key={contact.id}
acceptedMessageRequest={contact.acceptedMessageRequest}
avatarPath={contact.avatarPath}
color={contact.color}
firstName={contact.firstName}
i18n={i18n}
id={contact.id}
isMe={contact.isMe}
phoneNumber={contact.phoneNumber}
profileName={contact.profileName}
sharedGroupNames={contact.sharedGroupNames}
title={contact.title}
onClickRemove={() => toggleSelectedConversation(contact.id)}
/>
))}
</ContactPills>
) : undefined}
{candidateConversations.length ? (
<Measure bounds>
{({ contentRect, measureRef }: MeasuredComponentProps) => (
<div
className="StoriesSettingsModal__conversation-list"
ref={measureRef}
>
<ConversationList
dimensions={contentRect.bounds}
getPreferredBadge={getPreferredBadge}
getRow={getRow}
i18n={i18n}
onClickArchiveButton={shouldNeverBeCalled}
onClickContactCheckbox={(conversationId: string) => {
toggleSelectedConversation(conversationId);
}}
lookupConversationWithoutUuid={asyncShouldNeverBeCalled}
showConversation={shouldNeverBeCalled}
showUserNotFoundModal={shouldNeverBeCalled}
setIsFetchingUUID={shouldNeverBeCalled}
onSelectConversation={shouldNeverBeCalled}
renderMessageSearchResult={() => {
shouldNeverBeCalled();
return <div />;
}}
rowCount={rowCount}
shouldRecomputeRowHeights={false}
showChooseGroupMembers={shouldNeverBeCalled}
theme={ThemeType.dark}
/>
</div>
)}
</Measure>
) : (
<div className="module-ForwardMessageModal__no-candidate-contacts">
{i18n('noContactsFound')}
</div>
)}
</ModalPage>
);
};

View File

@ -172,6 +172,7 @@ export const ContactModal = ({
hasXButton
i18n={i18n}
onClose={hideContactModal}
padded={false}
>
<div className="ContactModal">
<Avatar

View File

@ -31,12 +31,34 @@ export function DeliveryIssueDialog(props: PropsType): React.ReactElement {
// Focus first button after initial render, restore focus on teardown
const [focusRef] = useRestoreFocus();
const footer = (
<>
<Button
onClick={learnMoreAboutDeliveryIssue}
size={ButtonSize.Medium}
variant={ButtonVariant.Secondary}
>
{i18n('DeliveryIssue--learnMore')}
</Button>
<Button
onClick={onClose}
ref={focusRef}
size={ButtonSize.Medium}
variant={ButtonVariant.Primary}
className="module-delivery-issue-dialog__close-button"
>
{i18n('Confirmation--confirm')}
</Button>
</>
);
return (
<Modal
modalName="DeliveryIssueDialog"
hasXButton={false}
onClose={onClose}
i18n={i18n}
modalFooter={footer}
>
<section>
<div className="module-delivery-issue-dialog__image">
@ -60,24 +82,6 @@ export function DeliveryIssueDialog(props: PropsType): React.ReactElement {
/>
</div>
</section>
<Modal.ButtonFooter>
<Button
onClick={learnMoreAboutDeliveryIssue}
size={ButtonSize.Medium}
variant={ButtonVariant.Secondary}
>
{i18n('DeliveryIssue--learnMore')}
</Button>
<Button
onClick={onClose}
ref={focusRef}
size={ButtonSize.Medium}
variant={ButtonVariant.Primary}
className="module-delivery-issue-dialog__close-button"
>
{i18n('Confirmation--confirm')}
</Button>
</Modal.ButtonFooter>
</Modal>
);
}

View File

@ -47,11 +47,20 @@ export const ConversationNotificationsModal = ({
return (
<Modal
modalName="ConversationNotificationsModal"
hasStickyButtons
hasXButton
onClose={onClose}
i18n={i18n}
title={i18n('muteNotificationsTitle')}
modalFooter={
<>
<Button onClick={onClose} variant={ButtonVariant.Secondary}>
{i18n('cancel')}
</Button>
<Button onClick={onMuteChange} variant={ButtonVariant.Primary}>
{i18n('mute')}
</Button>
</>
}
>
{muteOptions
.filter(x => x.value > 0)
@ -67,14 +76,6 @@ export const ConversationNotificationsModal = ({
onChange={value => value && setMuteExpirationValue(option.value)}
/>
))}
<Modal.ButtonFooter>
<Button onClick={onClose} variant={ButtonVariant.Secondary}>
{i18n('cancel')}
</Button>
<Button onClick={onMuteChange} variant={ButtonVariant.Primary}>
{i18n('mute')}
</Button>
</Modal.ButtonFooter>
</Modal>
);
};

View File

@ -155,6 +155,7 @@ export const EditConversationAttributesModal: FunctionComponent<PropsType> = ({
} else {
content = (
<form
id="edit-conversation-form"
onSubmit={onSubmit}
className="module-EditConversationAttributesModal"
>
@ -199,40 +200,43 @@ export const EditConversationAttributesModal: FunctionComponent<PropsType> = ({
{i18n('updateGroupAttributes__error-message')}
</div>
)}
<Modal.ButtonFooter>
<Button
disabled={isRequestActive}
onClick={onClose}
variant={ButtonVariant.Secondary}
>
{i18n('cancel')}
</Button>
<Button
type="submit"
variant={ButtonVariant.Primary}
disabled={!canSubmit}
>
{isRequestActive ? (
<Spinner size="20px" svgSize="small" direction="on-avatar" />
) : (
i18n('save')
)}
</Button>
</Modal.ButtonFooter>
</form>
);
}
const modalFooter = (
<>
<Button
disabled={isRequestActive}
onClick={onClose}
variant={ButtonVariant.Secondary}
>
{i18n('cancel')}
</Button>
<Button
type="submit"
form="edit-conversation-form"
variant={ButtonVariant.Primary}
disabled={!canSubmit}
>
{isRequestActive ? (
<Spinner size="20px" svgSize="small" direction="on-avatar" />
) : (
i18n('save')
)}
</Button>
</>
);
return (
<Modal
modalName="EditConversationAttributesModal"
hasStickyButtons
hasXButton
i18n={i18n}
onClose={onClose}
title={i18n('updateGroupAttributes__title')}
modalFooter={modalFooter}
>
{content}
</Modal>

View File

@ -152,7 +152,6 @@ export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGr
{this.isEditingAvatar && (
<Modal
modalName="LeftPaneSetGroupMetadataHelper.AvatarEditor"
hasStickyButtons
hasXButton
i18n={i18n}
onClose={toggleComposeEditingAvatar}