Refactored and cleaned up Modal and friends
This commit is contained in:
parent
f64426fbe0
commit
00a720faa9
31 changed files with 853 additions and 787 deletions
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -9,8 +9,7 @@
|
|||
|
||||
user-select: none;
|
||||
|
||||
// We use this selector for specificity.
|
||||
&.module-Modal {
|
||||
&__width-container {
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,8 +4,7 @@
|
|||
.BadgeSustainerInstructionsDialog {
|
||||
user-select: none;
|
||||
|
||||
// We use this selector for specificity.
|
||||
&.module-Modal {
|
||||
&__width-container {
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -71,8 +71,4 @@
|
|||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
&__modal__body {
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -148,6 +148,7 @@ export const AddUserToAnotherGroupModal = ({
|
|||
onClose={toggleAddUserToAnotherGroupModal}
|
||||
title={i18n('AddUserToAnotherGroupModal__title')}
|
||||
moduleClassName="AddUserToAnotherGroupModal"
|
||||
padded={false}
|
||||
>
|
||||
<div className="AddUserToAnotherGroupModal__main-body">
|
||||
<SearchInput
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -140,6 +140,7 @@ export const CallingDeviceSelection = ({
|
|||
i18n={i18n}
|
||||
theme={Theme.Dark}
|
||||
onClose={toggleSettings}
|
||||
padded={false}
|
||||
>
|
||||
<div className="module-calling-device-selection">
|
||||
<button
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -61,7 +61,6 @@ export const ProfileEditorModal = ({
|
|||
<>
|
||||
<Modal
|
||||
modalName="ProfileEditorModal"
|
||||
hasStickyButtons
|
||||
hasXButton
|
||||
i18n={i18n}
|
||||
onClose={toggleProfileEditor}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -172,6 +172,7 @@ export const ContactModal = ({
|
|||
hasXButton
|
||||
i18n={i18n}
|
||||
onClose={hideContactModal}
|
||||
padded={false}
|
||||
>
|
||||
<div className="ContactModal">
|
||||
<Avatar
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -152,7 +152,6 @@ export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGr
|
|||
{this.isEditingAvatar && (
|
||||
<Modal
|
||||
modalName="LeftPaneSetGroupMetadataHelper.AvatarEditor"
|
||||
hasStickyButtons
|
||||
hasXButton
|
||||
i18n={i18n}
|
||||
onClose={toggleComposeEditingAvatar}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue