// Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { MeasuredComponentProps } from 'react-measure'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import Measure from 'react-measure'; import { noop } from 'lodash'; import type { ConversationType } from '../state/ducks/conversations'; import type { LocalizerType } from '../types/Util'; import type { PreferredBadgeSelectorType } from '../state/selectors/badges'; import type { Row } from './ConversationList'; import type { StoryDistributionListWithMembersDataType } from '../types/Stories'; import type { UUIDStringType } from '../types/UUID'; import { Avatar, AvatarSize } from './Avatar'; import { Button, ButtonVariant } from './Button'; import { Checkbox } from './Checkbox'; import { ConfirmationDialog } from './ConfirmationDialog'; import { ConversationList, RowType } from './ConversationList'; import { Input } from './Input'; import { Intl } from './Intl'; import { MY_STORIES_ID, getStoryDistributionListName } from '../types/Stories'; import { Modal } from './Modal'; import { SearchInput } from './SearchInput'; import { StoryDistributionListName } from './StoryDistributionListName'; import { Theme } from '../util/theme'; import { ThemeType } from '../types/Util'; import { UUID } from '../types/UUID'; import { filterAndSortConversationsByRecent } from '../util/filterAndSortConversations'; import { isNotNil } from '../util/isNotNil'; import { shouldNeverBeCalled, asyncShouldNeverBeCalled, } from '../util/shouldNeverBeCalled'; export type PropsType = { candidateConversations: Array; distributionLists: Array; getPreferredBadge: PreferredBadgeSelectorType; hideStoriesSettings: () => unknown; i18n: LocalizerType; me: ConversationType; onDeleteList: (listId: string) => unknown; onDistributionListCreated: ( name: string, viewerUuids: Array ) => unknown; onHideMyStoriesFrom: (viewerUuids: Array) => unknown; onRemoveMember: (listId: string, uuid: UUIDStringType | undefined) => unknown; onRepliesNReactionsChanged: ( listId: string, allowsReplies: boolean ) => unknown; onViewersUpdated: ( listId: string, viewerUuids: Array ) => unknown; setMyStoriesToAllSignalConnections: () => unknown; toggleSignalConnectionsModal: () => unknown; }; export enum Page { DistributionLists = 'DistributionLists', AddViewer = 'AddViewer', ChooseViewers = 'ChooseViewers', NameStory = 'NameStory', HideStoryFrom = 'HideStoryFrom', } function filterConversations( conversations: ReadonlyArray, searchTerm: string ) { return filterAndSortConversationsByRecent( conversations, searchTerm, undefined ).filter(conversation => conversation.uuid); } export const StoriesSettingsModal = ({ candidateConversations, distributionLists, getPreferredBadge, hideStoriesSettings, i18n, me, onDeleteList, onDistributionListCreated, onHideMyStoriesFrom, onRemoveMember, onRepliesNReactionsChanged, onViewersUpdated, setMyStoriesToAllSignalConnections, toggleSignalConnectionsModal, }: PropsType): JSX.Element => { const [listToEditId, setListToEditId] = useState( undefined ); const listToEdit = useMemo( () => distributionLists.find(x => x.id === listToEditId), [distributionLists, listToEditId] ); const [page, setPage] = useState(Page.DistributionLists); const [selectedContacts, setSelectedContacts] = useState< Array >([]); const resetChooseViewersScreen = useCallback(() => { setSelectedContacts([]); setPage(Page.DistributionLists); }, []); const [confirmDeleteListId, setConfirmDeleteListId] = useState< string | undefined >(); const [confirmRemoveMember, setConfirmRemoveMember] = useState< | undefined | { listId: string; title: string; uuid: UUIDStringType | undefined; } >(); let content: JSX.Element | null; if (page !== Page.DistributionLists) { content = ( { onDistributionListCreated(name, uuids); resetChooseViewersScreen(); }} onViewersUpdated={uuids => { if (listToEditId && page === Page.AddViewer) { onViewersUpdated(listToEditId, uuids); resetChooseViewersScreen(); } if (page === Page.ChooseViewers) { setPage(Page.NameStory); } if (page === Page.HideStoryFrom) { onHideMyStoriesFrom(uuids); resetChooseViewersScreen(); } }} page={page} selectedContacts={selectedContacts} setSelectedContacts={setSelectedContacts} /> ); } else if (listToEdit) { const isMyStories = listToEdit.id === MY_STORIES_ID; content = ( <> {!isMyStories && ( <>

)}
{i18n('StoriesSettings__who-can-see')}
{isMyStories && ( <> { setMyStoriesToAllSignalConnections(); }} /> 0} description={i18n('StoriesSettings__mine__exclude--description', [ listToEdit.isBlockList ? String(listToEdit.members.length) : '0', ])} isRadio label={i18n('StoriesSettings__mine__exclude--label')} moduleClassName="StoriesSettingsModal__checkbox" name="share" onChange={noop} onClick={() => { if (listToEdit.isBlockList) { setSelectedContacts(listToEdit.members); } setPage(Page.HideStoryFrom); }} /> 0} description={ !listToEdit.isBlockList && listToEdit.members.length ? i18n('StoriesSettings__mine__only--description--people', [ String(listToEdit.members.length), ]) : i18n('StoriesSettings__mine__only--description') } isRadio label={i18n('StoriesSettings__mine__only--label')} moduleClassName="StoriesSettingsModal__checkbox" name="share" onChange={noop} onClick={() => { if (!listToEdit.isBlockList) { setSelectedContacts(listToEdit.members); } setPage(Page.AddViewer); }} />
{i18n('StoriesSettings__mine__disclaimer--learn-more')} ), }} i18n={i18n} id="StoriesSettings__mine__disclaimer" />
)} {!isMyStories && ( <> {listToEdit.members.map(member => (
{member.title}
))} )}
{i18n('StoriesSettings__replies-reactions--title')}
onRepliesNReactionsChanged(listToEdit.id, value)} /> {!isMyStories && ( <>
)} ); } else { const privateStories = distributionLists.filter( list => list.id !== MY_STORIES_ID ); content = ( <>
{privateStories.map(list => ( ))} ); } const isChoosingViewers = page === Page.ChooseViewers || page === Page.AddViewer; let modalTitle: string = i18n('StoriesSettings__title'); if (page === Page.HideStoryFrom) { modalTitle = i18n('StoriesSettings__hide-story'); } else if (page === Page.NameStory) { modalTitle = i18n('StoriesSettings__name-story'); } else if (isChoosingViewers) { modalTitle = i18n('StoriesSettings__choose-viewers'); } else if (listToEdit) { modalTitle = getStoryDistributionListName( i18n, listToEdit.id, listToEdit.name ); } const hasBackButton = page !== Page.DistributionLists || listToEdit; const hasStickyButtons = isChoosingViewers || page === Page.NameStory || page === Page.HideStoryFrom; return ( <> { if (page === Page.HideStoryFrom) { resetChooseViewersScreen(); } else if (page === Page.NameStory) { setPage(Page.ChooseViewers); } else if (isChoosingViewers) { resetChooseViewersScreen(); } else if (listToEdit) { setListToEditId(undefined); } } : undefined } onClose={hideStoriesSettings} theme={Theme.Dark} title={modalTitle} > {content} {confirmDeleteListId && ( { onDeleteList(confirmDeleteListId); setListToEditId(undefined); }, style: 'negative', text: i18n('delete'), }, ]} i18n={i18n} onClose={() => { setConfirmDeleteListId(undefined); }} > {i18n('StoriesSettings__delete-list--confirm')} )} {confirmRemoveMember && ( onRemoveMember( confirmRemoveMember.listId, confirmRemoveMember.uuid ), style: 'negative', text: i18n('StoriesSettings__remove--action'), }, ]} i18n={i18n} onClose={() => { setConfirmRemoveMember(undefined); }} title={i18n('StoriesSettings__remove--title', [ confirmRemoveMember.title, ])} > {i18n('StoriesSettings__remove--body')} )} ); }; type EditDistributionListPropsType = { onDone: (name: string, viewerUuids: Array) => unknown; onViewersUpdated: (viewerUuids: Array) => unknown; page: Page; selectedContacts: Array; setSelectedContacts: (contacts: Array) => unknown; } & Pick; 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 = 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 ( <>
{i18n('StoriesSettings__who-can-see')}
{selectedContacts.map(contact => (
{contact.title}
))} ); } 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 ( <> { setSearchTerm(event.target.value); }} value={searchTerm} /> {selectedContacts.length ? (
{selectedContacts.map(contact => (
{contact.firstName || contact.profileName || contact.phoneNumber}
))}
) : undefined} {candidateConversations.length ? ( {({ contentRect, measureRef }: MeasuredComponentProps) => (
{ toggleSelectedConversation(conversationId); }} lookupConversationWithoutUuid={asyncShouldNeverBeCalled} showConversation={shouldNeverBeCalled} showUserNotFoundModal={shouldNeverBeCalled} setIsFetchingUUID={shouldNeverBeCalled} onSelectConversation={shouldNeverBeCalled} renderMessageSearchResult={() => { shouldNeverBeCalled(); return
; }} rowCount={rowCount} shouldRecomputeRowHeights={false} showChooseGroupMembers={shouldNeverBeCalled} theme={ThemeType.dark} />
)} ) : (
{i18n('noContactsFound')}
)} {isChoosingViewers && ( )} {page === Page.HideStoryFrom && ( )} ); } return null; };