Lets users send stories to groups
This commit is contained in:
		
					parent
					
						
							
								d4b74db05c
							
						
					
				
			
			
				commit
				
					
						ccc89545c5
					
				
			
		
					 21 changed files with 1177 additions and 400 deletions
				
			
		|  | @ -7381,6 +7381,30 @@ | ||||||
|     "message": "Send story", |     "message": "Send story", | ||||||
|     "description": "aria-label for the send story button" |     "description": "aria-label for the send story button" | ||||||
|   }, |   }, | ||||||
|  |   "SendStoryModal__new": { | ||||||
|  |     "message": "New story", | ||||||
|  |     "description": "button to create a new distribution list to send story to" | ||||||
|  |   }, | ||||||
|  |   "SendStoryModal__new-private--title": { | ||||||
|  |     "message": "New private story", | ||||||
|  |     "description": "Create a new distribution list" | ||||||
|  |   }, | ||||||
|  |   "SendStoryModal__new-private--description": { | ||||||
|  |     "message": "Visible only to specific people", | ||||||
|  |     "description": "Description of what a distribution list would do" | ||||||
|  |   }, | ||||||
|  |   "SendStoryModal__new-group--title": { | ||||||
|  |     "message": "New group story", | ||||||
|  |     "description": "Select a group to send a story to" | ||||||
|  |   }, | ||||||
|  |   "SendStoryModal__new-group--description": { | ||||||
|  |     "message": "Share to an existing group", | ||||||
|  |     "description": "Description of what selecting a group would do" | ||||||
|  |   }, | ||||||
|  |   "SendStoryModal__choose-groups": { | ||||||
|  |     "message": "Choose groups", | ||||||
|  |     "description": "Modal title when choosing groups" | ||||||
|  |   }, | ||||||
|   "Stories__settings-toggle--title": { |   "Stories__settings-toggle--title": { | ||||||
|     "message": "Share & View Stories", |     "message": "Share & View Stories", | ||||||
|     "description": "Select box title for the stories on/off toggle" |     "description": "Select box title for the stories on/off toggle" | ||||||
|  |  | ||||||
|  | @ -11,7 +11,7 @@ | ||||||
|   &__popper { |   &__popper { | ||||||
|     @extend %module-composition-popper; |     @extend %module-composition-popper; | ||||||
|     margin: 0; |     margin: 0; | ||||||
|     padding: 6px 0; |     padding: 6px 2px; | ||||||
|     width: auto; |     width: auto; | ||||||
| 
 | 
 | ||||||
|     &--single-item { |     &--single-item { | ||||||
|  | @ -40,8 +40,8 @@ | ||||||
|     display: flex; |     display: flex; | ||||||
|     justify-content: space-between; |     justify-content: space-between; | ||||||
|     padding: 6px; |     padding: 6px; | ||||||
|     margin: 0 2px; |  | ||||||
|     min-width: 150px; |     min-width: 150px; | ||||||
|  |     width: 100%; | ||||||
| 
 | 
 | ||||||
|     &--container { |     &--container { | ||||||
|       display: flex; |       display: flex; | ||||||
|  |  | ||||||
|  | @ -132,6 +132,7 @@ | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   &__body { |   &__body { | ||||||
|  |     @include scrollbar; | ||||||
|     @include font-body-1; |     @include font-body-1; | ||||||
|     margin: 0; |     margin: 0; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | @ -2,6 +2,52 @@ | ||||||
| // SPDX-License-Identifier: AGPL-3.0-only | // SPDX-License-Identifier: AGPL-3.0-only | ||||||
| 
 | 
 | ||||||
| .SendStoryModal { | .SendStoryModal { | ||||||
|  |   &__top-bar { | ||||||
|  |     align-items: center; | ||||||
|  |     display: flex; | ||||||
|  |     min-height: 40px; | ||||||
|  |     justify-content: space-between; | ||||||
|  |     user-select: none; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   &__new-story { | ||||||
|  |     &__container { | ||||||
|  |       &::before { | ||||||
|  |         @include color-svg('../images/icons/v2/plus-20.svg', $color-white); | ||||||
|  |         content: ''; | ||||||
|  |         height: 16px; | ||||||
|  |         margin-right: 8px; | ||||||
|  |         width: 16px; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     &__option--description { | ||||||
|  |       color: $color-gray-25; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   &__icon { | ||||||
|  |     &--lock { | ||||||
|  |       @include color-svg( | ||||||
|  |         '../images/icons/v2/lock-outline-24.svg', | ||||||
|  |         $color-white | ||||||
|  |       ); | ||||||
|  |       height: 14px; | ||||||
|  |       margin-top: 4px; | ||||||
|  |       width: 11px; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     &--group { | ||||||
|  |       @include color-svg( | ||||||
|  |         '../images/icons/v2/group-outline-24.svg', | ||||||
|  |         $color-white | ||||||
|  |       ); | ||||||
|  |       height: 14px; | ||||||
|  |       margin-top: 2px; | ||||||
|  |       width: 14px; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   &__distribution-list { |   &__distribution-list { | ||||||
|     &__container { |     &__container { | ||||||
|       justify-content: space-between; |       justify-content: space-between; | ||||||
|  | @ -29,29 +75,98 @@ | ||||||
|       @include font-body-2; |       @include font-body-2; | ||||||
|       color: $color-gray-60; |       color: $color-gray-60; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     &__checkbox { | ||||||
|  |       margin-right: 0; | ||||||
|  |       position: relative; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|   &__button-footer { |     &__checkbox input[type='checkbox'] { | ||||||
|     align-items: center; |       cursor: pointer; | ||||||
|     justify-content: space-between; |       height: 0; | ||||||
|  |       position: absolute; | ||||||
|  |       width: 0; | ||||||
|  | 
 | ||||||
|  |       @include keyboard-mode { | ||||||
|  |         &:focus { | ||||||
|  |           &::before { | ||||||
|  |             border-color: $color-ultramarine; | ||||||
|  |           } | ||||||
|  |           outline: none; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       &::before { | ||||||
|  |         @include rounded-corners; | ||||||
|  |         background: inherit; | ||||||
|  |         border: 1.5px solid $color-gray-60; | ||||||
|  |         content: ''; | ||||||
|  |         display: block; | ||||||
|  |         height: 20px; | ||||||
|  |         position: absolute; | ||||||
|  |         width: 20px; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       &:checked { | ||||||
|  |         &::before { | ||||||
|  |           background: $color-ultramarine; | ||||||
|  |           border: 1.5px solid $color-ultramarine; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         &::after { | ||||||
|  |           border: solid $color-white; | ||||||
|  |           border-width: 0 2px 2px 0; | ||||||
|  |           content: ''; | ||||||
|  |           display: block; | ||||||
|  |           height: 11px; | ||||||
|  |           left: 7px; | ||||||
|  |           position: absolute; | ||||||
|  |           top: 3px; | ||||||
|  |           transform: rotate(45deg); | ||||||
|  |           width: 6px; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   &__selected-lists { |   &__selected-lists { | ||||||
|     @include font-body-2; |     @include font-body-2; | ||||||
|     color: $color-gray-60; |     color: $color-gray-15; | ||||||
|     max-width: 280px; |     max-width: 280px; | ||||||
|     user-select: none; |     user-select: none; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   &__ok { | ||||||
|  |     @include button-reset; | ||||||
|  |     @include rounded-corners; | ||||||
|  |     align-items: center; | ||||||
|  |     background: $color-ultramarine; | ||||||
|  |     display: flex; | ||||||
|  |     height: 32px; | ||||||
|  |     justify-content: center; | ||||||
|  |     width: 32px; | ||||||
|  | 
 | ||||||
|  |     &::disabled { | ||||||
|  |       background: $color-gray-60; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     &::after { | ||||||
|  |       @include color-svg('../images/icons/v2/check-24.svg', $color-white); | ||||||
|  |       content: ''; | ||||||
|  |       height: 18px; | ||||||
|  |       width: 18px; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   &__send { |   &__send { | ||||||
|     @include button-reset; |     @include button-reset; | ||||||
|     @include rounded-corners; |     @include rounded-corners; | ||||||
|     align-items: center; |     align-items: center; | ||||||
|     background: $color-ultramarine; |     background: $color-ultramarine; | ||||||
|     display: flex; |     display: flex; | ||||||
|     height: 40px; |     height: 32px; | ||||||
|     justify-content: center; |     justify-content: center; | ||||||
|     width: 40px; |     width: 32px; | ||||||
| 
 | 
 | ||||||
|     &::disabled { |     &::disabled { | ||||||
|       background: $color-gray-60; |       background: $color-gray-60; | ||||||
|  | @ -60,8 +175,16 @@ | ||||||
|     &::after { |     &::after { | ||||||
|       @include color-svg('../images/icons/v2/send-24.svg', $color-white); |       @include color-svg('../images/icons/v2/send-24.svg', $color-white); | ||||||
|       content: ''; |       content: ''; | ||||||
|       height: 24px; |       height: 18px; | ||||||
|       width: 24px; |       width: 18px; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | .module-Modal--sticky-buttons .SendStoryModal__button-footer { | ||||||
|  |   align-items: center; | ||||||
|  |   justify-content: space-between; | ||||||
|  |   padding-top: 0; | ||||||
|  |   padding-left: 16px; | ||||||
|  |   padding-right: 16px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -2,13 +2,71 @@ | ||||||
| // SPDX-License-Identifier: AGPL-3.0-only | // SPDX-License-Identifier: AGPL-3.0-only | ||||||
| 
 | 
 | ||||||
| .StoriesSettingsModal { | .StoriesSettingsModal { | ||||||
|   &__modal { |   &__conversation-list { | ||||||
|     .module-conversation-list { |     .module-conversation-list, | ||||||
|       padding: 0; |     .module-conversation-list__item--contact-or-conversation { | ||||||
|  |       padding-left: 0; | ||||||
|  |       padding-right: 0; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .module-conversation-list__item--contact-or-conversation { |     .module-conversation-list__item--contact-or-conversation__checkbox--container { | ||||||
|       padding: 0; |       height: 20px; | ||||||
|  |       margin-right: 8px; | ||||||
|  |       position: relative; | ||||||
|  |       width: 20px; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     input[type='checkbox'] { | ||||||
|  |       background: transparent; | ||||||
|  |       border: none; | ||||||
|  |       cursor: pointer; | ||||||
|  |       display: block; | ||||||
|  |       height: 0; | ||||||
|  |       margin: 0; | ||||||
|  |       min-width: 0; | ||||||
|  |       position: absolute; | ||||||
|  |       width: 0; | ||||||
|  | 
 | ||||||
|  |       @include keyboard-mode { | ||||||
|  |         &:focus { | ||||||
|  |           &::before { | ||||||
|  |             border-color: $color-ultramarine; | ||||||
|  |           } | ||||||
|  |           outline: none; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       &::before { | ||||||
|  |         @include rounded-corners; | ||||||
|  |         background: inherit; | ||||||
|  |         border: 1.5px solid $color-gray-60; | ||||||
|  |         content: ''; | ||||||
|  |         display: block; | ||||||
|  |         height: 20px; | ||||||
|  |         position: absolute; | ||||||
|  |         width: 20px; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       &:checked { | ||||||
|  |         &::before { | ||||||
|  |           -webkit-mask: none; | ||||||
|  |           background: $color-ultramarine; | ||||||
|  |           border: 1.5px solid $color-ultramarine; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         &::after { | ||||||
|  |           border: solid $color-white; | ||||||
|  |           border-width: 0 2px 2px 0; | ||||||
|  |           content: ''; | ||||||
|  |           display: block; | ||||||
|  |           height: 11px; | ||||||
|  |           left: 7px; | ||||||
|  |           position: absolute; | ||||||
|  |           top: 3px; | ||||||
|  |           transform: rotate(45deg); | ||||||
|  |           width: 6px; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -57,11 +115,21 @@ | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       &--private { |       &--private { | ||||||
|         @include avatar('../images/icons/v2/group-solid-24.svg'); |         @include avatar('../images/icons/v2/lock-outline-24.svg'); | ||||||
|  | 
 | ||||||
|  |         &::after { | ||||||
|  |           height: 16px; | ||||||
|  |           width: 12px; | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         &--large { |         &--large { | ||||||
|           height: 64px; |           height: 64px; | ||||||
|           width: 64px; |           width: 64px; | ||||||
|  | 
 | ||||||
|  |           &::after { | ||||||
|  |             height: 24px; | ||||||
|  |             width: 18px; | ||||||
|  |           } | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -35,6 +35,7 @@ import { | ||||||
| } from '../mediaEditor/util/getTextStyleAttributes'; | } from '../mediaEditor/util/getTextStyleAttributes'; | ||||||
| 
 | 
 | ||||||
| export type PropsType = { | export type PropsType = { | ||||||
|  |   doneButtonLabel?: string; | ||||||
|   i18n: LocalizerType; |   i18n: LocalizerType; | ||||||
|   imageSrc: string; |   imageSrc: string; | ||||||
|   onClose: () => unknown; |   onClose: () => unknown; | ||||||
|  | @ -84,6 +85,7 @@ function isCmdOrCtrl(ev: KeyboardEvent): boolean { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const MediaEditor = ({ | export const MediaEditor = ({ | ||||||
|  |   doneButtonLabel, | ||||||
|   i18n, |   i18n, | ||||||
|   imageSrc, |   imageSrc, | ||||||
|   onClose, |   onClose, | ||||||
|  | @ -1065,7 +1067,7 @@ export const MediaEditor = ({ | ||||||
|             theme={Theme.Dark} |             theme={Theme.Dark} | ||||||
|             variant={ButtonVariant.Primary} |             variant={ButtonVariant.Primary} | ||||||
|           > |           > | ||||||
|             {i18n('save')} |             {doneButtonLabel || i18n('save')} | ||||||
|           </Button> |           </Button> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|  | @ -22,6 +22,7 @@ type PropsType = { | ||||||
|   hasStickyButtons?: boolean; |   hasStickyButtons?: boolean; | ||||||
|   hasXButton?: boolean; |   hasXButton?: boolean; | ||||||
|   i18n: LocalizerType; |   i18n: LocalizerType; | ||||||
|  |   modalFooter?: JSX.Element; | ||||||
|   moduleClassName?: string; |   moduleClassName?: string; | ||||||
|   onBackButtonClick?: () => unknown; |   onBackButtonClick?: () => unknown; | ||||||
|   onClose?: () => void; |   onClose?: () => void; | ||||||
|  | @ -41,12 +42,13 @@ export function Modal({ | ||||||
|   hasStickyButtons, |   hasStickyButtons, | ||||||
|   hasXButton, |   hasXButton, | ||||||
|   i18n, |   i18n, | ||||||
|  |   modalFooter, | ||||||
|   moduleClassName, |   moduleClassName, | ||||||
|   noMouseClose, |   noMouseClose, | ||||||
|   onBackButtonClick, |   onBackButtonClick, | ||||||
|   onClose = noop, |   onClose = noop, | ||||||
|   title, |  | ||||||
|   theme, |   theme, | ||||||
|  |   title, | ||||||
|   useFocusTrap, |   useFocusTrap, | ||||||
| }: Readonly<ModalPropsType>): ReactElement { | }: Readonly<ModalPropsType>): ReactElement { | ||||||
|   const { close, modalStyles, overlayStyles } = useAnimated(onClose, { |   const { close, modalStyles, overlayStyles } = useAnimated(onClose, { | ||||||
|  | @ -71,6 +73,7 @@ export function Modal({ | ||||||
|           hasStickyButtons={hasStickyButtons} |           hasStickyButtons={hasStickyButtons} | ||||||
|           hasXButton={hasXButton} |           hasXButton={hasXButton} | ||||||
|           i18n={i18n} |           i18n={i18n} | ||||||
|  |           modalFooter={modalFooter} | ||||||
|           moduleClassName={moduleClassName} |           moduleClassName={moduleClassName} | ||||||
|           onBackButtonClick={onBackButtonClick} |           onBackButtonClick={onBackButtonClick} | ||||||
|           onClose={close} |           onClose={close} | ||||||
|  | @ -88,6 +91,7 @@ export function ModalWindow({ | ||||||
|   hasStickyButtons, |   hasStickyButtons, | ||||||
|   hasXButton, |   hasXButton, | ||||||
|   i18n, |   i18n, | ||||||
|  |   modalFooter, | ||||||
|   moduleClassName, |   moduleClassName, | ||||||
|   onBackButtonClick, |   onBackButtonClick, | ||||||
|   onClose = noop, |   onClose = noop, | ||||||
|  | @ -192,6 +196,7 @@ export function ModalWindow({ | ||||||
|             </div> |             </div> | ||||||
|           )} |           )} | ||||||
|         </Measure> |         </Measure> | ||||||
|  |         {modalFooter} | ||||||
|       </div> |       </div> | ||||||
|     </> |     </> | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|  | @ -7,7 +7,10 @@ import React from 'react'; | ||||||
| import type { PropsType } from './SendStoryModal'; | import type { PropsType } from './SendStoryModal'; | ||||||
| import enMessages from '../../_locales/en/messages.json'; | import enMessages from '../../_locales/en/messages.json'; | ||||||
| import { SendStoryModal } from './SendStoryModal'; | import { SendStoryModal } from './SendStoryModal'; | ||||||
| import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation'; | import { | ||||||
|  |   getDefaultConversation, | ||||||
|  |   getDefaultGroup, | ||||||
|  | } from '../test-both/helpers/getDefaultConversation'; | ||||||
| import { setupI18n } from '../util/setupI18n'; | import { setupI18n } from '../util/setupI18n'; | ||||||
| import { | import { | ||||||
|   getMyStories, |   getMyStories, | ||||||
|  | @ -20,9 +23,19 @@ export default { | ||||||
|   title: 'Components/SendStoryModal', |   title: 'Components/SendStoryModal', | ||||||
|   component: SendStoryModal, |   component: SendStoryModal, | ||||||
|   argTypes: { |   argTypes: { | ||||||
|  |     candidateConversations: { | ||||||
|  |       defaultValue: Array.from(Array(100), () => getDefaultConversation()), | ||||||
|  |     }, | ||||||
|     distributionLists: { |     distributionLists: { | ||||||
|       defaultValue: [getMyStories()], |       defaultValue: [getMyStories()], | ||||||
|     }, |     }, | ||||||
|  |     getPreferredBadge: { action: true }, | ||||||
|  |     groupConversations: { | ||||||
|  |       defaultValue: Array.from(Array(7), getDefaultGroup), | ||||||
|  |     }, | ||||||
|  |     groupStories: { | ||||||
|  |       defaultValue: Array.from(Array(2), getDefaultGroup), | ||||||
|  |     }, | ||||||
|     i18n: { |     i18n: { | ||||||
|       defaultValue: i18n, |       defaultValue: i18n, | ||||||
|     }, |     }, | ||||||
|  | @ -30,10 +43,12 @@ export default { | ||||||
|       defaultValue: getDefaultConversation(), |       defaultValue: getDefaultConversation(), | ||||||
|     }, |     }, | ||||||
|     onClose: { action: true }, |     onClose: { action: true }, | ||||||
|  |     onDistributionListCreated: { action: true }, | ||||||
|     onSend: { action: true }, |     onSend: { action: true }, | ||||||
|     signalConnections: { |     signalConnections: { | ||||||
|       defaultValue: Array.from(Array(42), getDefaultConversation), |       defaultValue: Array.from(Array(42), getDefaultConversation), | ||||||
|     }, |     }, | ||||||
|  |     tagGroupsAsNewGroupStory: { action: true }, | ||||||
|   }, |   }, | ||||||
| } as Meta; | } as Meta; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,27 +1,61 @@ | ||||||
| // Copyright 2022 Signal Messenger, LLC
 | // Copyright 2022 Signal Messenger, LLC
 | ||||||
| // SPDX-License-Identifier: AGPL-3.0-only
 | // SPDX-License-Identifier: AGPL-3.0-only
 | ||||||
| 
 | 
 | ||||||
| import React, { useMemo, useState } from 'react'; | import React, { useEffect, useMemo, useState } from 'react'; | ||||||
|  | 
 | ||||||
|  | import { SearchInput } from './SearchInput'; | ||||||
|  | import { filterAndSortConversationsByRecent } from '../util/filterAndSortConversations'; | ||||||
| 
 | 
 | ||||||
| import type { ConversationType } from '../state/ducks/conversations'; | import type { ConversationType } from '../state/ducks/conversations'; | ||||||
| import type { LocalizerType } from '../types/Util'; | import type { LocalizerType } from '../types/Util'; | ||||||
|  | import type { PreferredBadgeSelectorType } from '../state/selectors/badges'; | ||||||
| import type { StoryDistributionListDataType } from '../state/ducks/storyDistributionLists'; | import type { StoryDistributionListDataType } from '../state/ducks/storyDistributionLists'; | ||||||
| import type { UUIDStringType } from '../types/UUID'; | import type { UUIDStringType } from '../types/UUID'; | ||||||
| import { Avatar, AvatarSize } from './Avatar'; | import { Avatar, AvatarSize } from './Avatar'; | ||||||
| import { Checkbox } from './Checkbox'; | import { Checkbox } from './Checkbox'; | ||||||
|  | import { ContextMenu } from './ContextMenu'; | ||||||
|  | import { | ||||||
|  |   EditDistributionList, | ||||||
|  |   Page as StoriesSettingsPage, | ||||||
|  | } from './StoriesSettingsModal'; | ||||||
| import { MY_STORIES_ID, getStoryDistributionListName } from '../types/Stories'; | import { MY_STORIES_ID, getStoryDistributionListName } from '../types/Stories'; | ||||||
| import { Modal } from './Modal'; | import { Modal } from './Modal'; | ||||||
| import { StoryDistributionListName } from './StoryDistributionListName'; | import { StoryDistributionListName } from './StoryDistributionListName'; | ||||||
|  | import { Theme } from '../util/theme'; | ||||||
| 
 | 
 | ||||||
| export type PropsType = { | export type PropsType = { | ||||||
|  |   candidateConversations: Array<ConversationType>; | ||||||
|   distributionLists: Array<StoryDistributionListDataType>; |   distributionLists: Array<StoryDistributionListDataType>; | ||||||
|  |   getPreferredBadge: PreferredBadgeSelectorType; | ||||||
|  |   groupConversations: Array<ConversationType>; | ||||||
|  |   groupStories: Array<ConversationType>; | ||||||
|   i18n: LocalizerType; |   i18n: LocalizerType; | ||||||
|   me: ConversationType; |   me: ConversationType; | ||||||
|   onClose: () => unknown; |   onClose: () => unknown; | ||||||
|   onSend: (listIds: Array<UUIDStringType>) => unknown; |   onDistributionListCreated: ( | ||||||
|  |     name: string, | ||||||
|  |     viewerUuids: Array<UUIDStringType> | ||||||
|  |   ) => unknown; | ||||||
|  |   onSend: ( | ||||||
|  |     listIds: Array<UUIDStringType>, | ||||||
|  |     conversationIds: Array<string> | ||||||
|  |   ) => unknown; | ||||||
|   signalConnections: Array<ConversationType>; |   signalConnections: Array<ConversationType>; | ||||||
|  |   tagGroupsAsNewGroupStory: (cids: Array<string>) => unknown; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | enum SendStoryPage { | ||||||
|  |   SendStory = 'SendStory', | ||||||
|  |   ChooseGroups = 'ChooseGroups', | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const Page = { | ||||||
|  |   ...SendStoryPage, | ||||||
|  |   ...StoriesSettingsPage, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | type PageType = SendStoryPage | StoriesSettingsPage; | ||||||
|  | 
 | ||||||
| function getListViewers( | function getListViewers( | ||||||
|   list: StoryDistributionListDataType, |   list: StoryDistributionListDataType, | ||||||
|   i18n: LocalizerType, |   i18n: LocalizerType, | ||||||
|  | @ -36,36 +70,220 @@ function getListViewers( | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   return memberCount === 1 |   return memberCount === 1 | ||||||
|     ? i18n('StoriesSettingsModal__viewers--singular', ['1']) |     ? i18n('StoriesSettings__viewers--singular', ['1']) | ||||||
|     : i18n('StoriesSettings__viewers--plural', [String(memberCount)]); |     : i18n('StoriesSettings__viewers--plural', [String(memberCount)]); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const SendStoryModal = ({ | export const SendStoryModal = ({ | ||||||
|  |   candidateConversations, | ||||||
|   distributionLists, |   distributionLists, | ||||||
|  |   getPreferredBadge, | ||||||
|  |   groupConversations, | ||||||
|  |   groupStories, | ||||||
|   i18n, |   i18n, | ||||||
|   me, |   me, | ||||||
|   onClose, |   onClose, | ||||||
|  |   onDistributionListCreated, | ||||||
|   onSend, |   onSend, | ||||||
|   signalConnections, |   signalConnections, | ||||||
|  |   tagGroupsAsNewGroupStory, | ||||||
| }: PropsType): JSX.Element => { | }: PropsType): JSX.Element => { | ||||||
|  |   const [page, setPage] = useState<PageType>(Page.SendStory); | ||||||
|  | 
 | ||||||
|   const [selectedListIds, setSelectedListIds] = useState<Set<UUIDStringType>>( |   const [selectedListIds, setSelectedListIds] = useState<Set<UUIDStringType>>( | ||||||
|     new Set() |     new Set() | ||||||
|   ); |   ); | ||||||
|   const selectedListNames = useMemo( |   const [selectedGroupIds, setSelectedGroupIds] = useState<Set<string>>( | ||||||
|  |     new Set() | ||||||
|  |   ); | ||||||
|  |   const selectedStoryNames = useMemo( | ||||||
|     () => |     () => | ||||||
|       distributionLists |       distributionLists | ||||||
|         .filter(list => selectedListIds.has(list.id)) |         .filter(list => selectedListIds.has(list.id)) | ||||||
|         .map(list => list.name), |         .map(list => list.name) | ||||||
|     [distributionLists, selectedListIds] |         .concat( | ||||||
|  |           groupStories | ||||||
|  |             .filter(group => selectedGroupIds.has(group.id)) | ||||||
|  |             .map(group => group.title) | ||||||
|  |         ), | ||||||
|  |     [distributionLists, groupStories, selectedGroupIds, selectedListIds] | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   return ( |   const [searchTerm, setSearchTerm] = useState(''); | ||||||
|     <Modal | 
 | ||||||
|       hasXButton |   const [filteredConversations, setFilteredConversations] = useState( | ||||||
|  |     filterAndSortConversationsByRecent( | ||||||
|  |       groupConversations, | ||||||
|  |       searchTerm, | ||||||
|  |       undefined | ||||||
|  |     ) | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   const normalizedSearchTerm = searchTerm.trim(); | ||||||
|  | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     const timeout = setTimeout(() => { | ||||||
|  |       setFilteredConversations( | ||||||
|  |         filterAndSortConversationsByRecent( | ||||||
|  |           groupConversations, | ||||||
|  |           normalizedSearchTerm, | ||||||
|  |           undefined | ||||||
|  |         ) | ||||||
|  |       ); | ||||||
|  |     }, 200); | ||||||
|  |     return () => { | ||||||
|  |       clearTimeout(timeout); | ||||||
|  |     }; | ||||||
|  |   }, [groupConversations, normalizedSearchTerm, setFilteredConversations]); | ||||||
|  | 
 | ||||||
|  |   const [chosenGroupIds, setChosenGroupIds] = useState<Set<string>>( | ||||||
|  |     new Set<string>() | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   const chosenGroupNames = useMemo( | ||||||
|  |     () => | ||||||
|  |       filteredConversations | ||||||
|  |         .filter(group => chosenGroupIds.has(group.id)) | ||||||
|  |         .map(group => group.title), | ||||||
|  |     [filteredConversations, chosenGroupIds] | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   const [selectedContacts, setSelectedContacts] = useState< | ||||||
|  |     Array<ConversationType> | ||||||
|  |   >([]); | ||||||
|  | 
 | ||||||
|  |   let content: JSX.Element; | ||||||
|  |   if (page === Page.ChooseViewers || page === Page.NameStory) { | ||||||
|  |     content = ( | ||||||
|  |       <EditDistributionList | ||||||
|  |         candidateConversations={candidateConversations} | ||||||
|  |         getPreferredBadge={getPreferredBadge} | ||||||
|         i18n={i18n} |         i18n={i18n} | ||||||
|       onClose={onClose} |         onDone={(name, uuids) => { | ||||||
|       title={i18n('SendStoryModal__title')} |           onDistributionListCreated(name, uuids); | ||||||
|  |           setPage(Page.SendStory); | ||||||
|  |         }} | ||||||
|  |         onViewersUpdated={() => { | ||||||
|  |           if (page === Page.ChooseViewers) { | ||||||
|  |             setPage(Page.NameStory); | ||||||
|  |           } else { | ||||||
|  |             setPage(Page.SendStory); | ||||||
|  |           } | ||||||
|  |         }} | ||||||
|  |         page={page} | ||||||
|  |         selectedContacts={selectedContacts} | ||||||
|  |         setSelectedContacts={setSelectedContacts} | ||||||
|  |       /> | ||||||
|  |     ); | ||||||
|  |   } else if (page === Page.ChooseGroups) { | ||||||
|  |     content = ( | ||||||
|  |       <> | ||||||
|  |         <SearchInput | ||||||
|  |           disabled={groupConversations.length === 0} | ||||||
|  |           i18n={i18n} | ||||||
|  |           placeholder={i18n('contactSearchPlaceholder')} | ||||||
|  |           moduleClassName="StoriesSettingsModal__search" | ||||||
|  |           onChange={event => { | ||||||
|  |             setSearchTerm(event.target.value); | ||||||
|  |           }} | ||||||
|  |           value={searchTerm} | ||||||
|  |         /> | ||||||
|  |         {filteredConversations.length ? ( | ||||||
|  |           filteredConversations.map(group => ( | ||||||
|  |             <Checkbox | ||||||
|  |               checked={chosenGroupIds.has(group.id)} | ||||||
|  |               key={group.id} | ||||||
|  |               label={group.title} | ||||||
|  |               moduleClassName="SendStoryModal__distribution-list" | ||||||
|  |               name="SendStoryModal__distribution-list" | ||||||
|  |               onChange={(value: boolean) => { | ||||||
|  |                 setChosenGroupIds(groupIds => { | ||||||
|  |                   if (value) { | ||||||
|  |                     groupIds.add(group.id); | ||||||
|  |                   } else { | ||||||
|  |                     groupIds.delete(group.id); | ||||||
|  |                   } | ||||||
|  |                   return new Set([...groupIds]); | ||||||
|  |                 }); | ||||||
|  |               }} | ||||||
|             > |             > | ||||||
|  |               {({ id, checkboxNode }) => ( | ||||||
|  |                 <> | ||||||
|  |                   <label | ||||||
|  |                     className="SendStoryModal__distribution-list__label" | ||||||
|  |                     htmlFor={id} | ||||||
|  |                   > | ||||||
|  |                     <Avatar | ||||||
|  |                       acceptedMessageRequest={group.acceptedMessageRequest} | ||||||
|  |                       avatarPath={group.avatarPath} | ||||||
|  |                       badge={undefined} | ||||||
|  |                       color={group.color} | ||||||
|  |                       conversationType={group.type} | ||||||
|  |                       i18n={i18n} | ||||||
|  |                       isMe={false} | ||||||
|  |                       sharedGroupNames={[]} | ||||||
|  |                       size={AvatarSize.THIRTY_SIX} | ||||||
|  |                       title={group.title} | ||||||
|  |                     /> | ||||||
|  | 
 | ||||||
|  |                     <div className="SendStoryModal__distribution-list__info"> | ||||||
|  |                       <div className="SendStoryModal__distribution-list__name"> | ||||||
|  |                         {group.title} | ||||||
|  |                       </div> | ||||||
|  | 
 | ||||||
|  |                       <div className="SendStoryModal__distribution-list__description"> | ||||||
|  |                         {group.membersCount === 1 | ||||||
|  |                           ? i18n('ConversationHero--members-1') | ||||||
|  |                           : i18n('ConversationHero--members', [ | ||||||
|  |                               String(group.membersCount), | ||||||
|  |                             ])} | ||||||
|  |                       </div> | ||||||
|  |                     </div> | ||||||
|  |                   </label> | ||||||
|  |                   {checkboxNode} | ||||||
|  |                 </> | ||||||
|  |               )} | ||||||
|  |             </Checkbox> | ||||||
|  |           )) | ||||||
|  |         ) : ( | ||||||
|  |           <div className="module-ForwardMessageModal__no-candidate-contacts"> | ||||||
|  |             {i18n('noContactsFound')} | ||||||
|  |           </div> | ||||||
|  |         )} | ||||||
|  |       </> | ||||||
|  |     ); | ||||||
|  |   } else { | ||||||
|  |     content = ( | ||||||
|  |       <> | ||||||
|  |         <div className="SendStoryModal__top-bar"> | ||||||
|  |           {i18n('stories')} | ||||||
|  |           <ContextMenu | ||||||
|  |             aria-label={i18n('SendStoryModal__new')} | ||||||
|  |             i18n={i18n} | ||||||
|  |             menuOptions={[ | ||||||
|  |               { | ||||||
|  |                 label: i18n('SendStoryModal__new-private--title'), | ||||||
|  |                 description: i18n('SendStoryModal__new-private--description'), | ||||||
|  |                 icon: 'SendStoryModal__icon--lock', | ||||||
|  |                 onClick: () => setPage(Page.ChooseViewers), | ||||||
|  |               }, | ||||||
|  |               { | ||||||
|  |                 label: i18n('SendStoryModal__new-group--title'), | ||||||
|  |                 description: i18n('SendStoryModal__new-group--description'), | ||||||
|  |                 icon: 'SendStoryModal__icon--group', | ||||||
|  |                 onClick: () => setPage(Page.ChooseGroups), | ||||||
|  |               }, | ||||||
|  |             ]} | ||||||
|  |             moduleClassName="SendStoryModal__new-story" | ||||||
|  |             popperOptions={{ | ||||||
|  |               placement: 'bottom', | ||||||
|  |               strategy: 'absolute', | ||||||
|  |             }} | ||||||
|  |             theme={Theme.Dark} | ||||||
|  |           > | ||||||
|  |             {i18n('SendStoryModal__new')} | ||||||
|  |           </ContextMenu> | ||||||
|  |         </div> | ||||||
|         {distributionLists.map(list => ( |         {distributionLists.map(list => ( | ||||||
|           <Checkbox |           <Checkbox | ||||||
|             checked={selectedListIds.has(list.id)} |             checked={selectedListIds.has(list.id)} | ||||||
|  | @ -74,17 +292,14 @@ export const SendStoryModal = ({ | ||||||
|             moduleClassName="SendStoryModal__distribution-list" |             moduleClassName="SendStoryModal__distribution-list" | ||||||
|             name="SendStoryModal__distribution-list" |             name="SendStoryModal__distribution-list" | ||||||
|             onChange={(value: boolean) => { |             onChange={(value: boolean) => { | ||||||
|  |               setSelectedListIds(listIds => { | ||||||
|                 if (value) { |                 if (value) { | ||||||
|               setSelectedListIds(listIds => { |  | ||||||
|                   listIds.add(list.id); |                   listIds.add(list.id); | ||||||
|                 return new Set([...listIds]); |  | ||||||
|               }); |  | ||||||
|                 } else { |                 } else { | ||||||
|               setSelectedListIds(listIds => { |  | ||||||
|                   listIds.delete(list.id); |                   listIds.delete(list.id); | ||||||
|  |                 } | ||||||
|                 return new Set([...listIds]); |                 return new Set([...listIds]); | ||||||
|               }); |               }); | ||||||
|             } |  | ||||||
|             }} |             }} | ||||||
|           > |           > | ||||||
|             {({ id, checkboxNode }) => ( |             {({ id, checkboxNode }) => ( | ||||||
|  | @ -129,25 +344,146 @@ export const SendStoryModal = ({ | ||||||
|             )} |             )} | ||||||
|           </Checkbox> |           </Checkbox> | ||||||
|         ))} |         ))} | ||||||
|  |         {groupStories.map(group => ( | ||||||
|  |           <Checkbox | ||||||
|  |             checked={selectedGroupIds.has(group.id)} | ||||||
|  |             key={group.id} | ||||||
|  |             label={group.title} | ||||||
|  |             moduleClassName="SendStoryModal__distribution-list" | ||||||
|  |             name="SendStoryModal__distribution-list" | ||||||
|  |             onChange={(value: boolean) => { | ||||||
|  |               setSelectedGroupIds(groupIds => { | ||||||
|  |                 if (value) { | ||||||
|  |                   groupIds.add(group.id); | ||||||
|  |                 } else { | ||||||
|  |                   groupIds.delete(group.id); | ||||||
|  |                 } | ||||||
|  |                 return new Set([...groupIds]); | ||||||
|  |               }); | ||||||
|  |             }} | ||||||
|  |           > | ||||||
|  |             {({ id, checkboxNode }) => ( | ||||||
|  |               <> | ||||||
|  |                 <label | ||||||
|  |                   className="SendStoryModal__distribution-list__label" | ||||||
|  |                   htmlFor={id} | ||||||
|  |                 > | ||||||
|  |                   <Avatar | ||||||
|  |                     acceptedMessageRequest={group.acceptedMessageRequest} | ||||||
|  |                     avatarPath={group.avatarPath} | ||||||
|  |                     badge={undefined} | ||||||
|  |                     color={group.color} | ||||||
|  |                     conversationType={group.type} | ||||||
|  |                     i18n={i18n} | ||||||
|  |                     isMe={false} | ||||||
|  |                     sharedGroupNames={[]} | ||||||
|  |                     size={AvatarSize.THIRTY_SIX} | ||||||
|  |                     title={group.title} | ||||||
|  |                   /> | ||||||
| 
 | 
 | ||||||
|       <Modal.ButtonFooter moduleClassName="SendStoryModal"> |                   <div className="SendStoryModal__distribution-list__info"> | ||||||
|         <div className="SendStoryModal__selected-lists"> |                     <div className="SendStoryModal__distribution-list__name"> | ||||||
|           {selectedListNames |                       {group.title} | ||||||
|             .map(listName => |  | ||||||
|               getStoryDistributionListName(i18n, listName, listName) |  | ||||||
|             ) |  | ||||||
|             .join(', ')} |  | ||||||
|                     </div> |                     </div> | ||||||
|  | 
 | ||||||
|  |                     <div className="SendStoryModal__distribution-list__description"> | ||||||
|  |                       {group.membersCount === 1 | ||||||
|  |                         ? i18n('ConversationHero--members-1') | ||||||
|  |                         : i18n('ConversationHero--members', [ | ||||||
|  |                             String(group.membersCount), | ||||||
|  |                           ])} | ||||||
|  |                     </div> | ||||||
|  |                   </div> | ||||||
|  |                 </label> | ||||||
|  |                 {checkboxNode} | ||||||
|  |               </> | ||||||
|  |             )} | ||||||
|  |           </Checkbox> | ||||||
|  |         ))} | ||||||
|  |       </> | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   let modalTitle: string; | ||||||
|  |   if (page === Page.ChooseGroups) { | ||||||
|  |     modalTitle = i18n('SendStoryModal__choose-groups'); | ||||||
|  |   } else if (page === Page.NameStory) { | ||||||
|  |     modalTitle = i18n('StoriesSettings__name-story'); | ||||||
|  |   } else if (page === Page.ChooseViewers) { | ||||||
|  |     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) { | ||||||
|  |     modalFooter = ( | ||||||
|  |       <Modal.ButtonFooter moduleClassName="SendStoryModal"> | ||||||
|  |         <div className="SendStoryModal__selected-lists">{selectedNames}</div> | ||||||
|  |         {page === Page.ChooseGroups && ( | ||||||
|           <button |           <button | ||||||
|           aria-label="SendStoryModal__send" |             aria-label="SendStoryModal__ok" | ||||||
|           className="SendStoryModal__send" |             className="SendStoryModal__ok" | ||||||
|           disabled={!selectedListIds.size} |             disabled={!chosenGroupIds.size} | ||||||
|             onClick={() => { |             onClick={() => { | ||||||
|             onSend(Array.from(selectedListIds)); |               tagGroupsAsNewGroupStory(Array.from(chosenGroupIds)); | ||||||
|  |               setChosenGroupIds(new Set()); | ||||||
|  |               setPage(Page.SendStory); | ||||||
|             }} |             }} | ||||||
|             type="button" |             type="button" | ||||||
|           /> |           /> | ||||||
|  |         )} | ||||||
|  |         {page === Page.SendStory && ( | ||||||
|  |           <button | ||||||
|  |             aria-label="SendStoryModal__send" | ||||||
|  |             className="SendStoryModal__send" | ||||||
|  |             disabled={!selectedListIds.size && !selectedGroupIds.size} | ||||||
|  |             onClick={() => { | ||||||
|  |               onSend(Array.from(selectedListIds), Array.from(selectedGroupIds)); | ||||||
|  |             }} | ||||||
|  |             type="button" | ||||||
|  |           /> | ||||||
|  |         )} | ||||||
|       </Modal.ButtonFooter> |       </Modal.ButtonFooter> | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <Modal | ||||||
|  |       hasStickyButtons | ||||||
|  |       hasXButton | ||||||
|  |       i18n={i18n} | ||||||
|  |       modalFooter={modalFooter} | ||||||
|  |       onBackButtonClick={ | ||||||
|  |         hasBackButton | ||||||
|  |           ? () => { | ||||||
|  |               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} | ||||||
|  |     > | ||||||
|  |       {content} | ||||||
|     </Modal> |     </Modal> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -59,7 +59,7 @@ export type PropsType = { | ||||||
|   toggleSignalConnectionsModal: () => unknown; |   toggleSignalConnectionsModal: () => unknown; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| enum Page { | export enum Page { | ||||||
|   DistributionLists = 'DistributionLists', |   DistributionLists = 'DistributionLists', | ||||||
|   AddViewer = 'AddViewer', |   AddViewer = 'AddViewer', | ||||||
|   ChooseViewers = 'ChooseViewers', |   ChooseViewers = 'ChooseViewers', | ||||||
|  | @ -105,73 +105,15 @@ export const StoriesSettingsModal = ({ | ||||||
| 
 | 
 | ||||||
|   const [page, setPage] = useState<Page>(Page.DistributionLists); |   const [page, setPage] = useState<Page>(Page.DistributionLists); | ||||||
| 
 | 
 | ||||||
|   const [storyName, setStoryName] = useState(''); |  | ||||||
| 
 |  | ||||||
|   const [searchTerm, setSearchTerm] = useState(''); |  | ||||||
| 
 |  | ||||||
|   const [filteredConversations, setFilteredConversations] = useState( |  | ||||||
|     filterConversations(candidateConversations, searchTerm) |  | ||||||
|   ); |  | ||||||
| 
 |  | ||||||
|   const [selectedContacts, setSelectedContacts] = useState< |   const [selectedContacts, setSelectedContacts] = useState< | ||||||
|     Array<ConversationType> |     Array<ConversationType> | ||||||
|   >([]); |   >([]); | ||||||
| 
 | 
 | ||||||
|   const contactLookup = useMemo(() => { |  | ||||||
|     const map = new Map(); |  | ||||||
|     candidateConversations.forEach(contact => { |  | ||||||
|       map.set(contact.id, contact); |  | ||||||
|     }); |  | ||||||
|     return map; |  | ||||||
|   }, [candidateConversations]); |  | ||||||
| 
 |  | ||||||
|   const toggleSelectedConversation = useCallback( |  | ||||||
|     (conversationId: string) => { |  | ||||||
|       let removeContact = false; |  | ||||||
|       const nextSelectedContacts = selectedContacts.filter(contact => { |  | ||||||
|         if (contact.id === conversationId) { |  | ||||||
|           removeContact = true; |  | ||||||
|           return false; |  | ||||||
|         } |  | ||||||
|         return true; |  | ||||||
|       }); |  | ||||||
|       if (removeContact) { |  | ||||||
|         setSelectedContacts(nextSelectedContacts); |  | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       const selectedContact = contactLookup.get(conversationId); |  | ||||||
|       if (selectedContact) { |  | ||||||
|         setSelectedContacts([...nextSelectedContacts, selectedContact]); |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     [contactLookup, selectedContacts, setSelectedContacts] |  | ||||||
|   ); |  | ||||||
| 
 |  | ||||||
|   const normalizedSearchTerm = searchTerm.trim(); |  | ||||||
| 
 |  | ||||||
|   useEffect(() => { |  | ||||||
|     const timeout = setTimeout(() => { |  | ||||||
|       setFilteredConversations( |  | ||||||
|         filterConversations(candidateConversations, normalizedSearchTerm) |  | ||||||
|       ); |  | ||||||
|     }, 200); |  | ||||||
|     return () => { |  | ||||||
|       clearTimeout(timeout); |  | ||||||
|     }; |  | ||||||
|   }, [candidateConversations, normalizedSearchTerm, setFilteredConversations]); |  | ||||||
| 
 |  | ||||||
|   const resetChooseViewersScreen = useCallback(() => { |   const resetChooseViewersScreen = useCallback(() => { | ||||||
|     setSelectedContacts([]); |     setSelectedContacts([]); | ||||||
|     setSearchTerm(''); |  | ||||||
|     setPage(Page.DistributionLists); |     setPage(Page.DistributionLists); | ||||||
|   }, []); |   }, []); | ||||||
| 
 | 
 | ||||||
|   const selectedConversationUuids: Set<UUIDStringType> = useMemo( |  | ||||||
|     () => |  | ||||||
|       new Set(selectedContacts.map(contact => contact.uuid).filter(isNotNil)), |  | ||||||
|     [selectedContacts] |  | ||||||
|   ); |  | ||||||
| 
 |  | ||||||
|   const [confirmDeleteListId, setConfirmDeleteListId] = useState< |   const [confirmDeleteListId, setConfirmDeleteListId] = useState< | ||||||
|     string | undefined |     string | undefined | ||||||
|   >(); |   >(); | ||||||
|  | @ -184,158 +126,37 @@ export const StoriesSettingsModal = ({ | ||||||
|       } |       } | ||||||
|   >(); |   >(); | ||||||
| 
 | 
 | ||||||
|   let content: JSX.Element; |   let content: JSX.Element | null; | ||||||
|   if (page === Page.NameStory) { | 
 | ||||||
|  |   if (page !== Page.DistributionLists) { | ||||||
|     content = ( |     content = ( | ||||||
|       <> |       <EditDistributionList | ||||||
|         <div className="StoriesSettingsModal__name-story-avatar-container"> |         candidateConversations={candidateConversations} | ||||||
|           <div className="StoriesSettingsModal__list__avatar--private StoriesSettingsModal__list__avatar--private--large" /> |         getPreferredBadge={getPreferredBadge} | ||||||
|         </div> |  | ||||||
| 
 |  | ||||||
|         <Input |  | ||||||
|         i18n={i18n} |         i18n={i18n} | ||||||
|           onChange={setStoryName} |         onDone={(name, uuids) => { | ||||||
|           placeholder={i18n('StoriesSettings__name-placeholder')} |           onDistributionListCreated(name, uuids); | ||||||
|           value={storyName} |           resetChooseViewersScreen(); | ||||||
|         /> |         }} | ||||||
| 
 |         onViewersUpdated={uuids => { | ||||||
|         <div className="StoriesSettingsModal__title"> |           if (listToEditId && page === Page.AddViewer) { | ||||||
|           {i18n('StoriesSettings__who-can-see')} |             onViewersUpdated(listToEditId, uuids); | ||||||
|         </div> |             resetChooseViewersScreen(); | ||||||
| 
 |  | ||||||
|         {selectedContacts.map(contact => ( |  | ||||||
|           <div |  | ||||||
|             className="StoriesSettingsModal__list StoriesSettingsModal__list--no-pointer" |  | ||||||
|             key={contact.id} |  | ||||||
|           > |  | ||||||
|             <span className="StoriesSettingsModal__list__left"> |  | ||||||
|               <Avatar |  | ||||||
|                 acceptedMessageRequest={contact.acceptedMessageRequest} |  | ||||||
|                 avatarPath={contact.avatarPath} |  | ||||||
|                 badge={getPreferredBadge(contact.badges)} |  | ||||||
|                 color={contact.color} |  | ||||||
|                 conversationType={contact.type} |  | ||||||
|                 i18n={i18n} |  | ||||||
|                 isMe |  | ||||||
|                 sharedGroupNames={contact.sharedGroupNames} |  | ||||||
|                 size={AvatarSize.THIRTY_SIX} |  | ||||||
|                 theme={ThemeType.dark} |  | ||||||
|                 title={contact.title} |  | ||||||
|               /> |  | ||||||
|               <span className="StoriesSettingsModal__list__title"> |  | ||||||
|                 {contact.title} |  | ||||||
|               </span> |  | ||||||
|             </span> |  | ||||||
|           </div> |  | ||||||
|         ))} |  | ||||||
|       </> |  | ||||||
|     ); |  | ||||||
|   } else if ( |  | ||||||
|     page === Page.AddViewer || |  | ||||||
|     page === Page.ChooseViewers || |  | ||||||
|     page === Page.HideStoryFrom |  | ||||||
|   ) { |  | ||||||
|     const rowCount = filteredConversations.length; |  | ||||||
|     const getRow = (index: number): undefined | Row => { |  | ||||||
|       const contact = filteredConversations[index]; |  | ||||||
|       if (!contact || !contact.uuid) { |  | ||||||
|         return undefined; |  | ||||||
|           } |           } | ||||||
| 
 | 
 | ||||||
|       const isSelected = selectedConversationUuids.has(UUID.cast(contact.uuid)); |           if (page === Page.ChooseViewers) { | ||||||
|  |             setPage(Page.NameStory); | ||||||
|  |           } | ||||||
| 
 | 
 | ||||||
|       return { |           if (page === Page.HideStoryFrom) { | ||||||
|         type: RowType.ContactCheckbox, |             onHideMyStoriesFrom(uuids); | ||||||
|         contact, |             resetChooseViewersScreen(); | ||||||
|         isChecked: isSelected, |           } | ||||||
|       }; |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     content = ( |  | ||||||
|       <> |  | ||||||
|         <SearchInput |  | ||||||
|           disabled={candidateConversations.length === 0} |  | ||||||
|           i18n={i18n} |  | ||||||
|           placeholder={i18n('contactSearchPlaceholder')} |  | ||||||
|           moduleClassName="StoriesSettingsModal__search" |  | ||||||
|           onChange={event => { |  | ||||||
|             setSearchTerm(event.target.value); |  | ||||||
|         }} |         }} | ||||||
|           value={searchTerm} |         page={page} | ||||||
|  |         selectedContacts={selectedContacts} | ||||||
|  |         setSelectedContacts={setSelectedContacts} | ||||||
|       /> |       /> | ||||||
|         {selectedContacts.length ? ( |  | ||||||
|           <div className="StoriesSettingsModal__tags"> |  | ||||||
|             {selectedContacts.map(contact => ( |  | ||||||
|               <div className="StoriesSettingsModal__tag" key={contact.id}> |  | ||||||
|                 <Avatar |  | ||||||
|                   acceptedMessageRequest={contact.acceptedMessageRequest} |  | ||||||
|                   avatarPath={contact.avatarPath} |  | ||||||
|                   badge={getPreferredBadge(contact.badges)} |  | ||||||
|                   color={contact.color} |  | ||||||
|                   conversationType={contact.type} |  | ||||||
|                   i18n={i18n} |  | ||||||
|                   isMe={contact.isMe} |  | ||||||
|                   sharedGroupNames={contact.sharedGroupNames} |  | ||||||
|                   size={AvatarSize.TWENTY_EIGHT} |  | ||||||
|                   theme={ThemeType.dark} |  | ||||||
|                   title={contact.title} |  | ||||||
|                 /> |  | ||||||
|                 <span className="StoriesSettingsModal__tag__name"> |  | ||||||
|                   {contact.firstName || |  | ||||||
|                     contact.profileName || |  | ||||||
|                     contact.phoneNumber} |  | ||||||
|                 </span> |  | ||||||
|                 <button |  | ||||||
|                   aria-label={i18n('StoriesSettings__remove--title', [ |  | ||||||
|                     contact.title, |  | ||||||
|                   ])} |  | ||||||
|                   className="StoriesSettingsModal__tag__remove" |  | ||||||
|                   onClick={() => toggleSelectedConversation(contact.id)} |  | ||||||
|                   type="button" |  | ||||||
|                 /> |  | ||||||
|               </div> |  | ||||||
|             ))} |  | ||||||
|           </div> |  | ||||||
|         ) : 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> |  | ||||||
|         )} |  | ||||||
|       </> |  | ||||||
|     ); |     ); | ||||||
|   } else if (listToEdit) { |   } else if (listToEdit) { | ||||||
|     const isMyStories = listToEdit.id === MY_STORIES_ID; |     const isMyStories = listToEdit.id === MY_STORIES_ID; | ||||||
|  | @ -662,61 +483,6 @@ export const StoriesSettingsModal = ({ | ||||||
|         title={modalTitle} |         title={modalTitle} | ||||||
|       > |       > | ||||||
|         {content} |         {content} | ||||||
|         {isChoosingViewers && ( |  | ||||||
|           <Modal.ButtonFooter> |  | ||||||
|             <Button |  | ||||||
|               disabled={selectedContacts.length === 0} |  | ||||||
|               onClick={() => { |  | ||||||
|                 if (listToEdit && page === Page.AddViewer) { |  | ||||||
|                   onViewersUpdated( |  | ||||||
|                     listToEdit.id, |  | ||||||
|                     Array.from(selectedConversationUuids) |  | ||||||
|                   ); |  | ||||||
|                   resetChooseViewersScreen(); |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 if (page === Page.ChooseViewers) { |  | ||||||
|                   setPage(Page.NameStory); |  | ||||||
|                 } |  | ||||||
|               }} |  | ||||||
|               variant={ButtonVariant.Primary} |  | ||||||
|             > |  | ||||||
|               {page === Page.AddViewer ? i18n('done') : i18n('next2')} |  | ||||||
|             </Button> |  | ||||||
|           </Modal.ButtonFooter> |  | ||||||
|         )} |  | ||||||
|         {page === Page.NameStory && ( |  | ||||||
|           <Modal.ButtonFooter> |  | ||||||
|             <Button |  | ||||||
|               disabled={!storyName} |  | ||||||
|               onClick={() => { |  | ||||||
|                 onDistributionListCreated( |  | ||||||
|                   storyName, |  | ||||||
|                   Array.from(selectedConversationUuids) |  | ||||||
|                 ); |  | ||||||
|                 setStoryName(''); |  | ||||||
|                 resetChooseViewersScreen(); |  | ||||||
|               }} |  | ||||||
|               variant={ButtonVariant.Primary} |  | ||||||
|             > |  | ||||||
|               {i18n('done')} |  | ||||||
|             </Button> |  | ||||||
|           </Modal.ButtonFooter> |  | ||||||
|         )} |  | ||||||
|         {page === Page.HideStoryFrom && ( |  | ||||||
|           <Modal.ButtonFooter> |  | ||||||
|             <Button |  | ||||||
|               disabled={selectedContacts.length === 0} |  | ||||||
|               onClick={() => { |  | ||||||
|                 onHideMyStoriesFrom(Array.from(selectedConversationUuids)); |  | ||||||
|                 resetChooseViewersScreen(); |  | ||||||
|               }} |  | ||||||
|               variant={ButtonVariant.Primary} |  | ||||||
|             > |  | ||||||
|               {i18n('update')} |  | ||||||
|             </Button> |  | ||||||
|           </Modal.ButtonFooter> |  | ||||||
|         )} |  | ||||||
|       </Modal> |       </Modal> | ||||||
|       {confirmDeleteListId && ( |       {confirmDeleteListId && ( | ||||||
|         <ConfirmationDialog |         <ConfirmationDialog | ||||||
|  | @ -765,3 +531,289 @@ export const StoriesSettingsModal = ({ | ||||||
|     </> |     </> | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  | 
 | ||||||
|  | type EditDistributionListPropsType = { | ||||||
|  |   onDone: (name: string, viewerUuids: Array<UUIDStringType>) => unknown; | ||||||
|  |   onViewersUpdated: (viewerUuids: Array<UUIDStringType>) => unknown; | ||||||
|  |   page: Page; | ||||||
|  |   selectedContacts: Array<ConversationType>; | ||||||
|  |   setSelectedContacts: (contacts: Array<ConversationType>) => unknown; | ||||||
|  | } & Pick<PropsType, 'candidateConversations' | 'getPreferredBadge' | 'i18n'>; | ||||||
|  | 
 | ||||||
|  | export const EditDistributionList = ({ | ||||||
|  |   candidateConversations, | ||||||
|  |   getPreferredBadge, | ||||||
|  |   i18n, | ||||||
|  |   onDone, | ||||||
|  |   onViewersUpdated, | ||||||
|  |   page, | ||||||
|  |   selectedContacts, | ||||||
|  |   setSelectedContacts, | ||||||
|  | }: EditDistributionListPropsType): JSX.Element | null => { | ||||||
|  |   const [storyName, setStoryName] = useState(''); | ||||||
|  |   const [searchTerm, setSearchTerm] = useState(''); | ||||||
|  | 
 | ||||||
|  |   const normalizedSearchTerm = searchTerm.trim(); | ||||||
|  | 
 | ||||||
|  |   const [filteredConversations, setFilteredConversations] = useState( | ||||||
|  |     filterConversations(candidateConversations, normalizedSearchTerm) | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     const timeout = setTimeout(() => { | ||||||
|  |       setFilteredConversations( | ||||||
|  |         filterConversations(candidateConversations, normalizedSearchTerm) | ||||||
|  |       ); | ||||||
|  |     }, 200); | ||||||
|  |     return () => { | ||||||
|  |       clearTimeout(timeout); | ||||||
|  |     }; | ||||||
|  |   }, [candidateConversations, normalizedSearchTerm, setFilteredConversations]); | ||||||
|  | 
 | ||||||
|  |   const isEditingDistributionList = | ||||||
|  |     page === Page.AddViewer || | ||||||
|  |     page === Page.ChooseViewers || | ||||||
|  |     page === Page.NameStory || | ||||||
|  |     page === Page.HideStoryFrom; | ||||||
|  | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (!isEditingDistributionList) { | ||||||
|  |       setSearchTerm(''); | ||||||
|  |     } | ||||||
|  |   }, [isEditingDistributionList]); | ||||||
|  | 
 | ||||||
|  |   const contactLookup = useMemo(() => { | ||||||
|  |     const map = new Map(); | ||||||
|  |     candidateConversations.forEach(contact => { | ||||||
|  |       map.set(contact.id, contact); | ||||||
|  |     }); | ||||||
|  |     return map; | ||||||
|  |   }, [candidateConversations]); | ||||||
|  | 
 | ||||||
|  |   const selectedConversationUuids: Set<UUIDStringType> = useMemo( | ||||||
|  |     () => | ||||||
|  |       new Set(selectedContacts.map(contact => contact.uuid).filter(isNotNil)), | ||||||
|  |     [selectedContacts] | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   const toggleSelectedConversation = useCallback( | ||||||
|  |     (conversationId: string) => { | ||||||
|  |       let removeContact = false; | ||||||
|  |       const nextSelectedContacts = selectedContacts.filter(contact => { | ||||||
|  |         if (contact.id === conversationId) { | ||||||
|  |           removeContact = true; | ||||||
|  |           return false; | ||||||
|  |         } | ||||||
|  |         return true; | ||||||
|  |       }); | ||||||
|  |       if (removeContact) { | ||||||
|  |         setSelectedContacts(nextSelectedContacts); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       const selectedContact = contactLookup.get(conversationId); | ||||||
|  |       if (selectedContact) { | ||||||
|  |         setSelectedContacts([...nextSelectedContacts, selectedContact]); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     [contactLookup, selectedContacts, setSelectedContacts] | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   const isChoosingViewers = | ||||||
|  |     page === Page.ChooseViewers || page === Page.AddViewer; | ||||||
|  | 
 | ||||||
|  |   if (page === Page.NameStory) { | ||||||
|  |     return ( | ||||||
|  |       <> | ||||||
|  |         <div className="StoriesSettingsModal__name-story-avatar-container"> | ||||||
|  |           <div className="StoriesSettingsModal__list__avatar--private StoriesSettingsModal__list__avatar--private--large" /> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         <Input | ||||||
|  |           i18n={i18n} | ||||||
|  |           onChange={setStoryName} | ||||||
|  |           placeholder={i18n('StoriesSettings__name-placeholder')} | ||||||
|  |           value={storyName} | ||||||
|  |         /> | ||||||
|  | 
 | ||||||
|  |         <div className="StoriesSettingsModal__title"> | ||||||
|  |           {i18n('StoriesSettings__who-can-see')} | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         {selectedContacts.map(contact => ( | ||||||
|  |           <div | ||||||
|  |             className="StoriesSettingsModal__list StoriesSettingsModal__list--no-pointer" | ||||||
|  |             key={contact.id} | ||||||
|  |           > | ||||||
|  |             <span className="StoriesSettingsModal__list__left"> | ||||||
|  |               <Avatar | ||||||
|  |                 acceptedMessageRequest={contact.acceptedMessageRequest} | ||||||
|  |                 avatarPath={contact.avatarPath} | ||||||
|  |                 badge={getPreferredBadge(contact.badges)} | ||||||
|  |                 color={contact.color} | ||||||
|  |                 conversationType={contact.type} | ||||||
|  |                 i18n={i18n} | ||||||
|  |                 isMe | ||||||
|  |                 sharedGroupNames={contact.sharedGroupNames} | ||||||
|  |                 size={AvatarSize.THIRTY_SIX} | ||||||
|  |                 theme={ThemeType.dark} | ||||||
|  |                 title={contact.title} | ||||||
|  |               /> | ||||||
|  |               <span className="StoriesSettingsModal__list__title"> | ||||||
|  |                 {contact.title} | ||||||
|  |               </span> | ||||||
|  |             </span> | ||||||
|  |           </div> | ||||||
|  |         ))} | ||||||
|  |         <Modal.ButtonFooter> | ||||||
|  |           <Button | ||||||
|  |             disabled={!storyName} | ||||||
|  |             onClick={() => { | ||||||
|  |               onDone(storyName, Array.from(selectedConversationUuids)); | ||||||
|  |               setStoryName(''); | ||||||
|  |             }} | ||||||
|  |             variant={ButtonVariant.Primary} | ||||||
|  |           > | ||||||
|  |             {i18n('done')} | ||||||
|  |           </Button> | ||||||
|  |         </Modal.ButtonFooter> | ||||||
|  |       </> | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if ( | ||||||
|  |     page === Page.AddViewer || | ||||||
|  |     page === Page.ChooseViewers || | ||||||
|  |     page === Page.HideStoryFrom | ||||||
|  |   ) { | ||||||
|  |     const rowCount = filteredConversations.length; | ||||||
|  |     const getRow = (index: number): undefined | Row => { | ||||||
|  |       const contact = filteredConversations[index]; | ||||||
|  |       if (!contact || !contact.uuid) { | ||||||
|  |         return undefined; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       const isSelected = selectedConversationUuids.has(UUID.cast(contact.uuid)); | ||||||
|  | 
 | ||||||
|  |       return { | ||||||
|  |         type: RowType.ContactCheckbox, | ||||||
|  |         contact, | ||||||
|  |         isChecked: isSelected, | ||||||
|  |       }; | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     return ( | ||||||
|  |       <> | ||||||
|  |         <SearchInput | ||||||
|  |           disabled={candidateConversations.length === 0} | ||||||
|  |           i18n={i18n} | ||||||
|  |           placeholder={i18n('contactSearchPlaceholder')} | ||||||
|  |           moduleClassName="StoriesSettingsModal__search" | ||||||
|  |           onChange={event => { | ||||||
|  |             setSearchTerm(event.target.value); | ||||||
|  |           }} | ||||||
|  |           value={searchTerm} | ||||||
|  |         /> | ||||||
|  |         {selectedContacts.length ? ( | ||||||
|  |           <div className="StoriesSettingsModal__tags"> | ||||||
|  |             {selectedContacts.map(contact => ( | ||||||
|  |               <div className="StoriesSettingsModal__tag" key={contact.id}> | ||||||
|  |                 <Avatar | ||||||
|  |                   acceptedMessageRequest={contact.acceptedMessageRequest} | ||||||
|  |                   avatarPath={contact.avatarPath} | ||||||
|  |                   badge={getPreferredBadge(contact.badges)} | ||||||
|  |                   color={contact.color} | ||||||
|  |                   conversationType={contact.type} | ||||||
|  |                   i18n={i18n} | ||||||
|  |                   isMe={contact.isMe} | ||||||
|  |                   sharedGroupNames={contact.sharedGroupNames} | ||||||
|  |                   size={AvatarSize.TWENTY_EIGHT} | ||||||
|  |                   theme={ThemeType.dark} | ||||||
|  |                   title={contact.title} | ||||||
|  |                 /> | ||||||
|  |                 <span className="StoriesSettingsModal__tag__name"> | ||||||
|  |                   {contact.firstName || | ||||||
|  |                     contact.profileName || | ||||||
|  |                     contact.phoneNumber} | ||||||
|  |                 </span> | ||||||
|  |                 <button | ||||||
|  |                   aria-label={i18n('StoriesSettings__remove--title', [ | ||||||
|  |                     contact.title, | ||||||
|  |                   ])} | ||||||
|  |                   className="StoriesSettingsModal__tag__remove" | ||||||
|  |                   onClick={() => toggleSelectedConversation(contact.id)} | ||||||
|  |                   type="button" | ||||||
|  |                 /> | ||||||
|  |               </div> | ||||||
|  |             ))} | ||||||
|  |           </div> | ||||||
|  |         ) : 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; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | @ -8,7 +8,10 @@ import type { PropsType } from './StoryCreator'; | ||||||
| import enMessages from '../../_locales/en/messages.json'; | import enMessages from '../../_locales/en/messages.json'; | ||||||
| import { StoryCreator } from './StoryCreator'; | import { StoryCreator } from './StoryCreator'; | ||||||
| import { fakeAttachment } from '../test-both/helpers/fakeAttachment'; | import { fakeAttachment } from '../test-both/helpers/fakeAttachment'; | ||||||
| import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation'; | import { | ||||||
|  |   getDefaultConversation, | ||||||
|  |   getDefaultGroup, | ||||||
|  | } from '../test-both/helpers/getDefaultConversation'; | ||||||
| import { getFakeDistributionLists } from '../test-both/helpers/getFakeDistributionLists'; | import { getFakeDistributionLists } from '../test-both/helpers/getFakeDistributionLists'; | ||||||
| import { setupI18n } from '../util/setupI18n'; | import { setupI18n } from '../util/setupI18n'; | ||||||
| 
 | 
 | ||||||
|  | @ -18,19 +21,30 @@ export default { | ||||||
|   title: 'Components/StoryCreator', |   title: 'Components/StoryCreator', | ||||||
|   component: StoryCreator, |   component: StoryCreator, | ||||||
|   argTypes: { |   argTypes: { | ||||||
|  |     candidateConversations: { | ||||||
|  |       defaultValue: Array.from(Array(100), getDefaultConversation), | ||||||
|  |     }, | ||||||
|     debouncedMaybeGrabLinkPreview: { action: true }, |     debouncedMaybeGrabLinkPreview: { action: true }, | ||||||
|     distributionLists: { defaultValue: getFakeDistributionLists() }, |     distributionLists: { defaultValue: getFakeDistributionLists() }, | ||||||
|     linkPreview: { |     getPreferredBadge: { action: true }, | ||||||
|       defaultValue: undefined, |     groupConversations: { | ||||||
|  |       defaultValue: Array.from(Array(7), getDefaultGroup), | ||||||
|  |     }, | ||||||
|  |     groupStories: { | ||||||
|  |       defaultValue: Array.from(Array(4), getDefaultGroup), | ||||||
|     }, |     }, | ||||||
|     i18n: { defaultValue: i18n }, |     i18n: { defaultValue: i18n }, | ||||||
|     installedPacks: { |     installedPacks: { | ||||||
|       defaultValue: [], |       defaultValue: [], | ||||||
|     }, |     }, | ||||||
|  |     linkPreview: { | ||||||
|  |       defaultValue: undefined, | ||||||
|  |     }, | ||||||
|     me: { |     me: { | ||||||
|       defaultValue: getDefaultConversation(), |       defaultValue: getDefaultConversation(), | ||||||
|     }, |     }, | ||||||
|     onClose: { action: true }, |     onClose: { action: true }, | ||||||
|  |     onDistributionListCreated: { action: true }, | ||||||
|     onSend: { action: true }, |     onSend: { action: true }, | ||||||
|     processAttachment: { action: true }, |     processAttachment: { action: true }, | ||||||
|     recentStickers: { |     recentStickers: { | ||||||
|  |  | ||||||
|  | @ -12,6 +12,7 @@ import type { ConversationType } from '../state/ducks/conversations'; | ||||||
| import type { LinkPreviewSourceType } from '../types/LinkPreview'; | import type { LinkPreviewSourceType } from '../types/LinkPreview'; | ||||||
| import type { LinkPreviewType } from '../types/message/LinkPreviews'; | import type { LinkPreviewType } from '../types/message/LinkPreviews'; | ||||||
| import type { LocalizerType } from '../types/Util'; | import type { LocalizerType } from '../types/Util'; | ||||||
|  | import type { PreferredBadgeSelectorType } from '../state/selectors/badges'; | ||||||
| import type { Props as StickerButtonProps } from './stickers/StickerButton'; | import type { Props as StickerButtonProps } from './stickers/StickerButton'; | ||||||
| import type { StoryDistributionListDataType } from '../state/ducks/storyDistributionLists'; | import type { StoryDistributionListDataType } from '../state/ducks/storyDistributionLists'; | ||||||
| import type { UUIDStringType } from '../types/UUID'; | import type { UUIDStringType } from '../types/UUID'; | ||||||
|  | @ -24,16 +25,24 @@ import { MediaEditor } from './MediaEditor'; | ||||||
| import { TextStoryCreator } from './TextStoryCreator'; | import { TextStoryCreator } from './TextStoryCreator'; | ||||||
| 
 | 
 | ||||||
| export type PropsType = { | export type PropsType = { | ||||||
|  |   candidateConversations: Array<ConversationType>; | ||||||
|   debouncedMaybeGrabLinkPreview: ( |   debouncedMaybeGrabLinkPreview: ( | ||||||
|     message: string, |     message: string, | ||||||
|     source: LinkPreviewSourceType |     source: LinkPreviewSourceType | ||||||
|   ) => unknown; |   ) => unknown; | ||||||
|   distributionLists: Array<StoryDistributionListDataType>; |   distributionLists: Array<StoryDistributionListDataType>; | ||||||
|   file?: File; |   file?: File; | ||||||
|  |   getPreferredBadge: PreferredBadgeSelectorType; | ||||||
|  |   groupConversations: Array<ConversationType>; | ||||||
|  |   groupStories: Array<ConversationType>; | ||||||
|   i18n: LocalizerType; |   i18n: LocalizerType; | ||||||
|   linkPreview?: LinkPreviewType; |   linkPreview?: LinkPreviewType; | ||||||
|   me: ConversationType; |   me: ConversationType; | ||||||
|   onClose: () => unknown; |   onClose: () => unknown; | ||||||
|  |   onDistributionListCreated: ( | ||||||
|  |     name: string, | ||||||
|  |     viewerUuids: Array<UUIDStringType> | ||||||
|  |   ) => unknown; | ||||||
|   onSend: ( |   onSend: ( | ||||||
|     listIds: Array<UUIDStringType>, |     listIds: Array<UUIDStringType>, | ||||||
|     conversationIds: Array<string>, |     conversationIds: Array<string>, | ||||||
|  | @ -43,21 +52,28 @@ export type PropsType = { | ||||||
|     file: File |     file: File | ||||||
|   ) => Promise<void | InMemoryAttachmentDraftType>; |   ) => Promise<void | InMemoryAttachmentDraftType>; | ||||||
|   signalConnections: Array<ConversationType>; |   signalConnections: Array<ConversationType>; | ||||||
|  |   tagGroupsAsNewGroupStory: (cids: Array<string>) => unknown; | ||||||
| } & Pick<StickerButtonProps, 'installedPacks' | 'recentStickers'>; | } & Pick<StickerButtonProps, 'installedPacks' | 'recentStickers'>; | ||||||
| 
 | 
 | ||||||
| export const StoryCreator = ({ | export const StoryCreator = ({ | ||||||
|  |   candidateConversations, | ||||||
|   debouncedMaybeGrabLinkPreview, |   debouncedMaybeGrabLinkPreview, | ||||||
|   distributionLists, |   distributionLists, | ||||||
|   file, |   file, | ||||||
|  |   getPreferredBadge, | ||||||
|  |   groupConversations, | ||||||
|  |   groupStories, | ||||||
|   i18n, |   i18n, | ||||||
|   installedPacks, |   installedPacks, | ||||||
|   linkPreview, |   linkPreview, | ||||||
|   me, |   me, | ||||||
|   onClose, |   onClose, | ||||||
|  |   onDistributionListCreated, | ||||||
|   onSend, |   onSend, | ||||||
|   processAttachment, |   processAttachment, | ||||||
|   recentStickers, |   recentStickers, | ||||||
|   signalConnections, |   signalConnections, | ||||||
|  |   tagGroupsAsNewGroupStory, | ||||||
| }: PropsType): JSX.Element => { | }: PropsType): JSX.Element => { | ||||||
|   const [draftAttachment, setDraftAttachment] = useState< |   const [draftAttachment, setDraftAttachment] = useState< | ||||||
|     AttachmentType | undefined |     AttachmentType | undefined | ||||||
|  | @ -100,20 +116,27 @@ export const StoryCreator = ({ | ||||||
|     <> |     <> | ||||||
|       {draftAttachment && ( |       {draftAttachment && ( | ||||||
|         <SendStoryModal |         <SendStoryModal | ||||||
|  |           candidateConversations={candidateConversations} | ||||||
|           distributionLists={distributionLists} |           distributionLists={distributionLists} | ||||||
|  |           getPreferredBadge={getPreferredBadge} | ||||||
|  |           groupConversations={groupConversations} | ||||||
|  |           groupStories={groupStories} | ||||||
|           i18n={i18n} |           i18n={i18n} | ||||||
|           me={me} |           me={me} | ||||||
|           onClose={() => setDraftAttachment(undefined)} |           onClose={() => setDraftAttachment(undefined)} | ||||||
|           onSend={listIds => { |           onDistributionListCreated={onDistributionListCreated} | ||||||
|             onSend(listIds, [], draftAttachment); |           onSend={(listIds, groupIds) => { | ||||||
|  |             onSend(listIds, groupIds, draftAttachment); | ||||||
|             setDraftAttachment(undefined); |             setDraftAttachment(undefined); | ||||||
|             onClose(); |             onClose(); | ||||||
|           }} |           }} | ||||||
|           signalConnections={signalConnections} |           signalConnections={signalConnections} | ||||||
|  |           tagGroupsAsNewGroupStory={tagGroupsAsNewGroupStory} | ||||||
|         /> |         /> | ||||||
|       )} |       )} | ||||||
|       {attachmentUrl && ( |       {attachmentUrl && ( | ||||||
|         <MediaEditor |         <MediaEditor | ||||||
|  |           doneButtonLabel={i18n('next2')} | ||||||
|           i18n={i18n} |           i18n={i18n} | ||||||
|           imageSrc={attachmentUrl} |           imageSrc={attachmentUrl} | ||||||
|           installedPacks={installedPacks} |           installedPacks={installedPacks} | ||||||
|  |  | ||||||
|  | @ -28,6 +28,7 @@ export const HEADER_CONTACT_NAME_CLASS_NAME = `${HEADER_NAME_CLASS_NAME}__contac | ||||||
| export const DATE_CLASS_NAME = `${HEADER_CLASS_NAME}__date`; | export const DATE_CLASS_NAME = `${HEADER_CLASS_NAME}__date`; | ||||||
| const MESSAGE_CLASS_NAME = `${CONTENT_CLASS_NAME}__message`; | const MESSAGE_CLASS_NAME = `${CONTENT_CLASS_NAME}__message`; | ||||||
| export const MESSAGE_TEXT_CLASS_NAME = `${MESSAGE_CLASS_NAME}__text`; | export const MESSAGE_TEXT_CLASS_NAME = `${MESSAGE_CLASS_NAME}__text`; | ||||||
|  | const CHECKBOX_CONTAINER_CLASS_NAME = `${BASE_CLASS_NAME}__checkbox--container`; | ||||||
| const CHECKBOX_CLASS_NAME = `${BASE_CLASS_NAME}__checkbox`; | const CHECKBOX_CLASS_NAME = `${BASE_CLASS_NAME}__checkbox`; | ||||||
| const SPINNER_CLASS_NAME = `${BASE_CLASS_NAME}__spinner`; | const SPINNER_CLASS_NAME = `${BASE_CLASS_NAME}__spinner`; | ||||||
| 
 | 
 | ||||||
|  | @ -131,6 +132,7 @@ export const BaseConversationListItem: FunctionComponent<PropsType> = | ||||||
|         ariaLabel = i18n('selectContact', [title]); |         ariaLabel = i18n('selectContact', [title]); | ||||||
|       } |       } | ||||||
|       actionNode = ( |       actionNode = ( | ||||||
|  |         <div className={CHECKBOX_CONTAINER_CLASS_NAME}> | ||||||
|           <input |           <input | ||||||
|             aria-label={ariaLabel} |             aria-label={ariaLabel} | ||||||
|             checked={checked} |             checked={checked} | ||||||
|  | @ -145,6 +147,7 @@ export const BaseConversationListItem: FunctionComponent<PropsType> = | ||||||
|             }} |             }} | ||||||
|             type="checkbox" |             type="checkbox" | ||||||
|           /> |           /> | ||||||
|  |         </div> | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -71,6 +71,14 @@ export async function sendStory( | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     const messageConversation = message.getConversation(); | ||||||
|  |     if (messageConversation !== conversation) { | ||||||
|  |       log.error( | ||||||
|  |         `stories.sendStory(${messageId}): Message conversation '${messageConversation?.idForLogging()}' does not match job conversation ${conversation.idForLogging()}` | ||||||
|  |       ); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     const attachments = message.get('attachments') || []; |     const attachments = message.get('attachments') || []; | ||||||
|     const [attachment] = attachments; |     const [attachment] = attachments; | ||||||
| 
 | 
 | ||||||
|  | @ -92,12 +100,17 @@ export async function sendStory( | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     const groupV2 = isGroupV2(conversation.attributes) | ||||||
|  |       ? conversation.getGroupV2Info() | ||||||
|  |       : undefined; | ||||||
|  | 
 | ||||||
|     // Some distribution lists need allowsReplies false, some need it set to true
 |     // Some distribution lists need allowsReplies false, some need it set to true
 | ||||||
|     // we create this proto (for the sync message) and also to re-use some of the
 |     // we create this proto (for the sync message) and also to re-use some of the
 | ||||||
|     // attributes inside it.
 |     // attributes inside it.
 | ||||||
|     return messaging.getStoryMessage({ |     return messaging.getStoryMessage({ | ||||||
|       allowsReplies: true, |       allowsReplies: true, | ||||||
|       fileAttachment, |       fileAttachment, | ||||||
|  |       groupV2, | ||||||
|       textAttachment, |       textAttachment, | ||||||
|       profileKey, |       profileKey, | ||||||
|     }); |     }); | ||||||
|  | @ -153,7 +166,7 @@ export async function sendStory( | ||||||
|       const messageConversation = message.getConversation(); |       const messageConversation = message.getConversation(); | ||||||
|       if (messageConversation !== conversation) { |       if (messageConversation !== conversation) { | ||||||
|         log.error( |         log.error( | ||||||
|           `stories.sendStory: Message conversation '${messageConversation?.idForLogging()}' does not match job conversation ${conversation.idForLogging()}` |           `stories.sendStory(${messageId}): Message conversation '${messageConversation?.idForLogging()}' does not match job conversation ${conversation.idForLogging()}` | ||||||
|         ); |         ); | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|  | @ -301,7 +314,9 @@ export async function sendStory( | ||||||
|           urgent: false, |           urgent: false, | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         message.doNotSendSyncMessage = true; |         // Do not send sync messages for distribution lists since that's sent
 | ||||||
|  |         // in bulk at the end.
 | ||||||
|  |         message.doNotSendSyncMessage = Boolean(distributionList); | ||||||
| 
 | 
 | ||||||
|         const messageSendPromise = message.send( |         const messageSendPromise = message.send( | ||||||
|           handleMessageSend(innerPromise, { |           handleMessageSend(innerPromise, { | ||||||
|  |  | ||||||
							
								
								
									
										1
									
								
								ts/model-types.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								ts/model-types.d.ts
									
										
									
									
										vendored
									
									
								
							|  | @ -343,6 +343,7 @@ export type ConversationAttributesType = { | ||||||
|   //   to leave a group.
 |   //   to leave a group.
 | ||||||
|   left?: boolean; |   left?: boolean; | ||||||
|   groupVersion?: number; |   groupVersion?: number; | ||||||
|  |   isGroupStorySendReady?: boolean; | ||||||
| 
 | 
 | ||||||
|   // GroupV1 only
 |   // GroupV1 only
 | ||||||
|   members?: Array<string>; |   members?: Array<string>; | ||||||
|  |  | ||||||
|  | @ -1807,6 +1807,7 @@ export class ConversationModel extends window.Backbone | ||||||
|       groupVersion, |       groupVersion, | ||||||
|       groupId: this.get('groupId'), |       groupId: this.get('groupId'), | ||||||
|       groupLink: this.getGroupLink(), |       groupLink: this.getGroupLink(), | ||||||
|  |       isGroupStorySendReady: Boolean(this.get('isGroupStorySendReady')), | ||||||
|       hideStory: Boolean(this.get('hideStory')), |       hideStory: Boolean(this.get('hideStory')), | ||||||
|       inboxPosition, |       inboxPosition, | ||||||
|       isArchived: this.get('isArchived'), |       isArchived: this.get('isArchived'), | ||||||
|  |  | ||||||
|  | @ -195,6 +195,7 @@ export type ConversationType = { | ||||||
|   groupVersion?: 1 | 2; |   groupVersion?: 1 | 2; | ||||||
|   groupId?: string; |   groupId?: string; | ||||||
|   groupLink?: string; |   groupLink?: string; | ||||||
|  |   isGroupStorySendReady?: boolean; | ||||||
|   messageRequestsEnabled?: boolean; |   messageRequestsEnabled?: boolean; | ||||||
|   acceptedMessageRequest: boolean; |   acceptedMessageRequest: boolean; | ||||||
|   secretParams?: string; |   secretParams?: string; | ||||||
|  | @ -852,6 +853,7 @@ export const actions = { | ||||||
|   showConversation, |   showConversation, | ||||||
|   startComposing, |   startComposing, | ||||||
|   startSettingGroupMetadata, |   startSettingGroupMetadata, | ||||||
|  |   tagGroupsAsNewGroupStory, | ||||||
|   toggleAdmin, |   toggleAdmin, | ||||||
|   toggleConversationInChooseMembers, |   toggleConversationInChooseMembers, | ||||||
|   toggleComposeEditingAvatar, |   toggleComposeEditingAvatar, | ||||||
|  | @ -1953,6 +1955,29 @@ function removeMemberFromGroup( | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | function tagGroupsAsNewGroupStory( | ||||||
|  |   conversationIds: Array<string> | ||||||
|  | ): ThunkAction<void, RootStateType, unknown, NoopActionType> { | ||||||
|  |   return async dispatch => { | ||||||
|  |     await Promise.all( | ||||||
|  |       conversationIds.map(async conversationId => { | ||||||
|  |         const conversation = window.ConversationController.get(conversationId); | ||||||
|  |         if (!conversation) { | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         conversation.set({ isGroupStorySendReady: true }); | ||||||
|  |         await window.Signal.Data.updateConversation(conversation.attributes); | ||||||
|  |       }) | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     dispatch({ | ||||||
|  |       type: 'NOOP', | ||||||
|  |       payload: null, | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| function toggleAdmin( | function toggleAdmin( | ||||||
|   conversationId: string, |   conversationId: string, | ||||||
|   contactId: string |   contactId: string | ||||||
|  |  | ||||||
|  | @ -517,6 +517,20 @@ export const getComposableGroups = createSelector( | ||||||
|     ) |     ) | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
|  | export const getNonGroupStories = createSelector( | ||||||
|  |   getComposableGroups, | ||||||
|  |   (groups: Array<ConversationType>): Array<ConversationType> => | ||||||
|  |     groups.filter(group => !group.isGroupStorySendReady) | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | export const getGroupStories = createSelector( | ||||||
|  |   getConversationLookup, | ||||||
|  |   (conversationLookup: ConversationLookupType): Array<ConversationType> => | ||||||
|  |     Object.values(conversationLookup).filter( | ||||||
|  |       conversation => conversation.isGroupStorySendReady | ||||||
|  |     ) | ||||||
|  | ); | ||||||
|  | 
 | ||||||
| const getNormalizedComposerConversationSearchTerm = createSelector( | const getNormalizedComposerConversationSearchTerm = createSelector( | ||||||
|   getComposerConversationSearchTerm, |   getComposerConversationSearchTerm, | ||||||
|   (searchTerm: string): string => searchTerm.trim() |   (searchTerm: string): string => searchTerm.trim() | ||||||
|  |  | ||||||
|  | @ -7,13 +7,14 @@ import type { StateType } from '../reducer'; | ||||||
| import type { StoryDistributionListDataType } from '../ducks/storyDistributionLists'; | import type { StoryDistributionListDataType } from '../ducks/storyDistributionLists'; | ||||||
| import type { StoryDistributionListWithMembersDataType } from '../../types/Stories'; | import type { StoryDistributionListWithMembersDataType } from '../../types/Stories'; | ||||||
| import { getConversationSelector } from './conversations'; | import { getConversationSelector } from './conversations'; | ||||||
|  | import { MY_STORIES_ID } from '../../types/Stories'; | ||||||
| 
 | 
 | ||||||
| export const getDistributionLists = ( | export const getDistributionLists = ( | ||||||
|   state: StateType |   state: StateType | ||||||
| ): Array<StoryDistributionListDataType> => | ): Array<StoryDistributionListDataType> => | ||||||
|   state.storyDistributionLists.distributionLists.filter( |   state.storyDistributionLists.distributionLists | ||||||
|     list => !list.deletedAtTimestamp |     .filter(list => !list.deletedAtTimestamp) | ||||||
|   ); |     .sort(list => (list.id === MY_STORIES_ID ? -1 : 1)); | ||||||
| 
 | 
 | ||||||
| export const getDistributionListSelector = createSelector( | export const getDistributionListSelector = createSelector( | ||||||
|   getDistributionLists, |   getDistributionLists, | ||||||
|  |  | ||||||
|  | @ -8,7 +8,13 @@ import type { LocalizerType } from '../../types/Util'; | ||||||
| import type { StateType } from '../reducer'; | import type { StateType } from '../reducer'; | ||||||
| import { LinkPreviewSourceType } from '../../types/LinkPreview'; | import { LinkPreviewSourceType } from '../../types/LinkPreview'; | ||||||
| import { StoryCreator } from '../../components/StoryCreator'; | import { StoryCreator } from '../../components/StoryCreator'; | ||||||
| import { getAllSignalConnections, getMe } from '../selectors/conversations'; | import { | ||||||
|  |   getAllSignalConnections, | ||||||
|  |   getCandidateContactsForNewGroup, | ||||||
|  |   getGroupStories, | ||||||
|  |   getMe, | ||||||
|  |   getNonGroupStories, | ||||||
|  | } from '../selectors/conversations'; | ||||||
| import { getDistributionLists } from '../selectors/storyDistributionLists'; | import { getDistributionLists } from '../selectors/storyDistributionLists'; | ||||||
| import { getIntl } from '../selectors/user'; | import { getIntl } from '../selectors/user'; | ||||||
| import { | import { | ||||||
|  | @ -16,9 +22,12 @@ import { | ||||||
|   getRecentStickers, |   getRecentStickers, | ||||||
| } from '../selectors/stickers'; | } from '../selectors/stickers'; | ||||||
| import { getLinkPreview } from '../selectors/linkPreviews'; | import { getLinkPreview } from '../selectors/linkPreviews'; | ||||||
|  | import { getPreferredBadgeSelector } from '../selectors/badges'; | ||||||
| import { processAttachment } from '../../util/processAttachment'; | import { processAttachment } from '../../util/processAttachment'; | ||||||
|  | import { useConversationsActions } from '../ducks/conversations'; | ||||||
| import { useLinkPreviewActions } from '../ducks/linkPreviews'; | import { useLinkPreviewActions } from '../ducks/linkPreviews'; | ||||||
| import { useStoriesActions } from '../ducks/stories'; | import { useStoriesActions } from '../ducks/stories'; | ||||||
|  | import { useStoryDistributionListsActions } from '../ducks/storyDistributionLists'; | ||||||
| 
 | 
 | ||||||
| export type PropsType = { | export type PropsType = { | ||||||
|   file?: File; |   file?: File; | ||||||
|  | @ -31,9 +40,15 @@ export function SmartStoryCreator({ | ||||||
| }: PropsType): JSX.Element | null { | }: PropsType): JSX.Element | null { | ||||||
|   const { debouncedMaybeGrabLinkPreview } = useLinkPreviewActions(); |   const { debouncedMaybeGrabLinkPreview } = useLinkPreviewActions(); | ||||||
|   const { sendStoryMessage } = useStoriesActions(); |   const { sendStoryMessage } = useStoriesActions(); | ||||||
|  |   const { tagGroupsAsNewGroupStory } = useConversationsActions(); | ||||||
|  |   const { createDistributionList } = useStoryDistributionListsActions(); | ||||||
| 
 | 
 | ||||||
|   const i18n = useSelector<StateType, LocalizerType>(getIntl); |   const candidateConversations = useSelector(getCandidateContactsForNewGroup); | ||||||
|   const distributionLists = useSelector(getDistributionLists); |   const distributionLists = useSelector(getDistributionLists); | ||||||
|  |   const getPreferredBadge = useSelector(getPreferredBadgeSelector); | ||||||
|  |   const groupConversations = useSelector(getNonGroupStories); | ||||||
|  |   const groupStories = useSelector(getGroupStories); | ||||||
|  |   const i18n = useSelector<StateType, LocalizerType>(getIntl); | ||||||
|   const installedPacks = useSelector(getInstalledStickerPacks); |   const installedPacks = useSelector(getInstalledStickerPacks); | ||||||
|   const linkPreviewForSource = useSelector(getLinkPreview); |   const linkPreviewForSource = useSelector(getLinkPreview); | ||||||
|   const me = useSelector(getMe); |   const me = useSelector(getMe); | ||||||
|  | @ -42,18 +57,24 @@ export function SmartStoryCreator({ | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <StoryCreator |     <StoryCreator | ||||||
|  |       candidateConversations={candidateConversations} | ||||||
|       debouncedMaybeGrabLinkPreview={debouncedMaybeGrabLinkPreview} |       debouncedMaybeGrabLinkPreview={debouncedMaybeGrabLinkPreview} | ||||||
|       distributionLists={distributionLists} |       distributionLists={distributionLists} | ||||||
|  |       file={file} | ||||||
|  |       getPreferredBadge={getPreferredBadge} | ||||||
|  |       groupConversations={groupConversations} | ||||||
|  |       groupStories={groupStories} | ||||||
|       i18n={i18n} |       i18n={i18n} | ||||||
|       installedPacks={installedPacks} |       installedPacks={installedPacks} | ||||||
|       file={file} |  | ||||||
|       linkPreview={linkPreviewForSource(LinkPreviewSourceType.StoryCreator)} |       linkPreview={linkPreviewForSource(LinkPreviewSourceType.StoryCreator)} | ||||||
|       me={me} |       me={me} | ||||||
|       onClose={onClose} |       onClose={onClose} | ||||||
|  |       onDistributionListCreated={createDistributionList} | ||||||
|       onSend={sendStoryMessage} |       onSend={sendStoryMessage} | ||||||
|       processAttachment={processAttachment} |       processAttachment={processAttachment} | ||||||
|       recentStickers={recentStickers} |       recentStickers={recentStickers} | ||||||
|       signalConnections={signalConnections} |       signalConnections={signalConnections} | ||||||
|  |       tagGroupsAsNewGroupStory={tagGroupsAsNewGroupStory} | ||||||
|     /> |     /> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -42,6 +42,39 @@ export function getDefaultConversation( | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export function getDefaultGroup( | ||||||
|  |   overrideProps: Partial<ConversationType> = {} | ||||||
|  | ): ConversationType { | ||||||
|  |   const memberships = Array.from(Array(casual.integer(1, 20)), () => ({ | ||||||
|  |     uuid: UUID.generate().toString(), | ||||||
|  |     isAdmin: Boolean(casual.coin_flip), | ||||||
|  |   })); | ||||||
|  | 
 | ||||||
|  |   return { | ||||||
|  |     acceptedMessageRequest: true, | ||||||
|  |     announcementsOnly: false, | ||||||
|  |     avatarPath: getAvatarPath(), | ||||||
|  |     badges: [], | ||||||
|  |     color: getRandomColor(), | ||||||
|  |     conversationColor: ConversationColors[0], | ||||||
|  |     groupDescription: casual.sentence, | ||||||
|  |     groupId: UUID.generate().toString(), | ||||||
|  |     groupLink: casual.url, | ||||||
|  |     groupVersion: 2, | ||||||
|  |     id: UUID.generate().toString(), | ||||||
|  |     isMe: false, | ||||||
|  |     lastUpdated: casual.unix_time, | ||||||
|  |     markedUnread: Boolean(overrideProps.markedUnread), | ||||||
|  |     membersCount: memberships.length, | ||||||
|  |     memberships, | ||||||
|  |     sharedGroupNames: [], | ||||||
|  |     title: casual.title, | ||||||
|  |     type: 'group' as const, | ||||||
|  |     uuid: UUID.generate().toString(), | ||||||
|  |     ...overrideProps, | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export function getDefaultConversationWithUuid( | export function getDefaultConversationWithUuid( | ||||||
|   overrideProps: Partial<ConversationType> = {}, |   overrideProps: Partial<ConversationType> = {}, | ||||||
|   uuid: UUIDStringType = UUID.generate().toString() |   uuid: UUIDStringType = UUID.generate().toString() | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Josh Perez
				Josh Perez