Allow adding to a group by phone number

This commit is contained in:
Fedor Indutny 2022-04-04 17:38:22 -07:00 committed by GitHub
parent 76a1a805ef
commit 9568d5792e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 1842 additions and 693 deletions

View file

@ -2,7 +2,14 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { FunctionComponent } from 'react';
import React, { useEffect, useMemo, useState, useRef } 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';
@ -11,9 +18,16 @@ 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 { filterAndSortConversationsByTitle } 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';
@ -23,24 +37,37 @@ import { ContactCheckboxDisabledReason } from '../../../conversationList/Contact
import { Button, ButtonVariant } from '../../../Button';
import { SearchInput } from '../../../SearchInput';
type PropsType = {
export type StatePropsType = {
regionCode: string | undefined;
candidateContacts: ReadonlyArray<ConversationType>;
confirmAdds: () => void;
conversationIdsAlreadyInGroup: Set<string>;
getPreferredBadge: PreferredBadgeSelectorType;
i18n: LocalizerType;
theme: ThemeType;
maxGroupSize: number;
onClose: () => void;
removeSelectedContact: (_: string) => void;
searchTerm: string;
selectedContacts: ReadonlyArray<ConversationType>;
confirmAdds: () => void;
onClose: () => void;
removeSelectedContact: (_: string) => void;
setSearchTerm: (_: string) => void;
theme: ThemeType;
toggleSelectedContact: (conversationId: string) => void;
};
} & Pick<
LookupConversationWithoutUuidActionsType,
'lookupConversationWithoutUuid'
>;
type ActionPropsType = Omit<
LookupConversationWithoutUuidActionsType,
'setIsFetchingUUID' | 'lookupConversationWithoutUuid'
>;
type PropsType = StatePropsType & ActionPropsType;
// TODO: This should use <Modal>. See DESKTOP-1038.
export const ChooseGroupMembersModal: FunctionComponent<PropsType> = ({
regionCode,
candidateContacts,
confirmAdds,
conversationIdsAlreadyInGroup,
@ -54,9 +81,24 @@ export const ChooseGroupMembersModal: FunctionComponent<PropsType> = ({
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 | HTMLInputElement>(null);
const numberOfContactsAlreadyInGroup = conversationIdsAlreadyInGroup.size;
@ -72,7 +114,7 @@ export const ChooseGroupMembersModal: FunctionComponent<PropsType> = ({
const canContinue = Boolean(selectedContacts.length);
const [filteredContacts, setFilteredContacts] = useState(
filterAndSortConversationsByTitle(candidateContacts, '')
filterAndSortConversationsByTitle(candidateContacts, '', regionCode)
);
const normalizedSearchTerm = searchTerm.trim();
useEffect(() => {
@ -80,38 +122,106 @@ export const ChooseGroupMembersModal: FunctionComponent<PropsType> = ({
setFilteredContacts(
filterAndSortConversationsByTitle(
candidateContacts,
normalizedSearchTerm
normalizedSearchTerm,
regionCode
)
);
}, 200);
return () => {
clearTimeout(timeout);
};
}, [candidateContacts, normalizedSearchTerm, setFilteredContacts]);
}, [
candidateContacts,
normalizedSearchTerm,
setFilteredContacts,
regionCode,
]);
const rowCount = filteredContacts.length;
const [uuidFetchState, setUuidFetchState] = useState<UUIDFetchStateType>({});
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 => {
const contact = filteredContacts[index];
if (!contact) {
return undefined;
let virtualIndex = index;
if (isPhoneNumberVisible && filteredContacts.length) {
if (virtualIndex === 0) {
return {
type: RowType.Header,
i18nKey: 'contactsHeader',
};
}
virtualIndex -= 1;
}
const isSelected = selectedConversationIdsSet.has(contact.id);
const isAlreadyInGroup = conversationIdsAlreadyInGroup.has(contact.id);
if (virtualIndex < filteredContacts.length) {
const contact = filteredContacts[virtualIndex];
let disabledReason: undefined | ContactCheckboxDisabledReason;
if (isAlreadyInGroup) {
disabledReason = ContactCheckboxDisabledReason.AlreadyAdded;
} else if (hasSelectedMaximumNumberOfContacts && !isSelected) {
disabledReason = ContactCheckboxDisabledReason.MaximumContactsSelected;
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,
};
}
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 (
@ -207,6 +317,12 @@ export const ChooseGroupMembersModal: FunctionComponent<PropsType> = ({
throw missingCaseError(disabledReason);
}
}}
lookupConversationWithoutUuid={
lookupConversationWithoutUuid
}
showUserNotFoundModal={showUserNotFoundModal}
setIsFetchingUUID={setIsFetchingUUID}
showConversation={shouldNeverBeCalled}
onSelectConversation={shouldNeverBeCalled}
renderMessageSearchResult={() => {
shouldNeverBeCalled();
@ -215,8 +331,6 @@ export const ChooseGroupMembersModal: FunctionComponent<PropsType> = ({
rowCount={rowCount}
shouldRecomputeRowHeights={false}
showChooseGroupMembers={shouldNeverBeCalled}
startNewConversationFromPhoneNumber={shouldNeverBeCalled}
startNewConversationFromUsername={shouldNeverBeCalled}
theme={theme}
/>
</div>

View file

@ -15,7 +15,7 @@ import { Intl } from '../../../Intl';
import { Emojify } from '../../Emojify';
import { ContactName } from '../../ContactName';
type PropsType = {
export type StatePropsType = {
groupTitle: string;
i18n: LocalizerType;
makeRequest: () => void;
@ -24,6 +24,8 @@ type PropsType = {
selectedContacts: ReadonlyArray<ConversationType>;
};
type PropsType = StatePropsType;
export const ConfirmAdditionsModal: FunctionComponent<PropsType> = ({
groupTitle,
i18n,