// Copyright 2021-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { FunctionComponent } from 'react'; import React, { useEffect, useMemo, useState, useRef, useCallback, } from 'react'; import { omit } from 'lodash'; import type { MeasuredComponentProps } from 'react-measure'; import Measure from 'react-measure'; import type { LocalizerType, ThemeType } from '../../../../types/Util'; import { assert } from '../../../../util/assert'; import { refMerger } from '../../../../util/refMerger'; import { useRestoreFocus } from '../../../../hooks/useRestoreFocus'; import { missingCaseError } from '../../../../util/missingCaseError'; import type { LookupConversationWithoutUuidActionsType } from '../../../../util/lookupConversationWithoutUuid'; import { parseAndFormatPhoneNumber } from '../../../../util/libphonenumberInstance'; import { filterAndSortConversationsByRecent } from '../../../../util/filterAndSortConversations'; import type { ConversationType } from '../../../../state/ducks/conversations'; import type { PreferredBadgeSelectorType } from '../../../../state/selectors/badges'; import type { UUIDFetchStateKeyType, UUIDFetchStateType, } from '../../../../util/uuidFetchState'; import { isFetchingByE164 } from '../../../../util/uuidFetchState'; import { ModalHost } from '../../../ModalHost'; import { ContactPills } from '../../../ContactPills'; import { ContactPill } from '../../../ContactPill'; import type { Row } from '../../../ConversationList'; import { ConversationList, RowType } from '../../../ConversationList'; import { ContactCheckboxDisabledReason } from '../../../conversationList/ContactCheckbox'; import { Button, ButtonVariant } from '../../../Button'; import { SearchInput } from '../../../SearchInput'; export type StatePropsType = { regionCode: string | undefined; candidateContacts: ReadonlyArray; conversationIdsAlreadyInGroup: Set; getPreferredBadge: PreferredBadgeSelectorType; i18n: LocalizerType; theme: ThemeType; maxGroupSize: number; searchTerm: string; selectedContacts: ReadonlyArray; confirmAdds: () => void; onClose: () => void; removeSelectedContact: (_: string) => void; setSearchTerm: (_: string) => void; toggleSelectedContact: (conversationId: string) => void; } & Pick< LookupConversationWithoutUuidActionsType, 'lookupConversationWithoutUuid' >; type ActionPropsType = Omit< LookupConversationWithoutUuidActionsType, 'setIsFetchingUUID' | 'lookupConversationWithoutUuid' >; type PropsType = StatePropsType & ActionPropsType; // TODO: This should use . See DESKTOP-1038. export const ChooseGroupMembersModal: FunctionComponent = ({ regionCode, candidateContacts, confirmAdds, conversationIdsAlreadyInGroup, getPreferredBadge, i18n, maxGroupSize, onClose, removeSelectedContact, searchTerm, selectedContacts, setSearchTerm, theme, toggleSelectedContact, lookupConversationWithoutUuid, showUserNotFoundModal, }) => { const [focusRef] = useRestoreFocus(); const phoneNumber = parseAndFormatPhoneNumber(searchTerm, regionCode); let isPhoneNumberChecked = false; if (phoneNumber) { isPhoneNumberChecked = phoneNumber.isValid && selectedContacts.some(contact => contact.e164 === phoneNumber.e164); } const isPhoneNumberVisible = phoneNumber && candidateContacts.every(contact => contact.e164 !== phoneNumber.e164); const inputRef = useRef(null); const numberOfContactsAlreadyInGroup = conversationIdsAlreadyInGroup.size; const hasSelectedMaximumNumberOfContacts = selectedContacts.length + numberOfContactsAlreadyInGroup >= maxGroupSize; const selectedConversationIdsSet: Set = useMemo( () => new Set(selectedContacts.map(contact => contact.id)), [selectedContacts] ); const canContinue = Boolean(selectedContacts.length); const [filteredContacts, setFilteredContacts] = useState( filterAndSortConversationsByRecent(candidateContacts, '', regionCode) ); const normalizedSearchTerm = searchTerm.trim(); useEffect(() => { const timeout = setTimeout(() => { setFilteredContacts( filterAndSortConversationsByRecent( candidateContacts, normalizedSearchTerm, regionCode ) ); }, 200); return () => { clearTimeout(timeout); }; }, [ candidateContacts, normalizedSearchTerm, setFilteredContacts, regionCode, ]); const [uuidFetchState, setUuidFetchState] = useState({}); const setIsFetchingUUID = useCallback( (identifier: UUIDFetchStateKeyType, isFetching: boolean) => { setUuidFetchState(prevState => { return isFetching ? { ...prevState, [identifier]: isFetching, } : omit(prevState, identifier); }); }, [setUuidFetchState] ); let rowCount = 0; if (filteredContacts.length) { rowCount += filteredContacts.length; } if (isPhoneNumberVisible) { // "Contacts" header if (filteredContacts.length) { rowCount += 1; } // "Find by phone number" + phone number rowCount += 2; } const getRow = (index: number): undefined | Row => { let virtualIndex = index; if (isPhoneNumberVisible && filteredContacts.length) { if (virtualIndex === 0) { return { type: RowType.Header, i18nKey: 'contactsHeader', }; } virtualIndex -= 1; } if (virtualIndex < filteredContacts.length) { const contact = filteredContacts[virtualIndex]; const isSelected = selectedConversationIdsSet.has(contact.id); const isAlreadyInGroup = conversationIdsAlreadyInGroup.has(contact.id); let disabledReason: undefined | ContactCheckboxDisabledReason; if (isAlreadyInGroup) { disabledReason = ContactCheckboxDisabledReason.AlreadyAdded; } else if (hasSelectedMaximumNumberOfContacts && !isSelected) { disabledReason = ContactCheckboxDisabledReason.MaximumContactsSelected; } return { type: RowType.ContactCheckbox, contact, isChecked: isSelected || isAlreadyInGroup, disabledReason, }; } virtualIndex -= filteredContacts.length; if (isPhoneNumberVisible) { if (virtualIndex === 0) { return { type: RowType.Header, i18nKey: 'findByPhoneNumberHeader', }; } if (virtualIndex === 1) { return { type: RowType.PhoneNumberCheckbox, isChecked: isPhoneNumberChecked, isFetching: isFetchingByE164(uuidFetchState, phoneNumber.e164), phoneNumber, }; } virtualIndex -= 2; } return undefined; }; return (
); }; function shouldNeverBeCalled(..._args: ReadonlyArray): unknown { assert(false, 'This should never be called. Doing nothing'); }