Allow adding to a group by phone number
This commit is contained in:
parent
76a1a805ef
commit
9568d5792e
49 changed files with 1842 additions and 693 deletions
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue