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; | ||||
|     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,21 +74,7 @@ export function CaptchaDialog(props: Readonly<PropsType>): JSX.Element { | |||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <Modal | ||||
|       modalName="CaptchaDialog.pending" | ||||
|       moduleClassName="module-Modal--important" | ||||
|       i18n={i18n} | ||||
|       title={i18n('CaptchaDialog__title')} | ||||
|       hasXButton | ||||
|       onClose={() => setIsClosing(true)} | ||||
|       key="primary" | ||||
|     > | ||||
|       <section> | ||||
|         <p>{i18n('CaptchaDialog__first-paragraph')}</p> | ||||
|         <p>{i18n('CaptchaDialog__second-paragraph')}</p> | ||||
|       </section> | ||||
|       <Modal.ButtonFooter> | ||||
|   const footer = ( | ||||
|     <Button | ||||
|       disabled={isPending} | ||||
|       onClick={onContinueClick} | ||||
|  | @ -98,7 +87,23 @@ export function CaptchaDialog(props: Readonly<PropsType>): JSX.Element { | |||
|         'Continue' | ||||
|       )} | ||||
|     </Button> | ||||
|       </Modal.ButtonFooter> | ||||
|   ); | ||||
| 
 | ||||
|   return ( | ||||
|     <Modal | ||||
|       modalName="CaptchaDialog.pending" | ||||
|       moduleClassName="module-Modal--important" | ||||
|       i18n={i18n} | ||||
|       title={i18n('CaptchaDialog__title')} | ||||
|       hasXButton | ||||
|       onClose={() => setIsClosing(true)} | ||||
|       key="primary" | ||||
|       modalFooter={footer} | ||||
|     > | ||||
|       <section> | ||||
|         <p>{i18n('CaptchaDialog__first-paragraph')}</p> | ||||
|         <p>{i18n('CaptchaDialog__second-paragraph')}</p> | ||||
|       </section> | ||||
|     </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,28 +96,8 @@ export const ConfirmationDialog = React.memo( | |||
| 
 | ||||
|     const hasActions = Boolean(actions.length); | ||||
| 
 | ||||
|     const modalName = `ConfirmationDialog.${dialogName}`; | ||||
| 
 | ||||
|     return ( | ||||
|       <ModalHost | ||||
|         modalName={modalName} | ||||
|         noMouseClose={noMouseClose} | ||||
|         onClose={close} | ||||
|         onTopOfEverything={onTopOfEverything} | ||||
|         overlayStyles={overlayStyles} | ||||
|         theme={theme} | ||||
|       > | ||||
|         <animated.div style={modalStyles}> | ||||
|           <ModalWindow | ||||
|             modalName={modalName} | ||||
|             hasXButton={hasXButton} | ||||
|             i18n={i18n} | ||||
|             moduleClassName={moduleClassName} | ||||
|             onClose={cancelAndClose} | ||||
|             title={title} | ||||
|           > | ||||
|             {children} | ||||
|             <Modal.ButtonFooter> | ||||
|     const footer = ( | ||||
|       <> | ||||
|         <Button | ||||
|           onClick={handleCancel} | ||||
|           ref={focusRef} | ||||
|  | @ -141,8 +121,32 @@ export const ConfirmationDialog = React.memo( | |||
|             {action.text} | ||||
|           </Button> | ||||
|         ))} | ||||
|             </Modal.ButtonFooter> | ||||
|           </ModalWindow> | ||||
|       </> | ||||
|     ); | ||||
| 
 | ||||
|     const modalName = `ConfirmationDialog.${dialogName}`; | ||||
| 
 | ||||
|     return ( | ||||
|       <ModalHost | ||||
|         modalName={modalName} | ||||
|         noMouseClose={noMouseClose} | ||||
|         onClose={close} | ||||
|         onTopOfEverything={onTopOfEverything} | ||||
|         overlayStyles={overlayStyles} | ||||
|         theme={theme} | ||||
|       > | ||||
|         <animated.div style={modalStyles}> | ||||
|           <ModalPage | ||||
|             modalName={modalName} | ||||
|             hasXButton={hasXButton} | ||||
|             i18n={i18n} | ||||
|             moduleClassName={moduleClassName} | ||||
|             onClose={cancelAndClose} | ||||
|             title={title} | ||||
|             modalFooter={footer} | ||||
|           > | ||||
|             {children} | ||||
|           </ModalPage> | ||||
|         </animated.div> | ||||
|       </ModalHost> | ||||
|     ); | ||||
|  |  | |||
|  | @ -33,17 +33,8 @@ export function CrashReportDialog(props: Readonly<PropsType>): JSX.Element { | |||
|     uploadCrashReports(); | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <Modal | ||||
|       modalName="CrashReportDialog" | ||||
|       moduleClassName="module-Modal--important" | ||||
|       i18n={i18n} | ||||
|       title={i18n('CrashReportDialog__title')} | ||||
|       hasXButton | ||||
|       onClose={eraseCrashReports} | ||||
|     > | ||||
|       <section>{i18n('CrashReportDialog__body')}</section> | ||||
|       <Modal.ButtonFooter> | ||||
|   const footer = ( | ||||
|     <> | ||||
|       <Button | ||||
|         disabled={isPending} | ||||
|         onClick={onEraseClick} | ||||
|  | @ -63,7 +54,20 @@ export function CrashReportDialog(props: Readonly<PropsType>): JSX.Element { | |||
|           i18n('CrashReportDialog__submit') | ||||
|         )} | ||||
|       </Button> | ||||
|       </Modal.ButtonFooter> | ||||
|     </> | ||||
|   ); | ||||
| 
 | ||||
|   return ( | ||||
|     <Modal | ||||
|       modalName="CrashReportDialog" | ||||
|       moduleClassName="module-Modal--important" | ||||
|       i18n={i18n} | ||||
|       title={i18n('CrashReportDialog__title')} | ||||
|       hasXButton | ||||
|       onClose={eraseCrashReports} | ||||
|       modalFooter={footer} | ||||
|     > | ||||
|       <section>{i18n('CrashReportDialog__body')}</section> | ||||
|     </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> | ||||
|       </> | ||||
|     </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,21 +24,8 @@ export const NeedsScreenRecordingPermissionsModal = ({ | |||
|   openSystemPreferencesAction, | ||||
|   toggleScreenRecordingPermissionsDialog, | ||||
| }: PropsType): JSX.Element => { | ||||
|   return ( | ||||
|     <Modal | ||||
|       modalName="NeedsScreenRecordingPermissionsModal" | ||||
|       i18n={i18n} | ||||
|       title={i18n('calling__presenting--permission-title')} | ||||
|       theme={Theme.Dark} | ||||
|       onClose={toggleScreenRecordingPermissionsDialog} | ||||
|     > | ||||
|       <p>{i18n('calling__presenting--macos-permission-description')}</p> | ||||
|       <ol style={{ paddingLeft: 16 }}> | ||||
|         <li>{i18n('calling__presenting--permission-instruction-step1')}</li> | ||||
|         <li>{i18n('calling__presenting--permission-instruction-step2')}</li> | ||||
|         <li>{i18n('calling__presenting--permission-instruction-step3')}</li> | ||||
|       </ol> | ||||
|       <Modal.ButtonFooter> | ||||
|   const footer = ( | ||||
|     <> | ||||
|       <Button | ||||
|         onClick={toggleScreenRecordingPermissionsDialog} | ||||
|         ref={focusRef} | ||||
|  | @ -55,7 +42,23 @@ export const NeedsScreenRecordingPermissionsModal = ({ | |||
|       > | ||||
|         {i18n('calling__presenting--permission-open')} | ||||
|       </Button> | ||||
|       </Modal.ButtonFooter> | ||||
|     </> | ||||
|   ); | ||||
|   return ( | ||||
|     <Modal | ||||
|       modalName="NeedsScreenRecordingPermissionsModal" | ||||
|       i18n={i18n} | ||||
|       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 }}> | ||||
|         <li>{i18n('calling__presenting--permission-instruction-step1')}</li> | ||||
|         <li>{i18n('calling__presenting--permission-instruction-step2')}</li> | ||||
|         <li>{i18n('calling__presenting--permission-instruction-step3')}</li> | ||||
|       </ol> | ||||
|     </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,9 +255,63 @@ export const SendStoryModal = ({ | |||
|     Array<UUIDStringType> | ||||
|   >(initialMyStoriesMemberUuids); | ||||
| 
 | ||||
|   let content: JSX.Element; | ||||
|   let selectedNames: string | undefined; | ||||
|   if (page === Page.ChooseGroups) { | ||||
|     selectedNames = chosenGroupNames.join(', '); | ||||
|   } else { | ||||
|     selectedNames = selectedStoryNames | ||||
|       .map(listName => getStoryDistributionListName(i18n, listName, listName)) | ||||
|       .join(', '); | ||||
|   } | ||||
| 
 | ||||
|   const modalCommonProps: Pick<ModalPropsType, 'hasXButton' | 'i18n'> = { | ||||
|     hasXButton: true, | ||||
|     i18n, | ||||
|   }; | ||||
| 
 | ||||
|   let modal: RenderModalPage; | ||||
|   if (page === Page.SetMyStoriesPrivacy) { | ||||
|     content = ( | ||||
|     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} | ||||
|  | @ -302,10 +357,11 @@ export const SendStoryModal = ({ | |||
|           }} | ||||
|           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,27 +790,10 @@ 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]; | ||||
|  | @ -799,8 +810,48 @@ export const EditDistributionList = ({ | |||
|     }; | ||||
|   }; | ||||
| 
 | ||||
|   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 ( | ||||
|       <> | ||||
|     <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} | ||||
|  | @ -819,7 +870,7 @@ export const EditDistributionList = ({ | |||
|               acceptedMessageRequest={contact.acceptedMessageRequest} | ||||
|               avatarPath={contact.avatarPath} | ||||
|               color={contact.color} | ||||
|                 firstName={contact.systemGivenName ?? contact.firstName} | ||||
|               firstName={contact.firstName} | ||||
|               i18n={i18n} | ||||
|               id={contact.id} | ||||
|               isMe={contact.isMe} | ||||
|  | @ -870,35 +921,6 @@ export const EditDistributionList = ({ | |||
|           {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> | ||||
|         )} | ||||
|       </> | ||||
|     </ModalPage> | ||||
|   ); | ||||
|   } | ||||
| 
 | ||||
|   return null; | ||||
| }; | ||||
|  |  | |||
|  | @ -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,8 +200,12 @@ export const EditConversationAttributesModal: FunctionComponent<PropsType> = ({ | |||
|             {i18n('updateGroupAttributes__error-message')} | ||||
|           </div> | ||||
|         )} | ||||
|       </form> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|         <Modal.ButtonFooter> | ||||
|   const modalFooter = ( | ||||
|     <> | ||||
|       <Button | ||||
|         disabled={isRequestActive} | ||||
|         onClick={onClose} | ||||
|  | @ -211,6 +216,7 @@ export const EditConversationAttributesModal: FunctionComponent<PropsType> = ({ | |||
| 
 | ||||
|       <Button | ||||
|         type="submit" | ||||
|         form="edit-conversation-form" | ||||
|         variant={ButtonVariant.Primary} | ||||
|         disabled={!canSubmit} | ||||
|       > | ||||
|  | @ -220,19 +226,17 @@ export const EditConversationAttributesModal: FunctionComponent<PropsType> = ({ | |||
|           i18n('save') | ||||
|         )} | ||||
|       </Button> | ||||
|         </Modal.ButtonFooter> | ||||
|       </form> | ||||
|     </> | ||||
|   ); | ||||
|   } | ||||
| 
 | ||||
|   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
	
	 Alvaro
				Alvaro