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

@ -18,46 +18,78 @@ import {
} from '../AddGroupMemberErrorDialog';
import { Button } from '../Button';
import type { LocalizerType } from '../../types/Util';
import type { ParsedE164Type } from '../../util/libphonenumberInstance';
import { parseAndFormatPhoneNumber } from '../../util/libphonenumberInstance';
import type { UUIDFetchStateType } from '../../util/uuidFetchState';
import { isFetchingByE164 } from '../../util/uuidFetchState';
import {
getGroupSizeRecommendedLimit,
getGroupSizeHardLimit,
} from '../../groups/limits';
export type LeftPaneChooseGroupMembersPropsType = {
uuidFetchState: UUIDFetchStateType;
candidateContacts: ReadonlyArray<ConversationType>;
isShowingRecommendedGroupSizeModal: boolean;
isShowingMaximumGroupSizeModal: boolean;
searchTerm: string;
regionCode: string | undefined;
selectedContacts: Array<ConversationType>;
};
export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneChooseGroupMembersPropsType> {
private readonly candidateContacts: ReadonlyArray<ConversationType>;
private readonly isPhoneNumberChecked: boolean;
private readonly isShowingMaximumGroupSizeModal: boolean;
private readonly isShowingRecommendedGroupSizeModal: boolean;
private readonly searchTerm: string;
private readonly phoneNumber: ParsedE164Type | undefined;
private readonly selectedContacts: Array<ConversationType>;
private readonly selectedConversationIdsSet: Set<string>;
private readonly uuidFetchState: UUIDFetchStateType;
constructor({
candidateContacts,
isShowingMaximumGroupSizeModal,
isShowingRecommendedGroupSizeModal,
searchTerm,
regionCode,
selectedContacts,
uuidFetchState,
}: Readonly<LeftPaneChooseGroupMembersPropsType>) {
super();
this.uuidFetchState = uuidFetchState;
this.candidateContacts = candidateContacts;
this.isShowingMaximumGroupSizeModal = isShowingMaximumGroupSizeModal;
this.isShowingRecommendedGroupSizeModal =
isShowingRecommendedGroupSizeModal;
this.searchTerm = searchTerm;
const phoneNumber = parseAndFormatPhoneNumber(searchTerm, regionCode);
if (phoneNumber) {
this.isPhoneNumberChecked =
phoneNumber.isValid &&
selectedContacts.some(contact => contact.e164 === phoneNumber.e164);
const isVisible = this.candidateContacts.every(
contact => contact.e164 !== phoneNumber.e164
);
if (isVisible) {
this.phoneNumber = phoneNumber;
}
} else {
this.isPhoneNumberChecked = false;
}
this.selectedContacts = selectedContacts;
this.selectedConversationIdsSet = new Set(
@ -207,46 +239,90 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
}
getRowCount(): number {
if (!this.candidateContacts.length) {
return 0;
let rowCount = 0;
// Header + Phone Number
if (this.phoneNumber) {
rowCount += 2;
}
return this.candidateContacts.length + 2;
// Header + Contacts
if (this.candidateContacts.length) {
rowCount += 1 + this.candidateContacts.length;
}
// Footer
if (rowCount > 0) {
rowCount += 1;
}
return rowCount;
}
getRow(rowIndex: number): undefined | Row {
if (!this.candidateContacts.length) {
getRow(actualRowIndex: number): undefined | Row {
if (!this.candidateContacts.length && !this.phoneNumber) {
return undefined;
}
if (rowIndex === 0) {
return {
type: RowType.Header,
i18nKey: 'contactsHeader',
};
}
const rowCount = this.getRowCount();
// This puts a blank row for the footer.
if (rowIndex === this.candidateContacts.length + 1) {
if (actualRowIndex === rowCount - 1) {
return { type: RowType.Blank };
}
const contact = this.candidateContacts[rowIndex - 1];
if (!contact) {
return undefined;
let virtualRowIndex = actualRowIndex;
if (this.candidateContacts.length) {
if (virtualRowIndex === 0) {
return {
type: RowType.Header,
i18nKey: 'contactsHeader',
};
}
if (virtualRowIndex <= this.candidateContacts.length) {
const contact = this.candidateContacts[virtualRowIndex - 1];
const isChecked = this.selectedConversationIdsSet.has(contact.id);
const disabledReason =
!isChecked && this.hasSelectedMaximumNumberOfContacts()
? ContactCheckboxDisabledReason.MaximumContactsSelected
: undefined;
return {
type: RowType.ContactCheckbox,
contact,
isChecked,
disabledReason,
};
}
virtualRowIndex -= 1 + this.candidateContacts.length;
}
const isChecked = this.selectedConversationIdsSet.has(contact.id);
const disabledReason =
!isChecked && this.hasSelectedMaximumNumberOfContacts()
? ContactCheckboxDisabledReason.MaximumContactsSelected
: undefined;
if (this.phoneNumber) {
if (virtualRowIndex === 0) {
return {
type: RowType.Header,
i18nKey: 'findByPhoneNumberHeader',
};
}
if (virtualRowIndex === 1) {
return {
type: RowType.PhoneNumberCheckbox,
isChecked: this.isPhoneNumberChecked,
isFetching: isFetchingByE164(
this.uuidFetchState,
this.phoneNumber.e164
),
phoneNumber: this.phoneNumber,
};
}
virtualRowIndex -= 2;
}
return {
type: RowType.ContactCheckbox,
contact,
isChecked,
disabledReason,
};
return undefined;
}
// This is deliberately unimplemented because these keyboard shortcuts shouldn't work in

View file

@ -3,7 +3,6 @@
import type { ReactChild, ChangeEvent } from 'react';
import React from 'react';
import type { PhoneNumber } from 'google-libphonenumber';
import { LeftPaneHelper } from './LeftPaneHelper';
import type { Row } from '../ConversationList';
@ -12,13 +11,15 @@ import type { ContactListItemConversationType } from '../conversationList/Contac
import type { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem';
import { SearchInput } from '../SearchInput';
import type { LocalizerType } from '../../types/Util';
import {
instance as phoneNumberInstance,
PhoneNumberFormat,
} from '../../util/libphonenumberInstance';
import { assert } from '../../util/assert';
import type { ParsedE164Type } from '../../util/libphonenumberInstance';
import { parseAndFormatPhoneNumber } from '../../util/libphonenumberInstance';
import { missingCaseError } from '../../util/missingCaseError';
import { getUsernameFromSearch } from '../../types/Username';
import type { UUIDFetchStateType } from '../../util/uuidFetchState';
import {
isFetchingByUsername,
isFetchingByE164,
} from '../../util/uuidFetchState';
export type LeftPaneComposePropsType = {
composeContacts: ReadonlyArray<ContactListItemConversationType>;
@ -26,14 +27,13 @@ export type LeftPaneComposePropsType = {
regionCode: string | undefined;
searchTerm: string;
isFetchingUsername: boolean;
uuidFetchState: UUIDFetchStateType;
isUsernamesEnabled: boolean;
};
enum TopButton {
None,
CreateNewGroup,
StartNewConversation,
}
export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsType> {
@ -41,13 +41,15 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
private readonly composeGroups: ReadonlyArray<ConversationListItemPropsType>;
private readonly isFetchingUsername: boolean;
private readonly uuidFetchState: UUIDFetchStateType;
private readonly isUsernamesEnabled: boolean;
private readonly searchTerm: string;
private readonly phoneNumber: undefined | PhoneNumber;
private readonly phoneNumber: ParsedE164Type | undefined;
private readonly isPhoneNumberVisible: boolean;
constructor({
composeContacts,
@ -55,15 +57,23 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
regionCode,
searchTerm,
isUsernamesEnabled,
isFetchingUsername,
uuidFetchState,
}: Readonly<LeftPaneComposePropsType>) {
super();
this.composeContacts = composeContacts;
this.composeGroups = composeGroups;
this.searchTerm = searchTerm;
this.phoneNumber = parsePhoneNumber(searchTerm, regionCode);
this.isFetchingUsername = isFetchingUsername;
this.phoneNumber = parseAndFormatPhoneNumber(searchTerm, regionCode);
if (this.phoneNumber) {
const { phoneNumber } = this;
this.isPhoneNumberVisible = this.composeContacts.every(
contact => contact.e164 !== phoneNumber.e164
);
} else {
this.isPhoneNumberVisible = false;
}
this.uuidFetchState = uuidFetchState;
this.isUsernamesEnabled = isUsernamesEnabled;
}
@ -141,6 +151,9 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
if (this.getUsernameFromSearch()) {
result += 2;
}
if (this.isPhoneNumberVisible) {
result += 2;
}
return result;
}
@ -153,18 +166,6 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
switch (topButton) {
case TopButton.None:
break;
case TopButton.StartNewConversation:
assert(
this.phoneNumber,
'LeftPaneComposeHelper: we should have a phone number if the top button is "Start new conversation"'
);
return {
type: RowType.StartNewConversation,
phoneNumber: phoneNumberInstance.format(
this.phoneNumber,
PhoneNumberFormat.E164
),
};
case TopButton.CreateNewGroup:
return { type: RowType.CreateNewGroup };
default:
@ -232,7 +233,34 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
return {
type: RowType.UsernameSearchResult,
username,
isFetchingUsername: this.isFetchingUsername,
isFetchingUsername: isFetchingByUsername(
this.uuidFetchState,
username
),
};
virtualRowIndex -= 1;
}
}
if (this.phoneNumber && this.isPhoneNumberVisible) {
if (virtualRowIndex === 0) {
return {
type: RowType.Header,
i18nKey: 'findByPhoneNumberHeader',
};
}
virtualRowIndex -= 1;
if (virtualRowIndex === 0) {
return {
type: RowType.StartNewConversation,
phoneNumber: this.phoneNumber,
isFetching: isFetchingByE164(
this.uuidFetchState,
this.phoneNumber.e164
),
};
virtualRowIndex -= 1;
@ -272,9 +300,6 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
}
private getTopButton(): TopButton {
if (this.phoneNumber) {
return TopButton.StartNewConversation;
}
if (this.searchTerm) {
return TopButton.None;
}
@ -352,21 +377,3 @@ function focusRef(el: HTMLElement | null) {
el.focus();
}
}
function parsePhoneNumber(
str: string,
regionCode: string | undefined
): undefined | PhoneNumber {
let result: PhoneNumber;
try {
result = phoneNumberInstance.parse(str, regionCode);
} catch (err) {
return undefined;
}
if (!phoneNumberInstance.isValidNumber(result)) {
return undefined;
}
return result;
}