signal-desktop/ts/components/leftPane/LeftPaneChooseGroupMembersHelper.tsx

415 lines
12 KiB
TypeScript
Raw Normal View History

2023-01-03 11:55:46 -08:00
// Copyright 2021 Signal Messenger, LLC
2021-03-03 14:09:58 -06:00
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReactChild, ChangeEvent } from 'react';
import React from 'react';
2021-03-03 14:09:58 -06:00
import { LeftPaneHelper } from './LeftPaneHelper';
import type { Row } from '../ConversationList';
import { RowType } from '../ConversationList';
import type { ConversationType } from '../../state/ducks/conversations';
2021-03-03 14:09:58 -06:00
import { ContactCheckboxDisabledReason } from '../conversationList/ContactCheckbox';
import { ContactPills } from '../ContactPills';
import { ContactPill } from '../ContactPill';
2021-05-10 20:50:43 -04:00
import { SearchInput } from '../SearchInput';
2021-03-11 15:29:31 -06:00
import {
AddGroupMemberErrorDialog,
AddGroupMemberErrorDialogMode,
} from '../AddGroupMemberErrorDialog';
2021-03-03 14:09:58 -06:00
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';
2022-06-16 17:38:28 -07:00
import {
isFetchingByUsername,
isFetchingByE164,
} from '../../util/uuidFetchState';
2021-03-03 14:09:58 -06:00
export type LeftPaneChooseGroupMembersPropsType = {
uuidFetchState: UUIDFetchStateType;
2021-03-03 14:09:58 -06:00
candidateContacts: ReadonlyArray<ConversationType>;
groupSizeRecommendedLimit: number;
groupSizeHardLimit: number;
2021-03-03 14:09:58 -06:00
isShowingRecommendedGroupSizeModal: boolean;
isShowingMaximumGroupSizeModal: boolean;
ourE164: string | undefined;
ourUsername: string | undefined;
2021-03-03 14:09:58 -06:00
searchTerm: string;
regionCode: string | undefined;
2024-02-14 10:18:49 -08:00
username: string | undefined;
2021-03-03 14:09:58 -06:00
selectedContacts: Array<ConversationType>;
};
2021-04-26 11:38:50 -05:00
export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneChooseGroupMembersPropsType> {
readonly #candidateContacts: ReadonlyArray<ConversationType>;
readonly #isPhoneNumberChecked: boolean;
readonly #isUsernameChecked: boolean;
readonly #isShowingMaximumGroupSizeModal: boolean;
readonly #isShowingRecommendedGroupSizeModal: boolean;
readonly #groupSizeRecommendedLimit: number;
readonly #groupSizeHardLimit: number;
readonly #searchTerm: string;
readonly #phoneNumber: ParsedE164Type | undefined;
readonly #username: string | undefined;
readonly #selectedContacts: Array<ConversationType>;
readonly #selectedConversationIdsSet: Set<string>;
readonly #uuidFetchState: UUIDFetchStateType;
2021-03-03 14:09:58 -06:00
constructor({
candidateContacts,
isShowingMaximumGroupSizeModal,
isShowingRecommendedGroupSizeModal,
groupSizeRecommendedLimit,
groupSizeHardLimit,
ourE164,
ourUsername,
2021-03-03 14:09:58 -06:00
searchTerm,
regionCode,
2021-03-03 14:09:58 -06:00
selectedContacts,
uuidFetchState,
2024-02-14 10:18:49 -08:00
username,
2021-03-03 14:09:58 -06:00
}: Readonly<LeftPaneChooseGroupMembersPropsType>) {
super();
this.#uuidFetchState = uuidFetchState;
this.#groupSizeRecommendedLimit = groupSizeRecommendedLimit - 1;
this.#groupSizeHardLimit = groupSizeHardLimit - 1;
this.#candidateContacts = candidateContacts;
this.#isShowingMaximumGroupSizeModal = isShowingMaximumGroupSizeModal;
this.#isShowingRecommendedGroupSizeModal =
2021-11-11 16:43:05 -06:00
isShowingRecommendedGroupSizeModal;
this.#searchTerm = searchTerm;
const isUsernameVisible =
username !== undefined &&
username !== ourUsername &&
this.#candidateContacts.every(contact => contact.username !== username);
2022-06-16 17:38:28 -07:00
2024-02-07 16:34:31 -08:00
if (isUsernameVisible) {
this.#username = username;
2022-06-16 17:38:28 -07:00
}
2022-10-18 10:12:02 -07:00
this.#isUsernameChecked = selectedContacts.some(
contact => contact.username === this.#username
2024-02-07 16:34:31 -08:00
);
2022-10-18 10:12:02 -07:00
const phoneNumber = parseAndFormatPhoneNumber(searchTerm, regionCode);
if (
!isUsernameVisible &&
(ourUsername === undefined || username !== ourUsername) &&
phoneNumber
) {
const { e164 } = phoneNumber;
this.#isPhoneNumberChecked =
2022-10-18 10:12:02 -07:00
phoneNumber.isValid &&
selectedContacts.some(contact => contact.e164 === e164);
2022-10-18 10:12:02 -07:00
const isVisible =
e164 !== ourE164 &&
this.#candidateContacts.every(contact => contact.e164 !== e164);
2022-10-18 10:12:02 -07:00
if (isVisible) {
this.#phoneNumber = phoneNumber;
2022-10-18 10:12:02 -07:00
}
} else {
this.#isPhoneNumberChecked = false;
2022-10-18 10:12:02 -07:00
}
this.#selectedContacts = selectedContacts;
2021-03-03 14:09:58 -06:00
this.#selectedConversationIdsSet = new Set(
2021-03-03 14:09:58 -06:00
selectedContacts.map(contact => contact.id)
);
}
override getHeaderContents({
2021-03-03 14:09:58 -06:00
i18n,
startComposing,
}: Readonly<{
i18n: LocalizerType;
startComposing: () => void;
}>): ReactChild {
2023-03-29 17:03:25 -07:00
const backButtonLabel = i18n('icu:chooseGroupMembers__back-button');
2021-03-03 14:09:58 -06:00
return (
<div className="module-left-pane__header__contents">
<button
aria-label={backButtonLabel}
className="module-left-pane__header__contents__back-button"
onClick={this.getBackAction({ startComposing })}
2021-03-03 14:09:58 -06:00
title={backButtonLabel}
type="button"
/>
<div className="module-left-pane__header__contents__text">
2023-03-29 17:03:25 -07:00
{i18n('icu:chooseGroupMembers__title')}
2021-03-03 14:09:58 -06:00
</div>
</div>
);
}
override getBackAction({
startComposing,
}: {
startComposing: () => void;
}): () => void {
return startComposing;
}
2022-01-27 17:12:26 -05:00
override getSearchInput({
i18n,
onChangeComposeSearchTerm,
}: Readonly<{
i18n: LocalizerType;
onChangeComposeSearchTerm: (
event: ChangeEvent<HTMLInputElement>
) => unknown;
}>): ReactChild {
return (
<SearchInput
2022-02-14 12:57:11 -05:00
i18n={i18n}
2022-01-27 17:12:26 -05:00
moduleClassName="module-left-pane__compose-search-form"
onChange={onChangeComposeSearchTerm}
2023-03-29 17:03:25 -07:00
placeholder={i18n('icu:contactSearchPlaceholder')}
2022-01-27 17:12:26 -05:00
ref={focusRef}
value={this.#searchTerm}
2022-01-27 17:12:26 -05:00
/>
);
}
override getPreRowsNode({
2021-03-03 14:09:58 -06:00
closeMaximumGroupSizeModal,
closeRecommendedGroupSizeModal,
i18n,
removeSelectedContact,
}: Readonly<{
closeMaximumGroupSizeModal: () => unknown;
closeRecommendedGroupSizeModal: () => unknown;
i18n: LocalizerType;
removeSelectedContact: (conversationId: string) => unknown;
}>): ReactChild {
2021-03-11 15:29:31 -06:00
let modalNode: undefined | ReactChild;
if (this.#isShowingMaximumGroupSizeModal) {
2021-03-11 15:29:31 -06:00
modalNode = (
<AddGroupMemberErrorDialog
i18n={i18n}
maximumNumberOfContacts={this.#groupSizeHardLimit}
2021-03-11 15:29:31 -06:00
mode={AddGroupMemberErrorDialogMode.MaximumGroupSize}
onClose={closeMaximumGroupSizeModal}
/>
);
} else if (this.#isShowingRecommendedGroupSizeModal) {
2021-03-11 15:29:31 -06:00
modalNode = (
<AddGroupMemberErrorDialog
i18n={i18n}
recommendedMaximumNumberOfContacts={this.#groupSizeRecommendedLimit}
2021-03-11 15:29:31 -06:00
mode={AddGroupMemberErrorDialogMode.RecommendedMaximumGroupSize}
onClose={closeRecommendedGroupSizeModal}
/>
);
2021-03-03 14:09:58 -06:00
}
return (
<>
{Boolean(this.#selectedContacts.length) && (
2021-03-03 14:09:58 -06:00
<ContactPills>
{this.#selectedContacts.map(contact => (
2021-03-03 14:09:58 -06:00
<ContactPill
key={contact.id}
2021-05-07 17:21:10 -05:00
acceptedMessageRequest={contact.acceptedMessageRequest}
2024-07-11 12:44:09 -07:00
avatarUrl={contact.avatarUrl}
2021-03-03 14:09:58 -06:00
color={contact.color}
firstName={contact.systemGivenName ?? contact.firstName}
2021-03-03 14:09:58 -06:00
i18n={i18n}
id={contact.id}
2021-05-07 17:21:10 -05:00
isMe={contact.isMe}
2021-03-03 14:09:58 -06:00
phoneNumber={contact.phoneNumber}
profileName={contact.profileName}
2021-05-07 17:21:10 -05:00
sharedGroupNames={contact.sharedGroupNames}
2021-03-03 14:09:58 -06:00
title={contact.title}
onClickRemove={removeSelectedContact}
/>
))}
</ContactPills>
)}
{this.getRowCount() ? null : (
<div className="module-left-pane__compose-no-contacts">
2023-03-29 17:03:25 -07:00
{i18n('icu:noContactsFound')}
2021-03-03 14:09:58 -06:00
</div>
)}
2021-03-11 15:29:31 -06:00
{modalNode}
2021-03-03 14:09:58 -06:00
</>
);
}
override getFooterContents({
2021-03-03 14:09:58 -06:00
i18n,
startSettingGroupMetadata,
}: Readonly<{
i18n: LocalizerType;
startSettingGroupMetadata: () => void;
}>): ReactChild {
return (
<Button
disabled={this.#hasExceededMaximumNumberOfContacts()}
2021-03-03 14:09:58 -06:00
onClick={startSettingGroupMetadata}
>
{this.#selectedContacts.length
2023-03-29 17:03:25 -07:00
? i18n('icu:chooseGroupMembers__next')
: i18n('icu:chooseGroupMembers__skip')}
2021-03-03 14:09:58 -06:00
</Button>
);
}
getRowCount(): number {
let rowCount = 0;
// Header + Phone Number
if (this.#phoneNumber) {
rowCount += 2;
2021-03-03 14:09:58 -06:00
}
2022-06-16 17:38:28 -07:00
// Header + Username
if (this.#username) {
2022-06-16 17:38:28 -07:00
rowCount += 2;
}
// Header + Contacts
if (this.#candidateContacts.length) {
rowCount += 1 + this.#candidateContacts.length;
}
// Footer
if (rowCount > 0) {
rowCount += 1;
}
return rowCount;
2021-03-03 14:09:58 -06:00
}
getRow(actualRowIndex: number): undefined | Row {
if (
!this.#candidateContacts.length &&
!this.#phoneNumber &&
!this.#username
) {
2021-03-03 14:09:58 -06:00
return undefined;
}
const rowCount = this.getRowCount();
2021-03-03 14:09:58 -06:00
// This puts a blank row for the footer.
if (actualRowIndex === rowCount - 1) {
2021-03-03 14:09:58 -06:00
return { type: RowType.Blank };
}
let virtualRowIndex = actualRowIndex;
if (this.#candidateContacts.length) {
if (virtualRowIndex === 0) {
return {
type: RowType.Header,
2023-03-29 17:03:25 -07:00
getHeaderText: i18n => i18n('icu: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;
}
if (this.#phoneNumber) {
if (virtualRowIndex === 0) {
return {
type: RowType.Header,
2023-03-29 17:03:25 -07:00
getHeaderText: i18n => i18n('icu:findByPhoneNumberHeader'),
};
}
if (virtualRowIndex === 1) {
return {
type: RowType.PhoneNumberCheckbox,
isChecked: this.#isPhoneNumberChecked,
isFetching: isFetchingByE164(
this.#uuidFetchState,
this.#phoneNumber.e164
),
phoneNumber: this.#phoneNumber,
};
}
virtualRowIndex -= 2;
2021-03-03 14:09:58 -06:00
}
if (this.#username) {
2022-06-16 17:38:28 -07:00
if (virtualRowIndex === 0) {
return {
type: RowType.Header,
2023-03-29 17:03:25 -07:00
getHeaderText: i18n => i18n('icu:findByUsernameHeader'),
2022-06-16 17:38:28 -07:00
};
}
if (virtualRowIndex === 1) {
return {
type: RowType.UsernameCheckbox,
isChecked: this.#isUsernameChecked,
isFetching: isFetchingByUsername(
this.#uuidFetchState,
this.#username
),
username: this.#username,
2022-06-16 17:38:28 -07:00
};
}
virtualRowIndex -= 2;
}
return undefined;
2021-03-03 14:09:58 -06:00
}
// This is deliberately unimplemented because these keyboard shortcuts shouldn't work in
// the composer. The same is true for the "in direction" function below.
getConversationAndMessageAtIndex(
..._args: ReadonlyArray<unknown>
): undefined {
return undefined;
}
getConversationAndMessageInDirection(
..._args: ReadonlyArray<unknown>
): undefined {
return undefined;
}
shouldRecomputeRowHeights(_old: unknown): boolean {
return false;
}
#hasSelectedMaximumNumberOfContacts(): boolean {
return this.#selectedContacts.length >= this.#groupSizeHardLimit;
2021-03-03 14:09:58 -06:00
}
#hasExceededMaximumNumberOfContacts(): boolean {
2021-03-03 14:09:58 -06:00
// It should be impossible to reach this state. This is here as a failsafe.
return this.#selectedContacts.length > this.#groupSizeHardLimit;
2021-03-03 14:09:58 -06:00
}
}
function focusRef(el: HTMLElement | null) {
if (el) {
el.focus();
}
}