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/ | // 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 { | .module-title-bar-drag-area { | ||||||
|   -webkit-app-region: drag; |   -webkit-app-region: drag; | ||||||
|   height: var(--title-bar-drag-area-height); |   height: var(--title-bar-drag-area-height); | ||||||
|  |  | ||||||
|  | @ -1,12 +1,6 @@ | ||||||
| // Copyright 2022 Signal Messenger, LLC | // Copyright 2022 Signal Messenger, LLC | ||||||
| // SPDX-License-Identifier: AGPL-3.0-only | // SPDX-License-Identifier: AGPL-3.0-only | ||||||
| 
 | 
 | ||||||
| div.AddUserToAnotherGroupModal__body { |  | ||||||
|   padding-left: 0; |  | ||||||
|   padding-bottom: 0; |  | ||||||
|   padding-right: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .AddUserToAnotherGroupModal { | .AddUserToAnotherGroupModal { | ||||||
|   &__main-body { |   &__main-body { | ||||||
|     display: flex; |     display: flex; | ||||||
|  |  | ||||||
|  | @ -9,8 +9,7 @@ | ||||||
| 
 | 
 | ||||||
|   user-select: none; |   user-select: none; | ||||||
| 
 | 
 | ||||||
|   // We use this selector for specificity. |   &__width-container { | ||||||
|   &.module-Modal { |  | ||||||
|     max-width: 420px; |     max-width: 420px; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -4,8 +4,7 @@ | ||||||
| .BadgeSustainerInstructionsDialog { | .BadgeSustainerInstructionsDialog { | ||||||
|   user-select: none; |   user-select: none; | ||||||
| 
 | 
 | ||||||
|   // We use this selector for specificity. |   &__width-container { | ||||||
|   &.module-Modal { |  | ||||||
|     max-width: 420px; |     max-width: 420px; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -2,27 +2,23 @@ | ||||||
| // SPDX-License-Identifier: AGPL-3.0-only | // SPDX-License-Identifier: AGPL-3.0-only | ||||||
| 
 | 
 | ||||||
| .module-CallingSelectPresentingSourcesModal { | .module-CallingSelectPresentingSourcesModal { | ||||||
|   // specificity |   &__width-container { | ||||||
|   &.module-Modal { |  | ||||||
|     max-width: 665px; |     max-width: 665px; | ||||||
|     position: relative; |     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; |     background-color: $color-gray-95; | ||||||
|     bottom: 0; |  | ||||||
|     margin-left: -16px; |  | ||||||
|     margin-top: 0; |  | ||||||
|     padding: 16px; |  | ||||||
|     position: absolute; |  | ||||||
|     width: 100%; |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   &__sources { |   &__sources { | ||||||
|     margin-bottom: 20px; |     margin-bottom: 34px; | ||||||
|     margin-left: -6px; |     display: flex; | ||||||
|     margin-right: -6px; |     flex-wrap: wrap; | ||||||
|  |     align-items: center; | ||||||
|  |     gap: 12px; | ||||||
| 
 | 
 | ||||||
|     &:last-child { |     &:last-child { | ||||||
|       margin-bottom: 0; |       margin-bottom: 0; | ||||||
|  | @ -38,9 +34,6 @@ | ||||||
| 
 | 
 | ||||||
|     border-radius: 4px; |     border-radius: 4px; | ||||||
|     border: 1px solid $color-gray-60; |     border: 1px solid $color-gray-60; | ||||||
|     margin-bottom: 14px; |  | ||||||
|     margin-left: 6px; |  | ||||||
|     margin-right: 6px; |  | ||||||
|     overflow: hidden; |     overflow: hidden; | ||||||
|     padding: 8px; |     padding: 8px; | ||||||
|     text-align: center; |     text-align: center; | ||||||
|  |  | ||||||
|  | @ -71,8 +71,4 @@ | ||||||
|     height: 24px; |     height: 24px; | ||||||
|     width: 24px; |     width: 24px; | ||||||
|   } |   } | ||||||
| 
 |  | ||||||
|   &__modal__body { |  | ||||||
|     overflow-x: hidden !important; |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -7,6 +7,7 @@ | ||||||
|   flex-direction: column; |   flex-direction: column; | ||||||
|   justify-content: center; |   justify-content: center; | ||||||
|   margin-top: 4px; |   margin-top: 4px; | ||||||
|  |   margin-bottom: 16px; | ||||||
| 
 | 
 | ||||||
|   &__name { |   &__name { | ||||||
|     @include font-title-2; |     @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 { | .module-Modal { | ||||||
|   @include popper-shadow(); |   @include popper-shadow(); | ||||||
|   border-radius: 8px; |   border-radius: 8px; | ||||||
|  |   overflow: hidden; | ||||||
|   // We need this to be a number not divisible by 5 so that if we have sticky |   // 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. |   // buttons the bottom doesn't bleed through by 1px. | ||||||
|   max-height: 89vh; |   max-height: 89vh; | ||||||
|  | @ -23,9 +24,7 @@ | ||||||
|     align-items: center; |     align-items: center; | ||||||
|     display: flex; |     display: flex; | ||||||
|     justify-content: space-between; |     justify-content: space-between; | ||||||
|     margin-bottom: 1em; |     padding: 16px 16px 1em 16px; | ||||||
|     padding: 16px 16px 0 16px; |  | ||||||
|     position: sticky; |  | ||||||
| 
 | 
 | ||||||
|     &--with-back-button .module-Modal__title { |     &--with-back-button .module-Modal__title { | ||||||
|       text-align: center; |       text-align: center; | ||||||
|  | @ -132,16 +131,20 @@ | ||||||
|     @include scrollbar; |     @include scrollbar; | ||||||
|     @include font-body-1; |     @include font-body-1; | ||||||
|     margin: 0; |     margin: 0; | ||||||
|     padding: 16px; |     overflow-y: overlay; | ||||||
|  |     overflow-x: auto; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   &--padded { | ||||||
|  |     .module-Modal__body { | ||||||
|  |       padding: 16px; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   &--has-header { |   &--has-header { | ||||||
|     .module-Modal__body { |     .module-Modal__body { | ||||||
|       padding-top: 0; |       padding-top: 0; | ||||||
|       border-top: 1px solid transparent; |       border-top: 1px solid transparent; | ||||||
|       // If there's a header, just the body scrolls |  | ||||||
|       overflow-y: overlay; |  | ||||||
|       overflow-x: auto; |  | ||||||
| 
 | 
 | ||||||
|       &--scrolled { |       &--scrolled { | ||||||
|         @include light-theme { |         @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 { |   &__button-footer { | ||||||
|     display: flex; |     display: flex; | ||||||
|     flex-wrap: wrap; |     flex-wrap: wrap; | ||||||
|     justify-content: flex-end; |     justify-content: flex-end; | ||||||
|     margin-top: 8px; |     align-items: center; | ||||||
|  |     padding: 1em 16px 16px 16px; | ||||||
|  |     gap: 8px; | ||||||
| 
 | 
 | ||||||
|     .module-Button { |     .module-Button { | ||||||
|       margin-left: 8px; |       margin-left: 8px; | ||||||
|       margin-top: 8px; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     &--one-button-per-line { |     &--one-button-per-line { | ||||||
|       flex-direction: column; |       flex-direction: column; | ||||||
|       align-items: flex-end; |       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 |   // Overrides for a modal with important message | ||||||
|  | @ -251,6 +211,7 @@ | ||||||
|       margin-top: 27px; |       margin-top: 27px; | ||||||
|       flex-grow: 0; |       flex-grow: 0; | ||||||
|       flex-shrink: 0; |       flex-shrink: 0; | ||||||
|  |       padding: 0 12px 4px 12px; | ||||||
| 
 | 
 | ||||||
|       .module-Button { |       .module-Button { | ||||||
|         flex-grow: 1; |         flex-grow: 1; | ||||||
|  |  | ||||||
|  | @ -2,6 +2,27 @@ | ||||||
| // SPDX-License-Identifier: AGPL-3.0-only | // SPDX-License-Identifier: AGPL-3.0-only | ||||||
| 
 | 
 | ||||||
| .SendStoryModal { | .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 { |   &__top-bar { | ||||||
|     align-items: center; |     align-items: center; | ||||||
|     display: flex; |     display: flex; | ||||||
|  | @ -85,7 +106,6 @@ | ||||||
|       justify-content: space-between; |       justify-content: space-between; | ||||||
|       margin: 8px 0; |       margin: 8px 0; | ||||||
|       user-select: none; |       user-select: none; | ||||||
|       width: 100%; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     &__info { |     &__info { | ||||||
|  | @ -164,8 +184,9 @@ | ||||||
|   &__selected-lists { |   &__selected-lists { | ||||||
|     @include font-body-2; |     @include font-body-2; | ||||||
|     color: $color-gray-15; |     color: $color-gray-15; | ||||||
|     max-width: 280px; |     padding-right: 16px; | ||||||
|     user-select: none; |     user-select: none; | ||||||
|  |     flex: 1; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   &__ok { |   &__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 | // SPDX-License-Identifier: AGPL-3.0-only | ||||||
| 
 | 
 | ||||||
| .StoriesSettingsModal { | .StoriesSettingsModal { | ||||||
|  |   &__modal__body { | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: column; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   &__conversation-list { |   &__conversation-list { | ||||||
|     .module-conversation-list, |     .module-conversation-list { | ||||||
|     .module-conversation-list__item--contact-or-conversation { |  | ||||||
|       padding-left: 0; |       padding-left: 0; | ||||||
|       padding-right: 0; |       padding-right: 0; | ||||||
|     } |     } | ||||||
|  | @ -194,20 +198,6 @@ | ||||||
|     overflow: hidden; |     overflow: hidden; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   &__search { |  | ||||||
|     &__container { |  | ||||||
|       margin-left: 0; |  | ||||||
|       margin-right: 0; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   &__tags { |  | ||||||
|     margin: 0 -4px; |  | ||||||
| 
 |  | ||||||
|     // Override .module-ContactPills |  | ||||||
|     padding: 0; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   &__name-story-avatar-container { |   &__name-story-avatar-container { | ||||||
|     align-items: center; |     align-items: center; | ||||||
|     display: flex; |     display: flex; | ||||||
|  |  | ||||||
|  | @ -148,6 +148,7 @@ export const AddUserToAnotherGroupModal = ({ | ||||||
|           onClose={toggleAddUserToAnotherGroupModal} |           onClose={toggleAddUserToAnotherGroupModal} | ||||||
|           title={i18n('AddUserToAnotherGroupModal__title')} |           title={i18n('AddUserToAnotherGroupModal__title')} | ||||||
|           moduleClassName="AddUserToAnotherGroupModal" |           moduleClassName="AddUserToAnotherGroupModal" | ||||||
|  |           padded={false} | ||||||
|         > |         > | ||||||
|           <div className="AddUserToAnotherGroupModal__main-body"> |           <div className="AddUserToAnotherGroupModal__main-body"> | ||||||
|             <SearchInput |             <SearchInput | ||||||
|  |  | ||||||
|  | @ -21,10 +21,15 @@ export const Alert: FunctionComponent<PropsType> = ({ | ||||||
|   onClose, |   onClose, | ||||||
|   title, |   title, | ||||||
| }) => ( | }) => ( | ||||||
|   <Modal modalName="Alert" i18n={i18n} onClose={onClose} title={title}> |   <Modal | ||||||
|     {body} |     modalName="Alert" | ||||||
|     <Modal.ButtonFooter> |     i18n={i18n} | ||||||
|  |     onClose={onClose} | ||||||
|  |     title={title} | ||||||
|  |     modalFooter={ | ||||||
|       <Button onClick={onClose}>{i18n('Confirmation--confirm')}</Button> |       <Button onClick={onClose}>{i18n('Confirmation--confirm')}</Button> | ||||||
|     </Modal.ButtonFooter> |     } | ||||||
|  |   > | ||||||
|  |     {body} | ||||||
|   </Modal> |   </Modal> | ||||||
| ); | ); | ||||||
|  |  | ||||||
|  | @ -58,6 +58,7 @@ type PropsType = { | ||||||
|     } |     } | ||||||
|   | { |   | { | ||||||
|       type: 'submit'; |       type: 'submit'; | ||||||
|  |       form?: string; | ||||||
|     } |     } | ||||||
| ) & | ) & | ||||||
|   ( |   ( | ||||||
|  | @ -117,12 +118,14 @@ export const Button = React.forwardRef<HTMLButtonElement, PropsType>( | ||||||
| 
 | 
 | ||||||
|     let onClick: undefined | MouseEventHandler<HTMLButtonElement>; |     let onClick: undefined | MouseEventHandler<HTMLButtonElement>; | ||||||
|     let type: 'button' | 'submit'; |     let type: 'button' | 'submit'; | ||||||
|  |     let form; | ||||||
|     if ('onClick' in props) { |     if ('onClick' in props) { | ||||||
|       ({ onClick } = props); |       ({ onClick } = props); | ||||||
|       type = 'button'; |       type = 'button'; | ||||||
|     } else { |     } else { | ||||||
|       onClick = undefined; |       onClick = undefined; | ||||||
|       ({ type } = props); |       ({ type } = props); | ||||||
|  |       ({ form } = props); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const sizeClassName = SIZE_CLASS_NAMES.get(size); |     const sizeClassName = SIZE_CLASS_NAMES.get(size); | ||||||
|  | @ -143,6 +146,7 @@ export const Button = React.forwardRef<HTMLButtonElement, PropsType>( | ||||||
|         )} |         )} | ||||||
|         disabled={disabled} |         disabled={disabled} | ||||||
|         onClick={onClick} |         onClick={onClick} | ||||||
|  |         form={form} | ||||||
|         ref={ref} |         ref={ref} | ||||||
|         style={style} |         style={style} | ||||||
|         tabIndex={tabIndex} |         tabIndex={tabIndex} | ||||||
|  |  | ||||||
|  | @ -140,6 +140,7 @@ export const CallingDeviceSelection = ({ | ||||||
|       i18n={i18n} |       i18n={i18n} | ||||||
|       theme={Theme.Dark} |       theme={Theme.Dark} | ||||||
|       onClose={toggleSettings} |       onClose={toggleSettings} | ||||||
|  |       padded={false} | ||||||
|     > |     > | ||||||
|       <div className="module-calling-device-selection"> |       <div className="module-calling-device-selection"> | ||||||
|         <button |         <button | ||||||
|  |  | ||||||
|  | @ -82,6 +82,20 @@ export const CallingSelectPresentingSourcesModal = ({ | ||||||
|     source => source.isScreen |     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 ( |   return ( | ||||||
|     <Modal |     <Modal | ||||||
|       modalName="CallingSelectPresentingSourcesModal" |       modalName="CallingSelectPresentingSourcesModal" | ||||||
|  | @ -93,6 +107,7 @@ export const CallingSelectPresentingSourcesModal = ({ | ||||||
|       }} |       }} | ||||||
|       theme={Theme.Dark} |       theme={Theme.Dark} | ||||||
|       title={i18n('calling__SelectPresentingSourcesModal--title')} |       title={i18n('calling__SelectPresentingSourcesModal--title')} | ||||||
|  |       modalFooter={footer} | ||||||
|     > |     > | ||||||
|       <div className="module-CallingSelectPresentingSourcesModal__title"> |       <div className="module-CallingSelectPresentingSourcesModal__title"> | ||||||
|         {i18n('calling__SelectPresentingSourcesModal--entireScreen')} |         {i18n('calling__SelectPresentingSourcesModal--entireScreen')} | ||||||
|  | @ -120,20 +135,6 @@ export const CallingSelectPresentingSourcesModal = ({ | ||||||
|           /> |           /> | ||||||
|         ))} |         ))} | ||||||
|       </div> |       </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> |     </Modal> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -34,6 +34,16 @@ export function CaptchaDialog(props: Readonly<PropsType>): JSX.Element { | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   if (isClosing && !isPending) { |   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 ( |     return ( | ||||||
|       <Modal |       <Modal | ||||||
|         modalName="CaptchaDialog" |         modalName="CaptchaDialog" | ||||||
|  | @ -42,18 +52,11 @@ export function CaptchaDialog(props: Readonly<PropsType>): JSX.Element { | ||||||
|         title={i18n('CaptchaDialog--can-close__title')} |         title={i18n('CaptchaDialog--can-close__title')} | ||||||
|         onClose={() => setIsClosing(false)} |         onClose={() => setIsClosing(false)} | ||||||
|         key="skip" |         key="skip" | ||||||
|  |         modalFooter={footer} | ||||||
|       > |       > | ||||||
|         <section> |         <section> | ||||||
|           <p>{i18n('CaptchaDialog--can-close__body')}</p> |           <p>{i18n('CaptchaDialog--can-close__body')}</p> | ||||||
|         </section> |         </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> |       </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 ( |   return ( | ||||||
|     <Modal |     <Modal | ||||||
|       modalName="CaptchaDialog.pending" |       modalName="CaptchaDialog.pending" | ||||||
|  | @ -80,25 +98,12 @@ export function CaptchaDialog(props: Readonly<PropsType>): JSX.Element { | ||||||
|       hasXButton |       hasXButton | ||||||
|       onClose={() => setIsClosing(true)} |       onClose={() => setIsClosing(true)} | ||||||
|       key="primary" |       key="primary" | ||||||
|  |       modalFooter={footer} | ||||||
|     > |     > | ||||||
|       <section> |       <section> | ||||||
|         <p>{i18n('CaptchaDialog__first-paragraph')}</p> |         <p>{i18n('CaptchaDialog__first-paragraph')}</p> | ||||||
|         <p>{i18n('CaptchaDialog__second-paragraph')}</p> |         <p>{i18n('CaptchaDialog__second-paragraph')}</p> | ||||||
|       </section> |       </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> |     </Modal> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -7,7 +7,7 @@ import { animated } from '@react-spring/web'; | ||||||
| import { Button, ButtonVariant } from './Button'; | import { Button, ButtonVariant } from './Button'; | ||||||
| import type { LocalizerType } from '../types/Util'; | import type { LocalizerType } from '../types/Util'; | ||||||
| import { ModalHost } from './ModalHost'; | import { ModalHost } from './ModalHost'; | ||||||
| import { Modal, ModalWindow } from './Modal'; | import { ModalPage } from './Modal'; | ||||||
| import type { Theme } from '../util/theme'; | import type { Theme } from '../util/theme'; | ||||||
| import { useAnimated } from '../hooks/useAnimated'; | import { useAnimated } from '../hooks/useAnimated'; | ||||||
| 
 | 
 | ||||||
|  | @ -96,6 +96,34 @@ export const ConfirmationDialog = React.memo( | ||||||
| 
 | 
 | ||||||
|     const hasActions = Boolean(actions.length); |     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}`; |     const modalName = `ConfirmationDialog.${dialogName}`; | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|  | @ -108,41 +136,17 @@ export const ConfirmationDialog = React.memo( | ||||||
|         theme={theme} |         theme={theme} | ||||||
|       > |       > | ||||||
|         <animated.div style={modalStyles}> |         <animated.div style={modalStyles}> | ||||||
|           <ModalWindow |           <ModalPage | ||||||
|             modalName={modalName} |             modalName={modalName} | ||||||
|             hasXButton={hasXButton} |             hasXButton={hasXButton} | ||||||
|             i18n={i18n} |             i18n={i18n} | ||||||
|             moduleClassName={moduleClassName} |             moduleClassName={moduleClassName} | ||||||
|             onClose={cancelAndClose} |             onClose={cancelAndClose} | ||||||
|             title={title} |             title={title} | ||||||
|  |             modalFooter={footer} | ||||||
|           > |           > | ||||||
|             {children} |             {children} | ||||||
|             <Modal.ButtonFooter> |           </ModalPage> | ||||||
|               <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> |  | ||||||
|         </animated.div> |         </animated.div> | ||||||
|       </ModalHost> |       </ModalHost> | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|  | @ -33,6 +33,30 @@ export function CrashReportDialog(props: Readonly<PropsType>): JSX.Element { | ||||||
|     uploadCrashReports(); |     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 ( |   return ( | ||||||
|     <Modal |     <Modal | ||||||
|       modalName="CrashReportDialog" |       modalName="CrashReportDialog" | ||||||
|  | @ -41,29 +65,9 @@ export function CrashReportDialog(props: Readonly<PropsType>): JSX.Element { | ||||||
|       title={i18n('CrashReportDialog__title')} |       title={i18n('CrashReportDialog__title')} | ||||||
|       hasXButton |       hasXButton | ||||||
|       onClose={eraseCrashReports} |       onClose={eraseCrashReports} | ||||||
|  |       modalFooter={footer} | ||||||
|     > |     > | ||||||
|       <section>{i18n('CrashReportDialog__body')}</section> |       <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> |     </Modal> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -104,6 +104,38 @@ export function CustomizingPreferredReactionsModal({ | ||||||
|     ); |     ); | ||||||
|   const canSave = !isSaving && hasChanged; |   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 ( |   return ( | ||||||
|     <Modal |     <Modal | ||||||
|       modalName="CustomizingPreferredReactionsModal" |       modalName="CustomizingPreferredReactionsModal" | ||||||
|  | @ -114,6 +146,7 @@ export function CustomizingPreferredReactionsModal({ | ||||||
|         cancelCustomizePreferredReactionsModal(); |         cancelCustomizePreferredReactionsModal(); | ||||||
|       }} |       }} | ||||||
|       title={i18n('CustomizingPreferredReactions__title')} |       title={i18n('CustomizingPreferredReactions__title')} | ||||||
|  |       modalFooter={footer} | ||||||
|     > |     > | ||||||
|       <div className="module-CustomizingPreferredReactionsModal__small-emoji-picker-wrapper"> |       <div className="module-CustomizingPreferredReactionsModal__small-emoji-picker-wrapper"> | ||||||
|         <ReactionPickerPicker |         <ReactionPickerPicker | ||||||
|  | @ -163,35 +196,6 @@ export function CustomizingPreferredReactionsModal({ | ||||||
|           /> |           /> | ||||||
|         </div> |         </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> |     </Modal> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -25,27 +25,23 @@ function focusRef(el: HTMLElement | null) { | ||||||
| export const ErrorModal = (props: PropsType): JSX.Element => { | export const ErrorModal = (props: PropsType): JSX.Element => { | ||||||
|   const { buttonText, description, i18n, onClose, title } = props; |   const { buttonText, description, i18n, onClose, title } = props; | ||||||
| 
 | 
 | ||||||
|  |   const footer = ( | ||||||
|  |     <Button onClick={onClose} ref={focusRef} variant={ButtonVariant.Secondary}> | ||||||
|  |       {buttonText || i18n('Confirmation--confirm')} | ||||||
|  |     </Button> | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|   return ( |   return ( | ||||||
|     <Modal |     <Modal | ||||||
|       modalName="ErrorModal" |       modalName="ErrorModal" | ||||||
|       i18n={i18n} |       i18n={i18n} | ||||||
|       onClose={onClose} |       onClose={onClose} | ||||||
|       title={title || i18n('ErrorModal--title')} |       title={title || i18n('ErrorModal--title')} | ||||||
|  |       modalFooter={footer} | ||||||
|     > |     > | ||||||
|       <> |       <div className="module-error-modal__description"> | ||||||
|         <div className="module-error-modal__description"> |         {description || i18n('ErrorModal--description')} | ||||||
|           {description || i18n('ErrorModal--description')} |       </div> | ||||||
|         </div> |  | ||||||
|         <Modal.ButtonFooter> |  | ||||||
|           <Button |  | ||||||
|             onClick={onClose} |  | ||||||
|             ref={focusRef} |  | ||||||
|             variant={ButtonVariant.Secondary} |  | ||||||
|           > |  | ||||||
|             {buttonText || i18n('Confirmation--confirm')} |  | ||||||
|           </Button> |  | ||||||
|         </Modal.ButtonFooter> |  | ||||||
|       </> |  | ||||||
|     </Modal> |     </Modal> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -46,14 +46,15 @@ BareBonesLong.story = { | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const BareBonesLongWithButton = (): JSX.Element => ( | 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> | ||||||
|     <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> |   </Modal> | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
|  | @ -68,11 +69,9 @@ export const TitleXButtonBodyAndButtonFooter = (): JSX.Element => ( | ||||||
|     title="Hello world" |     title="Hello world" | ||||||
|     onClose={onClose} |     onClose={onClose} | ||||||
|     hasXButton |     hasXButton | ||||||
|  |     modalFooter={<Button onClick={noop}>Okay</Button>} | ||||||
|   > |   > | ||||||
|     {LOREM_IPSUM} |     {LOREM_IPSUM} | ||||||
|     <Modal.ButtonFooter> |  | ||||||
|       <Button onClick={noop}>Okay</Button> |  | ||||||
|     </Modal.ButtonFooter> |  | ||||||
|   </Modal> |   </Modal> | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
|  | @ -81,21 +80,27 @@ TitleXButtonBodyAndButtonFooter.story = { | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const LotsOfButtonsInTheFooter = (): JSX.Element => ( | 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! |     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> |   </Modal> | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
|  | @ -123,14 +128,17 @@ LongBodyWithTitle.story = { | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const LongBodyWithTitleAndButton = (): JSX.Element => ( | 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> | ||||||
|     <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> |   </Modal> | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
|  | @ -160,19 +168,20 @@ LongBodyWithLongTitleAndXButton.story = { | ||||||
| export const WithStickyButtonsLongBody = (): JSX.Element => ( | export const WithStickyButtonsLongBody = (): JSX.Element => ( | ||||||
|   <Modal |   <Modal | ||||||
|     modalName="test" |     modalName="test" | ||||||
|     hasStickyButtons |  | ||||||
|     hasXButton |     hasXButton | ||||||
|     i18n={i18n} |     i18n={i18n} | ||||||
|     onClose={onClose} |     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> | ||||||
|     <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> |   </Modal> | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
|  | @ -183,16 +192,17 @@ WithStickyButtonsLongBody.story = { | ||||||
| export const WithStickyButtonsShortBody = (): JSX.Element => ( | export const WithStickyButtonsShortBody = (): JSX.Element => ( | ||||||
|   <Modal |   <Modal | ||||||
|     modalName="test" |     modalName="test" | ||||||
|     hasStickyButtons |  | ||||||
|     hasXButton |     hasXButton | ||||||
|     i18n={i18n} |     i18n={i18n} | ||||||
|     onClose={onClose} |     onClose={onClose} | ||||||
|  |     modalFooter={ | ||||||
|  |       <> | ||||||
|  |         <Button onClick={noop}>Okay</Button> | ||||||
|  |         <Button onClick={noop}>Okay</Button> | ||||||
|  |       </> | ||||||
|  |     } | ||||||
|   > |   > | ||||||
|     <p>{LOREM_IPSUM.slice(0, 140)}</p> |     <p>{LOREM_IPSUM.slice(0, 140)}</p> | ||||||
|     <Modal.ButtonFooter> |  | ||||||
|       <Button onClick={noop}>Okay</Button> |  | ||||||
|       <Button onClick={noop}>Okay</Button> |  | ||||||
|     </Modal.ButtonFooter> |  | ||||||
|   </Modal> |   </Modal> | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
|  | @ -203,25 +213,26 @@ WithStickyButtonsShortBody.story = { | ||||||
| export const StickyFooterLotsOfButtons = (): JSX.Element => ( | export const StickyFooterLotsOfButtons = (): JSX.Element => ( | ||||||
|   <Modal |   <Modal | ||||||
|     modalName="test" |     modalName="test" | ||||||
|     hasStickyButtons |  | ||||||
|     i18n={i18n} |     i18n={i18n} | ||||||
|     onClose={onClose} |     onClose={onClose} | ||||||
|     title="OK" |     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> |     <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> |   </Modal> | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -20,7 +20,6 @@ import { useRefMerger } from '../hooks/useRefMerger'; | ||||||
| type PropsType = { | type PropsType = { | ||||||
|   children: ReactNode; |   children: ReactNode; | ||||||
|   modalName: string; |   modalName: string; | ||||||
|   hasStickyButtons?: boolean; |  | ||||||
|   hasXButton?: boolean; |   hasXButton?: boolean; | ||||||
|   i18n: LocalizerType; |   i18n: LocalizerType; | ||||||
|   modalFooter?: JSX.Element; |   modalFooter?: JSX.Element; | ||||||
|  | @ -29,9 +28,10 @@ type PropsType = { | ||||||
|   onClose?: () => void; |   onClose?: () => void; | ||||||
|   title?: ReactNode; |   title?: ReactNode; | ||||||
|   useFocusTrap?: boolean; |   useFocusTrap?: boolean; | ||||||
|  |   padded?: boolean; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| type ModalPropsType = PropsType & { | export type ModalPropsType = PropsType & { | ||||||
|   noMouseClose?: boolean; |   noMouseClose?: boolean; | ||||||
|   theme?: Theme; |   theme?: Theme; | ||||||
| }; | }; | ||||||
|  | @ -41,7 +41,6 @@ const BASE_CLASS_NAME = 'module-Modal'; | ||||||
| export function Modal({ | export function Modal({ | ||||||
|   children, |   children, | ||||||
|   modalName, |   modalName, | ||||||
|   hasStickyButtons, |  | ||||||
|   hasXButton, |   hasXButton, | ||||||
|   i18n, |   i18n, | ||||||
|   modalFooter, |   modalFooter, | ||||||
|  | @ -52,6 +51,7 @@ export function Modal({ | ||||||
|   theme, |   theme, | ||||||
|   title, |   title, | ||||||
|   useFocusTrap, |   useFocusTrap, | ||||||
|  |   padded = true, | ||||||
| }: Readonly<ModalPropsType>): ReactElement { | }: Readonly<ModalPropsType>): ReactElement { | ||||||
|   const { close, modalStyles, overlayStyles } = useAnimated(onClose, { |   const { close, modalStyles, overlayStyles } = useAnimated(onClose, { | ||||||
|     getFrom: () => ({ opacity: 0, transform: 'translateY(48px)' }), |     getFrom: () => ({ opacity: 0, transform: 'translateY(48px)' }), | ||||||
|  | @ -72,9 +72,8 @@ export function Modal({ | ||||||
|       useFocusTrap={useFocusTrap} |       useFocusTrap={useFocusTrap} | ||||||
|     > |     > | ||||||
|       <animated.div style={modalStyles}> |       <animated.div style={modalStyles}> | ||||||
|         <ModalWindow |         <ModalPage | ||||||
|           modalName={modalName} |           modalName={modalName} | ||||||
|           hasStickyButtons={hasStickyButtons} |  | ||||||
|           hasXButton={hasXButton} |           hasXButton={hasXButton} | ||||||
|           i18n={i18n} |           i18n={i18n} | ||||||
|           modalFooter={modalFooter} |           modalFooter={modalFooter} | ||||||
|  | @ -82,25 +81,46 @@ export function Modal({ | ||||||
|           onBackButtonClick={onBackButtonClick} |           onBackButtonClick={onBackButtonClick} | ||||||
|           onClose={close} |           onClose={close} | ||||||
|           title={title} |           title={title} | ||||||
|  |           padded={padded} | ||||||
|         > |         > | ||||||
|           {children} |           {children} | ||||||
|         </ModalWindow> |         </ModalPage> | ||||||
|       </animated.div> |       </animated.div> | ||||||
|     </ModalHost> |     </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, |   children, | ||||||
|   hasStickyButtons, |  | ||||||
|   hasXButton, |   hasXButton, | ||||||
|   i18n, |   i18n, | ||||||
|   modalFooter, |   modalFooter, | ||||||
|   moduleClassName, |   moduleClassName, | ||||||
|   onBackButtonClick, |   onBackButtonClick, | ||||||
|   onClose = noop, |   onClose, | ||||||
|   title, |   title, | ||||||
| }: Readonly<PropsType>): JSX.Element { |   padded = true, | ||||||
|  | }: ModalPageProps): JSX.Element { | ||||||
|   const modalRef = useRef<HTMLDivElement | null>(null); |   const modalRef = useRef<HTMLDivElement | null>(null); | ||||||
| 
 | 
 | ||||||
|   const refMerger = useRefMerger(); |   const refMerger = useRefMerger(); | ||||||
|  | @ -131,7 +151,7 @@ export function ModalWindow({ | ||||||
|         className={classNames( |         className={classNames( | ||||||
|           getClassName(''), |           getClassName(''), | ||||||
|           getClassName(hasHeader ? '--has-header' : '--no-header'), |           getClassName(hasHeader ? '--has-header' : '--no-header'), | ||||||
|           hasStickyButtons && getClassName('--sticky-buttons') |           padded && getClassName('--padded') | ||||||
|         )} |         )} | ||||||
|         ref={modalRef} |         ref={modalRef} | ||||||
|         onClick={event => { |         onClick={event => { | ||||||
|  | @ -200,7 +220,7 @@ export function ModalWindow({ | ||||||
|             </div> |             </div> | ||||||
|           )} |           )} | ||||||
|         </Measure> |         </Measure> | ||||||
|         {modalFooter} |         {modalFooter && <Modal.ButtonFooter>{modalFooter}</Modal.ButtonFooter>} | ||||||
|       </div> |       </div> | ||||||
|     </> |     </> | ||||||
|   ); |   ); | ||||||
|  | @ -208,17 +228,12 @@ export function ModalWindow({ | ||||||
| 
 | 
 | ||||||
| Modal.ButtonFooter = function ButtonFooter({ | Modal.ButtonFooter = function ButtonFooter({ | ||||||
|   children, |   children, | ||||||
|   moduleClassName, |  | ||||||
| }: Readonly<{ | }: Readonly<{ | ||||||
|   children: ReactNode; |   children: ReactNode; | ||||||
|   moduleClassName?: string; |  | ||||||
| }>): ReactElement { | }>): ReactElement { | ||||||
|   const [ref, hasWrapped] = useHasWrapped<HTMLDivElement>(); |   const [ref, hasWrapped] = useHasWrapped<HTMLDivElement>(); | ||||||
| 
 | 
 | ||||||
|   const className = getClassNamesFor( |   const className = getClassNamesFor(BASE_CLASS_NAME)('__button-footer'); | ||||||
|     BASE_CLASS_NAME, |  | ||||||
|     moduleClassName |  | ||||||
|   )('__button-footer'); |  | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <div |     <div | ||||||
|  | @ -232,3 +247,55 @@ Modal.ButtonFooter = function ButtonFooter({ | ||||||
|     </div> |     </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, |   openSystemPreferencesAction, | ||||||
|   toggleScreenRecordingPermissionsDialog, |   toggleScreenRecordingPermissionsDialog, | ||||||
| }: PropsType): JSX.Element => { | }: 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 ( |   return ( | ||||||
|     <Modal |     <Modal | ||||||
|       modalName="NeedsScreenRecordingPermissionsModal" |       modalName="NeedsScreenRecordingPermissionsModal" | ||||||
|  | @ -31,6 +51,7 @@ export const NeedsScreenRecordingPermissionsModal = ({ | ||||||
|       title={i18n('calling__presenting--permission-title')} |       title={i18n('calling__presenting--permission-title')} | ||||||
|       theme={Theme.Dark} |       theme={Theme.Dark} | ||||||
|       onClose={toggleScreenRecordingPermissionsDialog} |       onClose={toggleScreenRecordingPermissionsDialog} | ||||||
|  |       modalFooter={footer} | ||||||
|     > |     > | ||||||
|       <p>{i18n('calling__presenting--macos-permission-description')}</p> |       <p>{i18n('calling__presenting--macos-permission-description')}</p> | ||||||
|       <ol style={{ paddingLeft: 16 }}> |       <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-step2')}</li> | ||||||
|         <li>{i18n('calling__presenting--permission-instruction-step3')}</li> |         <li>{i18n('calling__presenting--permission-instruction-step3')}</li> | ||||||
|       </ol> |       </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> |     </Modal> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -61,7 +61,6 @@ export const ProfileEditorModal = ({ | ||||||
|     <> |     <> | ||||||
|       <Modal |       <Modal | ||||||
|         modalName="ProfileEditorModal" |         modalName="ProfileEditorModal" | ||||||
|         hasStickyButtons |  | ||||||
|         hasXButton |         hasXButton | ||||||
|         i18n={i18n} |         i18n={i18n} | ||||||
|         onClose={toggleProfileEditor} |         onClose={toggleProfileEditor} | ||||||
|  |  | ||||||
|  | @ -19,13 +19,14 @@ import { Checkbox } from './Checkbox'; | ||||||
| import { ConfirmationDialog } from './ConfirmationDialog'; | import { ConfirmationDialog } from './ConfirmationDialog'; | ||||||
| import { ContextMenu } from './ContextMenu'; | import { ContextMenu } from './ContextMenu'; | ||||||
| import { | import { | ||||||
|   DistributionListSettings, |   DistributionListSettingsModal, | ||||||
|   EditDistributionList, |   EditDistributionListModal, | ||||||
|   EditMyStoriesPrivacy, |   EditMyStoriesPrivacy, | ||||||
|   Page as StoriesSettingsPage, |   Page as StoriesSettingsPage, | ||||||
| } from './StoriesSettingsModal'; | } from './StoriesSettingsModal'; | ||||||
| import { MY_STORIES_ID, getStoryDistributionListName } from '../types/Stories'; | 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 { StoryDistributionListName } from './StoryDistributionListName'; | ||||||
| import { Theme } from '../util/theme'; | import { Theme } from '../util/theme'; | ||||||
| import { isNotNil } from '../util/isNotNil'; | import { isNotNil } from '../util/isNotNil'; | ||||||
|  | @ -254,58 +255,113 @@ export const SendStoryModal = ({ | ||||||
|     Array<UUIDStringType> |     Array<UUIDStringType> | ||||||
|   >(initialMyStoriesMemberUuids); |   >(initialMyStoriesMemberUuids); | ||||||
| 
 | 
 | ||||||
|   let content: JSX.Element; |   let selectedNames: string | undefined; | ||||||
|   if (page === Page.SetMyStoriesPrivacy) { |   if (page === Page.ChooseGroups) { | ||||||
|     content = ( |     selectedNames = chosenGroupNames.join(', '); | ||||||
|       <EditMyStoriesPrivacy |   } else { | ||||||
|         hasDisclaimerAbove |     selectedNames = selectedStoryNames | ||||||
|         i18n={i18n} |       .map(listName => getStoryDistributionListName(i18n, listName, listName)) | ||||||
|         learnMore="SendStoryModal__privacy-disclaimer" |       .join(', '); | ||||||
|         myStories={stagedMyStories} |   } | ||||||
|         onClickExclude={() => { |  | ||||||
|           let nextSelectedContacts = stagedMyStories.members; |  | ||||||
| 
 | 
 | ||||||
|           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 => ({ |             setStagedMyStories(myStories => ({ | ||||||
|               ...myStories, |               ...myStories, | ||||||
|               isBlockList: true, |               isBlockList: true, | ||||||
|               members: [], |               members: [], | ||||||
|             })); |             })); | ||||||
|             nextSelectedContacts = []; |             setSelectedContacts([]); | ||||||
|           } |           }} | ||||||
| 
 |           toggleSignalConnectionsModal={toggleSignalConnectionsModal} | ||||||
|           setSelectedContacts(nextSelectedContacts); |         /> | ||||||
| 
 |       </ModalPage> | ||||||
|           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} |  | ||||||
|       /> |  | ||||||
|     ); |     ); | ||||||
|   } else if (page === Page.EditingDistributionList && listToEdit) { |   } else if (page === Page.EditingDistributionList && listToEdit) { | ||||||
|     content = ( |     modal = handleClose => ( | ||||||
|       <DistributionListSettings |       <DistributionListSettingsModal | ||||||
|         getPreferredBadge={getPreferredBadge} |         getPreferredBadge={getPreferredBadge} | ||||||
|         i18n={i18n} |         i18n={i18n} | ||||||
|         listToEdit={listToEdit} |         listToEdit={listToEdit} | ||||||
|  | @ -316,6 +372,8 @@ export const SendStoryModal = ({ | ||||||
|         setPage={setPage} |         setPage={setPage} | ||||||
|         setSelectedContacts={setSelectedContacts} |         setSelectedContacts={setSelectedContacts} | ||||||
|         toggleSignalConnectionsModal={toggleSignalConnectionsModal} |         toggleSignalConnectionsModal={toggleSignalConnectionsModal} | ||||||
|  |         onBackButtonClick={() => setListIdToEdit(undefined)} | ||||||
|  |         onClose={handleClose} | ||||||
|       /> |       /> | ||||||
|     ); |     ); | ||||||
|   } else if ( |   } else if ( | ||||||
|  | @ -324,8 +382,8 @@ export const SendStoryModal = ({ | ||||||
|     page === Page.AddViewer || |     page === Page.AddViewer || | ||||||
|     page === Page.HideStoryFrom |     page === Page.HideStoryFrom | ||||||
|   ) { |   ) { | ||||||
|     content = ( |     modal = handleClose => ( | ||||||
|       <EditDistributionList |       <EditDistributionListModal | ||||||
|         candidateConversations={candidateConversations} |         candidateConversations={candidateConversations} | ||||||
|         getPreferredBadge={getPreferredBadge} |         getPreferredBadge={getPreferredBadge} | ||||||
|         i18n={i18n} |         i18n={i18n} | ||||||
|  | @ -350,13 +408,60 @@ export const SendStoryModal = ({ | ||||||
|           } |           } | ||||||
|         }} |         }} | ||||||
|         page={page} |         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} |         selectedContacts={selectedContacts} | ||||||
|         setSelectedContacts={setSelectedContacts} |         setSelectedContacts={setSelectedContacts} | ||||||
|       /> |       /> | ||||||
|     ); |     ); | ||||||
|   } else if (page === Page.ChooseGroups) { |   } 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 |         <SearchInput | ||||||
|           disabled={groupConversations.length === 0} |           disabled={groupConversations.length === 0} | ||||||
|           i18n={i18n} |           i18n={i18n} | ||||||
|  | @ -429,11 +534,32 @@ export const SendStoryModal = ({ | ||||||
|             {i18n('noContactsFound')} |             {i18n('noContactsFound')} | ||||||
|           </div> |           </div> | ||||||
|         )} |         )} | ||||||
|       </> |       </ModalPage> | ||||||
|     ); |     ); | ||||||
|   } else { |   } 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"> |         <div className="SendStoryModal__top-bar"> | ||||||
|           {i18n('stories')} |           {i18n('stories')} | ||||||
|           <ContextMenu |           <ContextMenu | ||||||
|  | @ -649,159 +775,19 @@ export const SendStoryModal = ({ | ||||||
|             )} |             )} | ||||||
|           </Checkbox> |           </Checkbox> | ||||||
|         ))} |         ))} | ||||||
|       </> |       </ModalPage> | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   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> |  | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       <Modal |       <PagedModal | ||||||
|         modalName="SendStoryModal" |         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} |         theme={Theme.Dark} | ||||||
|  |         onClose={onClose} | ||||||
|       > |       > | ||||||
|         {content} |         {modal} | ||||||
|       </Modal> |       </PagedModal> | ||||||
|       {confirmRemoveGroupId && ( |       {confirmRemoveGroupId && ( | ||||||
|         <ConfirmationDialog |         <ConfirmationDialog | ||||||
|           dialogName="SendStoryModal.confirmRemoveGroupId" |           dialogName="SendStoryModal.confirmRemoveGroupId" | ||||||
|  |  | ||||||
|  | @ -12,6 +12,7 @@ import type { PreferredBadgeSelectorType } from '../state/selectors/badges'; | ||||||
| import type { Row } from './ConversationList'; | import type { Row } from './ConversationList'; | ||||||
| import type { StoryDistributionListWithMembersDataType } from '../types/Stories'; | import type { StoryDistributionListWithMembersDataType } from '../types/Stories'; | ||||||
| import type { UUIDStringType } from '../types/UUID'; | import type { UUIDStringType } from '../types/UUID'; | ||||||
|  | import type { RenderModalPage, ModalPropsType } from './Modal'; | ||||||
| import { Avatar, AvatarSize } from './Avatar'; | import { Avatar, AvatarSize } from './Avatar'; | ||||||
| import { Button, ButtonVariant } from './Button'; | import { Button, ButtonVariant } from './Button'; | ||||||
| import { Checkbox } from './Checkbox'; | import { Checkbox } from './Checkbox'; | ||||||
|  | @ -22,7 +23,7 @@ import { ConversationList, RowType } from './ConversationList'; | ||||||
| import { Input } from './Input'; | import { Input } from './Input'; | ||||||
| import { Intl } from './Intl'; | import { Intl } from './Intl'; | ||||||
| import { MY_STORIES_ID, getStoryDistributionListName } from '../types/Stories'; | import { MY_STORIES_ID, getStoryDistributionListName } from '../types/Stories'; | ||||||
| import { Modal } from './Modal'; | import { PagedModal, ModalPage } from './Modal'; | ||||||
| import { SearchInput } from './SearchInput'; | import { SearchInput } from './SearchInput'; | ||||||
| import { StoryDistributionListName } from './StoryDistributionListName'; | import { StoryDistributionListName } from './StoryDistributionListName'; | ||||||
| import { Theme } from '../util/theme'; | import { Theme } from '../util/theme'; | ||||||
|  | @ -80,6 +81,12 @@ function filterConversations( | ||||||
|   ).filter(conversation => conversation.uuid); |   ).filter(conversation => conversation.uuid); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | const modalCommonProps: Pick<ModalPropsType, 'hasXButton' | 'moduleClassName'> = | ||||||
|  |   { | ||||||
|  |     hasXButton: true, | ||||||
|  |     moduleClassName: 'StoriesSettingsModal__modal', | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
| export const StoriesSettingsModal = ({ | export const StoriesSettingsModal = ({ | ||||||
|   candidateConversations, |   candidateConversations, | ||||||
|   distributionLists, |   distributionLists, | ||||||
|  | @ -120,18 +127,34 @@ export const StoriesSettingsModal = ({ | ||||||
|     string | undefined |     string | undefined | ||||||
|   >(); |   >(); | ||||||
| 
 | 
 | ||||||
|   let content: JSX.Element | null; |   let modal: RenderModalPage | null; | ||||||
| 
 | 
 | ||||||
|   if (page !== Page.DistributionLists) { |   if (page !== Page.DistributionLists) { | ||||||
|     content = ( |     const isChoosingViewers = | ||||||
|       <EditDistributionList |       page === Page.ChooseViewers || page === Page.AddViewer; | ||||||
|  | 
 | ||||||
|  |     modal = onClose => ( | ||||||
|  |       <EditDistributionListModal | ||||||
|         candidateConversations={candidateConversations} |         candidateConversations={candidateConversations} | ||||||
|         getPreferredBadge={getPreferredBadge} |         getPreferredBadge={getPreferredBadge} | ||||||
|         i18n={i18n} |         i18n={i18n} | ||||||
|  |         page={page} | ||||||
|  |         onClose={onClose} | ||||||
|         onCreateList={(name, uuids) => { |         onCreateList={(name, uuids) => { | ||||||
|           onDistributionListCreated(name, uuids); |           onDistributionListCreated(name, uuids); | ||||||
|           resetChooseViewersScreen(); |           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 => { |         onViewersUpdated={uuids => { | ||||||
|           if (listToEditId && page === Page.AddViewer) { |           if (listToEditId && page === Page.AddViewer) { | ||||||
|             onViewersUpdated(listToEditId, uuids); |             onViewersUpdated(listToEditId, uuids); | ||||||
|  | @ -147,14 +170,14 @@ export const StoriesSettingsModal = ({ | ||||||
|             resetChooseViewersScreen(); |             resetChooseViewersScreen(); | ||||||
|           } |           } | ||||||
|         }} |         }} | ||||||
|         page={page} |  | ||||||
|         selectedContacts={selectedContacts} |         selectedContacts={selectedContacts} | ||||||
|         setSelectedContacts={setSelectedContacts} |         setSelectedContacts={setSelectedContacts} | ||||||
|       /> |       /> | ||||||
|     ); |     ); | ||||||
|   } else if (listToEdit) { |   } else if (listToEdit) { | ||||||
|     content = ( |     modal = onClose => ( | ||||||
|       <DistributionListSettings |       <DistributionListSettingsModal | ||||||
|  |         key="settings-modal" | ||||||
|         getPreferredBadge={getPreferredBadge} |         getPreferredBadge={getPreferredBadge} | ||||||
|         i18n={i18n} |         i18n={i18n} | ||||||
|         listToEdit={listToEdit} |         listToEdit={listToEdit} | ||||||
|  | @ -165,6 +188,8 @@ export const StoriesSettingsModal = ({ | ||||||
|         setPage={setPage} |         setPage={setPage} | ||||||
|         setSelectedContacts={setSelectedContacts} |         setSelectedContacts={setSelectedContacts} | ||||||
|         toggleSignalConnectionsModal={toggleSignalConnectionsModal} |         toggleSignalConnectionsModal={toggleSignalConnectionsModal} | ||||||
|  |         onBackButtonClick={() => setListToEditId(undefined)} | ||||||
|  |         onClose={onClose} | ||||||
|       /> |       /> | ||||||
|     ); |     ); | ||||||
|   } else { |   } else { | ||||||
|  | @ -172,8 +197,14 @@ export const StoriesSettingsModal = ({ | ||||||
|       list => list.id !== MY_STORIES_ID |       list => list.id !== MY_STORIES_ID | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     content = ( |     modal = onClose => ( | ||||||
|       <> |       <ModalPage | ||||||
|  |         modalName="StoriesSettingsModal__list" | ||||||
|  |         i18n={i18n} | ||||||
|  |         onClose={onClose} | ||||||
|  |         title={i18n('StoriesSettings__title')} | ||||||
|  |         {...modalCommonProps} | ||||||
|  |       > | ||||||
|         <button |         <button | ||||||
|           className="StoriesSettingsModal__list" |           className="StoriesSettingsModal__list" | ||||||
|           onClick={() => { |           onClick={() => { | ||||||
|  | @ -244,61 +275,19 @@ export const StoriesSettingsModal = ({ | ||||||
|             </span> |             </span> | ||||||
|           </button> |           </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 ( |   return ( | ||||||
|     <> |     <> | ||||||
|       <Modal |       <PagedModal | ||||||
|         modalName="StoriesSettingsModal" |         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} |         theme={Theme.Dark} | ||||||
|         title={modalTitle} |         onClose={hideStoriesSettings} | ||||||
|       > |       > | ||||||
|         {content} |         {modal} | ||||||
|       </Modal> |       </PagedModal> | ||||||
|       {confirmDeleteListId && ( |       {confirmDeleteListId && ( | ||||||
|         <ConfirmationDialog |         <ConfirmationDialog | ||||||
|           dialogName="StoriesSettings.deleteList" |           dialogName="StoriesSettings.deleteList" | ||||||
|  | @ -324,12 +313,14 @@ export const StoriesSettingsModal = ({ | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| type DistributionListSettingsPropsType = { | type DistributionListSettingsModalPropsType = { | ||||||
|   i18n: LocalizerType; |   i18n: LocalizerType; | ||||||
|   listToEdit: StoryDistributionListWithMembersDataType; |   listToEdit: StoryDistributionListWithMembersDataType; | ||||||
|   setConfirmDeleteListId: (id: string) => unknown; |   setConfirmDeleteListId: (id: string) => unknown; | ||||||
|   setPage: (page: Page) => unknown; |   setPage: (page: Page) => unknown; | ||||||
|   setSelectedContacts: (contacts: Array<ConversationType>) => unknown; |   setSelectedContacts: (contacts: Array<ConversationType>) => unknown; | ||||||
|  |   onBackButtonClick: (() => void) | undefined; | ||||||
|  |   onClose: () => void; | ||||||
| } & Pick< | } & Pick< | ||||||
|   PropsType, |   PropsType, | ||||||
|   | 'getPreferredBadge' |   | 'getPreferredBadge' | ||||||
|  | @ -339,18 +330,20 @@ type DistributionListSettingsPropsType = { | ||||||
|   | 'toggleSignalConnectionsModal' |   | 'toggleSignalConnectionsModal' | ||||||
| >; | >; | ||||||
| 
 | 
 | ||||||
| export const DistributionListSettings = ({ | export const DistributionListSettingsModal = ({ | ||||||
|   getPreferredBadge, |   getPreferredBadge, | ||||||
|   i18n, |   i18n, | ||||||
|   listToEdit, |   listToEdit, | ||||||
|   onRemoveMember, |   onRemoveMember, | ||||||
|   onRepliesNReactionsChanged, |   onRepliesNReactionsChanged, | ||||||
|  |   onBackButtonClick, | ||||||
|  |   onClose, | ||||||
|   setConfirmDeleteListId, |   setConfirmDeleteListId, | ||||||
|   setMyStoriesToAllSignalConnections, |   setMyStoriesToAllSignalConnections, | ||||||
|   setPage, |   setPage, | ||||||
|   setSelectedContacts, |   setSelectedContacts, | ||||||
|   toggleSignalConnectionsModal, |   toggleSignalConnectionsModal, | ||||||
| }: DistributionListSettingsPropsType): JSX.Element => { | }: DistributionListSettingsModalPropsType): JSX.Element => { | ||||||
|   const [confirmRemoveMember, setConfirmRemoveMember] = useState< |   const [confirmRemoveMember, setConfirmRemoveMember] = useState< | ||||||
|     | undefined |     | undefined | ||||||
|     | { |     | { | ||||||
|  | @ -362,8 +355,21 @@ export const DistributionListSettings = ({ | ||||||
| 
 | 
 | ||||||
|   const isMyStories = listToEdit.id === MY_STORIES_ID; |   const isMyStories = listToEdit.id === MY_STORIES_ID; | ||||||
| 
 | 
 | ||||||
|  |   const modalTitle = getStoryDistributionListName( | ||||||
|  |     i18n, | ||||||
|  |     listToEdit.id, | ||||||
|  |     listToEdit.name | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|   return ( |   return ( | ||||||
|     <> |     <ModalPage | ||||||
|  |       modalName="DistributionListSettingsModal" | ||||||
|  |       i18n={i18n} | ||||||
|  |       onBackButtonClick={onBackButtonClick} | ||||||
|  |       onClose={onClose} | ||||||
|  |       title={modalTitle} | ||||||
|  |       {...modalCommonProps} | ||||||
|  |     > | ||||||
|       {!isMyStories && ( |       {!isMyStories && ( | ||||||
|         <> |         <> | ||||||
|           <div className="StoriesSettingsModal__list StoriesSettingsModal__list--no-pointer"> |           <div className="StoriesSettingsModal__list StoriesSettingsModal__list--no-pointer"> | ||||||
|  | @ -521,7 +527,7 @@ export const DistributionListSettings = ({ | ||||||
|           {i18n('StoriesSettings__remove--body')} |           {i18n('StoriesSettings__remove--body')} | ||||||
|         </ConfirmationDialog> |         </ConfirmationDialog> | ||||||
|       )} |       )} | ||||||
|     </> |     </ModalPage> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | @ -630,24 +636,37 @@ export const EditMyStoriesPrivacy = ({ | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| type EditDistributionListPropsType = { | type EditDistributionListModalPropsType = { | ||||||
|   onCreateList: (name: string, viewerUuids: Array<UUIDStringType>) => unknown; |   onCreateList: (name: string, viewerUuids: Array<UUIDStringType>) => unknown; | ||||||
|   onViewersUpdated: (viewerUuids: Array<UUIDStringType>) => unknown; |   onViewersUpdated: (viewerUuids: Array<UUIDStringType>) => unknown; | ||||||
|   page: Page; |   page: | ||||||
|  |     | Page.AddViewer | ||||||
|  |     | Page.ChooseViewers | ||||||
|  |     | Page.HideStoryFrom | ||||||
|  |     | Page.NameStory; | ||||||
|   selectedContacts: Array<ConversationType>; |   selectedContacts: Array<ConversationType>; | ||||||
|  |   onClose: () => unknown; | ||||||
|   setSelectedContacts: (contacts: Array<ConversationType>) => unknown; |   setSelectedContacts: (contacts: Array<ConversationType>) => unknown; | ||||||
|  |   onBackButtonClick: () => void; | ||||||
| } & Pick<PropsType, 'candidateConversations' | 'getPreferredBadge' | 'i18n'>; | } & Pick<PropsType, 'candidateConversations' | 'getPreferredBadge' | 'i18n'>; | ||||||
| 
 | 
 | ||||||
| export const EditDistributionList = ({ | /** | ||||||
|  |  * | ||||||
|  |  * @param param0 | ||||||
|  |  * @returns | ||||||
|  |  */ | ||||||
|  | export const EditDistributionListModal = ({ | ||||||
|   candidateConversations, |   candidateConversations, | ||||||
|   getPreferredBadge, |   getPreferredBadge, | ||||||
|   i18n, |   i18n, | ||||||
|   onCreateList, |   onCreateList, | ||||||
|   onViewersUpdated, |   onViewersUpdated, | ||||||
|   page, |   page, | ||||||
|  |   onClose, | ||||||
|   selectedContacts, |   selectedContacts, | ||||||
|   setSelectedContacts, |   setSelectedContacts, | ||||||
| }: EditDistributionListPropsType): JSX.Element | null => { |   onBackButtonClick, | ||||||
|  | }: EditDistributionListModalPropsType): JSX.Element => { | ||||||
|   const [storyName, setStoryName] = useState(''); |   const [storyName, setStoryName] = useState(''); | ||||||
|   const [searchTerm, setSearchTerm] = useState(''); |   const [searchTerm, setSearchTerm] = useState(''); | ||||||
| 
 | 
 | ||||||
|  | @ -668,18 +687,6 @@ export const EditDistributionList = ({ | ||||||
|     }; |     }; | ||||||
|   }, [candidateConversations, normalizedSearchTerm, setFilteredConversations]); |   }, [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 contactLookup = useMemo(() => { | ||||||
|     const map = new Map(); |     const map = new Map(); | ||||||
|     candidateConversations.forEach(contact => { |     candidateConversations.forEach(contact => { | ||||||
|  | @ -720,8 +727,29 @@ export const EditDistributionList = ({ | ||||||
|     page === Page.ChooseViewers || page === Page.AddViewer; |     page === Page.ChooseViewers || page === Page.AddViewer; | ||||||
| 
 | 
 | ||||||
|   if (page === Page.NameStory) { |   if (page === Page.NameStory) { | ||||||
|  |     const footer = ( | ||||||
|  |       <Button | ||||||
|  |         disabled={!storyName} | ||||||
|  |         onClick={() => { | ||||||
|  |           onCreateList(storyName, Array.from(selectedConversationUuids)); | ||||||
|  |           setStoryName(''); | ||||||
|  |         }} | ||||||
|  |         variant={ButtonVariant.Primary} | ||||||
|  |       > | ||||||
|  |         {i18n('done')} | ||||||
|  |       </Button> | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|     return ( |     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__name-story-avatar-container"> | ||||||
|           <div className="StoriesSettingsModal__list__avatar--private StoriesSettingsModal__list__avatar--private--large" /> |           <div className="StoriesSettingsModal__list__avatar--private StoriesSettingsModal__list__avatar--private--large" /> | ||||||
|         </div> |         </div> | ||||||
|  | @ -762,143 +790,137 @@ export const EditDistributionList = ({ | ||||||
|             </span> |             </span> | ||||||
|           </div> |           </div> | ||||||
|         ))} |         ))} | ||||||
|         <Modal.ButtonFooter> |       </ModalPage> | ||||||
|           <Button |  | ||||||
|             disabled={!storyName} |  | ||||||
|             onClick={() => { |  | ||||||
|               onCreateList(storyName, Array.from(selectedConversationUuids)); |  | ||||||
|               setStoryName(''); |  | ||||||
|             }} |  | ||||||
|             variant={ButtonVariant.Primary} |  | ||||||
|           > |  | ||||||
|             {i18n('done')} |  | ||||||
|           </Button> |  | ||||||
|         </Modal.ButtonFooter> |  | ||||||
|       </> |  | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   if ( |   const rowCount = filteredConversations.length; | ||||||
|     page === Page.AddViewer || |   const getRow = (index: number): undefined | Row => { | ||||||
|     page === Page.ChooseViewers || |     const contact = filteredConversations[index]; | ||||||
|     page === Page.HideStoryFrom |     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 { |     return { | ||||||
|         type: RowType.ContactCheckbox, |       type: RowType.ContactCheckbox, | ||||||
|         contact, |       contact, | ||||||
|         isChecked: isSelected, |       isChecked: isSelected, | ||||||
|       }; |  | ||||||
|     }; |     }; | ||||||
|  |   }; | ||||||
| 
 | 
 | ||||||
|     return ( |   let footer: JSX.Element | undefined; | ||||||
|       <> |   if (isChoosingViewers) { | ||||||
|         <SearchInput |     footer = ( | ||||||
|           disabled={candidateConversations.length === 0} |       <Button | ||||||
|           i18n={i18n} |         disabled={selectedContacts.length === 0} | ||||||
|           placeholder={i18n('contactSearchPlaceholder')} |         onClick={() => { | ||||||
|           moduleClassName="StoriesSettingsModal__search" |           onViewersUpdated(Array.from(selectedConversationUuids)); | ||||||
|           onChange={event => { |         }} | ||||||
|             setSearchTerm(event.target.value); |         variant={ButtonVariant.Primary} | ||||||
|           }} |       > | ||||||
|           value={searchTerm} |         {page === Page.AddViewer ? i18n('done') : i18n('next2')} | ||||||
|         /> |       </Button> | ||||||
|         {selectedContacts.length ? ( |     ); | ||||||
|           <ContactPills moduleClassName="StoriesSettingsModal__tags"> |   } else if (page === Page.HideStoryFrom) { | ||||||
|             {selectedContacts.map(contact => ( |     footer = ( | ||||||
|               <ContactPill |       <Button | ||||||
|                 key={contact.id} |         disabled={selectedContacts.length === 0} | ||||||
|                 acceptedMessageRequest={contact.acceptedMessageRequest} |         onClick={() => { | ||||||
|                 avatarPath={contact.avatarPath} |           onViewersUpdated(Array.from(selectedConversationUuids)); | ||||||
|                 color={contact.color} |         }} | ||||||
|                 firstName={contact.systemGivenName ?? contact.firstName} |         variant={ButtonVariant.Primary} | ||||||
|                 i18n={i18n} |       > | ||||||
|                 id={contact.id} |         {i18n('update')} | ||||||
|                 isMe={contact.isMe} |       </Button> | ||||||
|                 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> |  | ||||||
|         )} |  | ||||||
|       </> |  | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   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 |           hasXButton | ||||||
|           i18n={i18n} |           i18n={i18n} | ||||||
|           onClose={hideContactModal} |           onClose={hideContactModal} | ||||||
|  |           padded={false} | ||||||
|         > |         > | ||||||
|           <div className="ContactModal"> |           <div className="ContactModal"> | ||||||
|             <Avatar |             <Avatar | ||||||
|  |  | ||||||
|  | @ -31,12 +31,34 @@ export function DeliveryIssueDialog(props: PropsType): React.ReactElement { | ||||||
|   // Focus first button after initial render, restore focus on teardown
 |   // Focus first button after initial render, restore focus on teardown
 | ||||||
|   const [focusRef] = useRestoreFocus(); |   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 ( |   return ( | ||||||
|     <Modal |     <Modal | ||||||
|       modalName="DeliveryIssueDialog" |       modalName="DeliveryIssueDialog" | ||||||
|       hasXButton={false} |       hasXButton={false} | ||||||
|       onClose={onClose} |       onClose={onClose} | ||||||
|       i18n={i18n} |       i18n={i18n} | ||||||
|  |       modalFooter={footer} | ||||||
|     > |     > | ||||||
|       <section> |       <section> | ||||||
|         <div className="module-delivery-issue-dialog__image"> |         <div className="module-delivery-issue-dialog__image"> | ||||||
|  | @ -60,24 +82,6 @@ export function DeliveryIssueDialog(props: PropsType): React.ReactElement { | ||||||
|           /> |           /> | ||||||
|         </div> |         </div> | ||||||
|       </section> |       </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> |     </Modal> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -47,11 +47,20 @@ export const ConversationNotificationsModal = ({ | ||||||
|   return ( |   return ( | ||||||
|     <Modal |     <Modal | ||||||
|       modalName="ConversationNotificationsModal" |       modalName="ConversationNotificationsModal" | ||||||
|       hasStickyButtons |  | ||||||
|       hasXButton |       hasXButton | ||||||
|       onClose={onClose} |       onClose={onClose} | ||||||
|       i18n={i18n} |       i18n={i18n} | ||||||
|       title={i18n('muteNotificationsTitle')} |       title={i18n('muteNotificationsTitle')} | ||||||
|  |       modalFooter={ | ||||||
|  |         <> | ||||||
|  |           <Button onClick={onClose} variant={ButtonVariant.Secondary}> | ||||||
|  |             {i18n('cancel')} | ||||||
|  |           </Button> | ||||||
|  |           <Button onClick={onMuteChange} variant={ButtonVariant.Primary}> | ||||||
|  |             {i18n('mute')} | ||||||
|  |           </Button> | ||||||
|  |         </> | ||||||
|  |       } | ||||||
|     > |     > | ||||||
|       {muteOptions |       {muteOptions | ||||||
|         .filter(x => x.value > 0) |         .filter(x => x.value > 0) | ||||||
|  | @ -67,14 +76,6 @@ export const ConversationNotificationsModal = ({ | ||||||
|             onChange={value => value && setMuteExpirationValue(option.value)} |             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> |     </Modal> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -155,6 +155,7 @@ export const EditConversationAttributesModal: FunctionComponent<PropsType> = ({ | ||||||
|   } else { |   } else { | ||||||
|     content = ( |     content = ( | ||||||
|       <form |       <form | ||||||
|  |         id="edit-conversation-form" | ||||||
|         onSubmit={onSubmit} |         onSubmit={onSubmit} | ||||||
|         className="module-EditConversationAttributesModal" |         className="module-EditConversationAttributesModal" | ||||||
|       > |       > | ||||||
|  | @ -199,40 +200,43 @@ export const EditConversationAttributesModal: FunctionComponent<PropsType> = ({ | ||||||
|             {i18n('updateGroupAttributes__error-message')} |             {i18n('updateGroupAttributes__error-message')} | ||||||
|           </div> |           </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> |       </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 ( |   return ( | ||||||
|     <Modal |     <Modal | ||||||
|       modalName="EditConversationAttributesModal" |       modalName="EditConversationAttributesModal" | ||||||
|       hasStickyButtons |  | ||||||
|       hasXButton |       hasXButton | ||||||
|       i18n={i18n} |       i18n={i18n} | ||||||
|       onClose={onClose} |       onClose={onClose} | ||||||
|       title={i18n('updateGroupAttributes__title')} |       title={i18n('updateGroupAttributes__title')} | ||||||
|  |       modalFooter={modalFooter} | ||||||
|     > |     > | ||||||
|       {content} |       {content} | ||||||
|     </Modal> |     </Modal> | ||||||
|  |  | ||||||
|  | @ -152,7 +152,6 @@ export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGr | ||||||
|         {this.isEditingAvatar && ( |         {this.isEditingAvatar && ( | ||||||
|           <Modal |           <Modal | ||||||
|             modalName="LeftPaneSetGroupMetadataHelper.AvatarEditor" |             modalName="LeftPaneSetGroupMetadataHelper.AvatarEditor" | ||||||
|             hasStickyButtons |  | ||||||
|             hasXButton |             hasXButton | ||||||
|             i18n={i18n} |             i18n={i18n} | ||||||
|             onClose={toggleComposeEditingAvatar} |             onClose={toggleComposeEditingAvatar} | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Alvaro
				Alvaro