Support for creating New Groups
This commit is contained in:
parent
1934120e46
commit
5de4babc0d
56 changed files with 6222 additions and 526 deletions
304
ts/components/leftPane/LeftPaneChooseGroupMembersHelper.tsx
Normal file
304
ts/components/leftPane/LeftPaneChooseGroupMembersHelper.tsx
Normal file
|
@ -0,0 +1,304 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { ReactChild, ChangeEvent } from 'react';
|
||||
|
||||
import { LeftPaneHelper } from './LeftPaneHelper';
|
||||
import { Row, RowType } from '../ConversationList';
|
||||
import { ConversationType } from '../../state/ducks/conversations';
|
||||
import { ContactCheckboxDisabledReason } from '../conversationList/ContactCheckbox';
|
||||
import { ContactPills } from '../ContactPills';
|
||||
import { ContactPill } from '../ContactPill';
|
||||
import { Alert } from '../Alert';
|
||||
import { Button } from '../Button';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import {
|
||||
getGroupSizeRecommendedLimit,
|
||||
getGroupSizeHardLimit,
|
||||
} from '../../groups/limits';
|
||||
|
||||
export type LeftPaneChooseGroupMembersPropsType = {
|
||||
candidateContacts: ReadonlyArray<ConversationType>;
|
||||
cantAddContactForModal: undefined | ConversationType;
|
||||
isShowingRecommendedGroupSizeModal: boolean;
|
||||
isShowingMaximumGroupSizeModal: boolean;
|
||||
searchTerm: string;
|
||||
selectedContacts: Array<ConversationType>;
|
||||
};
|
||||
|
||||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<
|
||||
LeftPaneChooseGroupMembersPropsType
|
||||
> {
|
||||
private readonly candidateContacts: ReadonlyArray<ConversationType>;
|
||||
|
||||
private readonly cantAddContactForModal:
|
||||
| undefined
|
||||
| Readonly<{ title: string }>;
|
||||
|
||||
private readonly isShowingMaximumGroupSizeModal: boolean;
|
||||
|
||||
private readonly isShowingRecommendedGroupSizeModal: boolean;
|
||||
|
||||
private readonly searchTerm: string;
|
||||
|
||||
private readonly selectedContacts: Array<ConversationType>;
|
||||
|
||||
private readonly selectedConversationIdsSet: Set<string>;
|
||||
|
||||
constructor({
|
||||
candidateContacts,
|
||||
cantAddContactForModal,
|
||||
isShowingMaximumGroupSizeModal,
|
||||
isShowingRecommendedGroupSizeModal,
|
||||
searchTerm,
|
||||
selectedContacts,
|
||||
}: Readonly<LeftPaneChooseGroupMembersPropsType>) {
|
||||
super();
|
||||
|
||||
this.candidateContacts = candidateContacts;
|
||||
this.cantAddContactForModal = cantAddContactForModal;
|
||||
this.isShowingMaximumGroupSizeModal = isShowingMaximumGroupSizeModal;
|
||||
this.isShowingRecommendedGroupSizeModal = isShowingRecommendedGroupSizeModal;
|
||||
this.searchTerm = searchTerm;
|
||||
this.selectedContacts = selectedContacts;
|
||||
|
||||
this.selectedConversationIdsSet = new Set(
|
||||
selectedContacts.map(contact => contact.id)
|
||||
);
|
||||
}
|
||||
|
||||
getHeaderContents({
|
||||
i18n,
|
||||
startComposing,
|
||||
}: Readonly<{
|
||||
i18n: LocalizerType;
|
||||
startComposing: () => void;
|
||||
}>): ReactChild {
|
||||
const backButtonLabel = i18n('chooseGroupMembers__back-button');
|
||||
|
||||
return (
|
||||
<div className="module-left-pane__header__contents">
|
||||
<button
|
||||
aria-label={backButtonLabel}
|
||||
className="module-left-pane__header__contents__back-button"
|
||||
onClick={startComposing}
|
||||
title={backButtonLabel}
|
||||
type="button"
|
||||
/>
|
||||
<div className="module-left-pane__header__contents__text">
|
||||
{i18n('chooseGroupMembers__title')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getPreRowsNode({
|
||||
closeCantAddContactToGroupModal,
|
||||
closeMaximumGroupSizeModal,
|
||||
closeRecommendedGroupSizeModal,
|
||||
i18n,
|
||||
onChangeComposeSearchTerm,
|
||||
removeSelectedContact,
|
||||
}: Readonly<{
|
||||
closeCantAddContactToGroupModal: () => unknown;
|
||||
closeMaximumGroupSizeModal: () => unknown;
|
||||
closeRecommendedGroupSizeModal: () => unknown;
|
||||
i18n: LocalizerType;
|
||||
onChangeComposeSearchTerm: (
|
||||
event: ChangeEvent<HTMLInputElement>
|
||||
) => unknown;
|
||||
removeSelectedContact: (conversationId: string) => unknown;
|
||||
}>): ReactChild {
|
||||
let modalDetails:
|
||||
| undefined
|
||||
| { title: string; body: string; onClose: () => void };
|
||||
if (this.isShowingMaximumGroupSizeModal) {
|
||||
modalDetails = {
|
||||
title: i18n('chooseGroupMembers__maximum-group-size__title'),
|
||||
body: i18n('chooseGroupMembers__maximum-group-size__body', [
|
||||
this.getMaximumNumberOfContacts().toString(),
|
||||
]),
|
||||
onClose: closeMaximumGroupSizeModal,
|
||||
};
|
||||
} else if (this.isShowingRecommendedGroupSizeModal) {
|
||||
modalDetails = {
|
||||
title: i18n(
|
||||
'chooseGroupMembers__maximum-recommended-group-size__title'
|
||||
),
|
||||
body: i18n('chooseGroupMembers__maximum-recommended-group-size__body', [
|
||||
this.getRecommendedMaximumNumberOfContacts().toString(),
|
||||
]),
|
||||
onClose: closeRecommendedGroupSizeModal,
|
||||
};
|
||||
} else if (this.cantAddContactForModal) {
|
||||
modalDetails = {
|
||||
title: i18n('chooseGroupMembers__cant-add-member__title'),
|
||||
body: i18n('chooseGroupMembers__cant-add-member__body', [
|
||||
this.cantAddContactForModal.title,
|
||||
]),
|
||||
onClose: closeCantAddContactToGroupModal,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="module-left-pane__compose-search-form">
|
||||
<input
|
||||
type="text"
|
||||
ref={focusRef}
|
||||
className="module-left-pane__compose-search-form__input"
|
||||
placeholder={i18n('newConversationContactSearchPlaceholder')}
|
||||
dir="auto"
|
||||
value={this.searchTerm}
|
||||
onChange={onChangeComposeSearchTerm}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{Boolean(this.selectedContacts.length) && (
|
||||
<ContactPills>
|
||||
{this.selectedContacts.map(contact => (
|
||||
<ContactPill
|
||||
key={contact.id}
|
||||
avatarPath={contact.avatarPath}
|
||||
color={contact.color}
|
||||
firstName={contact.firstName}
|
||||
i18n={i18n}
|
||||
id={contact.id}
|
||||
name={contact.name}
|
||||
phoneNumber={contact.phoneNumber}
|
||||
profileName={contact.profileName}
|
||||
title={contact.title}
|
||||
onClickRemove={removeSelectedContact}
|
||||
/>
|
||||
))}
|
||||
</ContactPills>
|
||||
)}
|
||||
|
||||
{this.getRowCount() ? null : (
|
||||
<div className="module-left-pane__compose-no-contacts">
|
||||
{i18n('newConversationNoContacts')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{modalDetails && (
|
||||
<Alert
|
||||
body={modalDetails.body}
|
||||
i18n={i18n}
|
||||
onClose={modalDetails.onClose}
|
||||
title={modalDetails.title}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
getFooterContents({
|
||||
i18n,
|
||||
startSettingGroupMetadata,
|
||||
}: Readonly<{
|
||||
i18n: LocalizerType;
|
||||
startSettingGroupMetadata: () => void;
|
||||
}>): ReactChild {
|
||||
return (
|
||||
<Button
|
||||
disabled={this.hasExceededMaximumNumberOfContacts()}
|
||||
onClick={startSettingGroupMetadata}
|
||||
>
|
||||
{this.selectedContacts.length
|
||||
? i18n('chooseGroupMembers__next')
|
||||
: i18n('chooseGroupMembers__skip')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
getRowCount(): number {
|
||||
if (!this.candidateContacts.length) {
|
||||
return 0;
|
||||
}
|
||||
return this.candidateContacts.length + 2;
|
||||
}
|
||||
|
||||
getRow(rowIndex: number): undefined | Row {
|
||||
if (!this.candidateContacts.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (rowIndex === 0) {
|
||||
return {
|
||||
type: RowType.Header,
|
||||
i18nKey: 'contactsHeader',
|
||||
};
|
||||
}
|
||||
|
||||
// This puts a blank row for the footer.
|
||||
if (rowIndex === this.candidateContacts.length + 1) {
|
||||
return { type: RowType.Blank };
|
||||
}
|
||||
|
||||
const contact = this.candidateContacts[rowIndex - 1];
|
||||
if (!contact) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const isChecked = this.selectedConversationIdsSet.has(contact.id);
|
||||
|
||||
let disabledReason: undefined | ContactCheckboxDisabledReason;
|
||||
if (!isChecked) {
|
||||
if (this.hasSelectedMaximumNumberOfContacts()) {
|
||||
disabledReason = ContactCheckboxDisabledReason.MaximumContactsSelected;
|
||||
} else if (!contact.isGroupV2Capable) {
|
||||
disabledReason = ContactCheckboxDisabledReason.NotCapable;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: RowType.ContactCheckbox,
|
||||
contact,
|
||||
isChecked,
|
||||
disabledReason,
|
||||
};
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
private hasSelectedMaximumNumberOfContacts(): boolean {
|
||||
return this.selectedContacts.length >= this.getMaximumNumberOfContacts();
|
||||
}
|
||||
|
||||
private hasExceededMaximumNumberOfContacts(): boolean {
|
||||
// It should be impossible to reach this state. This is here as a failsafe.
|
||||
return this.selectedContacts.length > this.getMaximumNumberOfContacts();
|
||||
}
|
||||
|
||||
private getRecommendedMaximumNumberOfContacts(): number {
|
||||
return getGroupSizeRecommendedLimit(151) - 1;
|
||||
}
|
||||
|
||||
private getMaximumNumberOfContacts(): number {
|
||||
return getGroupSizeHardLimit(1001) - 1;
|
||||
}
|
||||
}
|
||||
|
||||
function focusRef(el: HTMLElement | null) {
|
||||
if (el) {
|
||||
el.focus();
|
||||
}
|
||||
}
|
|
@ -12,6 +12,9 @@ import {
|
|||
instance as phoneNumberInstance,
|
||||
PhoneNumberFormat,
|
||||
} from '../../util/libphonenumberInstance';
|
||||
import { assert } from '../../util/assert';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import { isStorageWriteFeatureEnabled } from '../../storage/isFeatureEnabled';
|
||||
|
||||
export type LeftPaneComposePropsType = {
|
||||
composeContacts: ReadonlyArray<ContactListItemPropsType>;
|
||||
|
@ -19,6 +22,12 @@ export type LeftPaneComposePropsType = {
|
|||
searchTerm: string;
|
||||
};
|
||||
|
||||
enum TopButton {
|
||||
None,
|
||||
CreateNewGroup,
|
||||
StartNewConversation,
|
||||
}
|
||||
|
||||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
export class LeftPaneComposeHelper extends LeftPaneHelper<
|
||||
|
@ -98,24 +107,53 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<
|
|||
}
|
||||
|
||||
getRowCount(): number {
|
||||
return this.composeContacts.length + (this.phoneNumber ? 1 : 0);
|
||||
let result = this.composeContacts.length;
|
||||
if (this.hasTopButton()) {
|
||||
result += 1;
|
||||
}
|
||||
if (this.hasContactsHeader()) {
|
||||
result += 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
getRow(rowIndex: number): undefined | Row {
|
||||
let contactIndex = rowIndex;
|
||||
|
||||
if (this.phoneNumber) {
|
||||
if (rowIndex === 0) {
|
||||
return {
|
||||
type: RowType.StartNewConversation,
|
||||
phoneNumber: phoneNumberInstance.format(
|
||||
if (rowIndex === 0) {
|
||||
const topButton = this.getTopButton();
|
||||
switch (topButton) {
|
||||
case TopButton.None:
|
||||
break;
|
||||
case TopButton.StartNewConversation:
|
||||
assert(
|
||||
this.phoneNumber,
|
||||
PhoneNumberFormat.E164
|
||||
),
|
||||
};
|
||||
'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:
|
||||
throw missingCaseError(topButton);
|
||||
}
|
||||
}
|
||||
|
||||
contactIndex -= 1;
|
||||
if (rowIndex === 1 && this.hasContactsHeader()) {
|
||||
return {
|
||||
type: RowType.Header,
|
||||
i18nKey: 'contactsHeader',
|
||||
};
|
||||
}
|
||||
|
||||
let contactIndex: number;
|
||||
if (this.hasTopButton()) {
|
||||
contactIndex = rowIndex - 2;
|
||||
} else {
|
||||
contactIndex = rowIndex;
|
||||
}
|
||||
|
||||
const contact = this.composeContacts[contactIndex];
|
||||
|
@ -141,8 +179,29 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<
|
|||
return undefined;
|
||||
}
|
||||
|
||||
shouldRecomputeRowHeights(_old: unknown): boolean {
|
||||
return false;
|
||||
shouldRecomputeRowHeights(old: Readonly<LeftPaneComposePropsType>): boolean {
|
||||
return (
|
||||
this.hasContactsHeader() !==
|
||||
new LeftPaneComposeHelper(old).hasContactsHeader()
|
||||
);
|
||||
}
|
||||
|
||||
private getTopButton(): TopButton {
|
||||
if (this.phoneNumber) {
|
||||
return TopButton.StartNewConversation;
|
||||
}
|
||||
if (this.searchTerm || !isStorageWriteFeatureEnabled()) {
|
||||
return TopButton.None;
|
||||
}
|
||||
return TopButton.CreateNewGroup;
|
||||
}
|
||||
|
||||
private hasTopButton(): boolean {
|
||||
return this.getTopButton() !== TopButton.None;
|
||||
}
|
||||
|
||||
private hasContactsHeader(): boolean {
|
||||
return this.hasTopButton() && Boolean(this.composeContacts.length);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,6 +23,8 @@ export abstract class LeftPaneHelper<T> {
|
|||
_: Readonly<{
|
||||
i18n: LocalizerType;
|
||||
showInbox: () => void;
|
||||
startComposing: () => void;
|
||||
showChooseGroupMembers: () => void;
|
||||
}>
|
||||
): null | ReactChild {
|
||||
return null;
|
||||
|
@ -34,10 +36,28 @@ export abstract class LeftPaneHelper<T> {
|
|||
|
||||
getPreRowsNode(
|
||||
_: Readonly<{
|
||||
clearGroupCreationError: () => void;
|
||||
closeCantAddContactToGroupModal: () => unknown;
|
||||
closeMaximumGroupSizeModal: () => unknown;
|
||||
closeRecommendedGroupSizeModal: () => unknown;
|
||||
createGroup: () => unknown;
|
||||
i18n: LocalizerType;
|
||||
setComposeGroupAvatar: (_: undefined | ArrayBuffer) => unknown;
|
||||
setComposeGroupName: (_: string) => unknown;
|
||||
onChangeComposeSearchTerm: (
|
||||
event: ChangeEvent<HTMLInputElement>
|
||||
) => unknown;
|
||||
removeSelectedContact: (_: string) => unknown;
|
||||
}>
|
||||
): null | ReactChild {
|
||||
return null;
|
||||
}
|
||||
|
||||
getFooterContents(
|
||||
_: Readonly<{
|
||||
i18n: LocalizerType;
|
||||
startSettingGroupMetadata: () => void;
|
||||
createGroup: () => unknown;
|
||||
}>
|
||||
): null | ReactChild {
|
||||
return null;
|
||||
|
|
218
ts/components/leftPane/LeftPaneSetGroupMetadataHelper.tsx
Normal file
218
ts/components/leftPane/LeftPaneSetGroupMetadataHelper.tsx
Normal file
|
@ -0,0 +1,218 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { ReactChild } from 'react';
|
||||
|
||||
import { LeftPaneHelper } from './LeftPaneHelper';
|
||||
import { Row, RowType } from '../ConversationList';
|
||||
import { PropsDataType as ContactListItemPropsType } from '../conversationList/ContactListItem';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import { AvatarInput } from '../AvatarInput';
|
||||
import { Alert } from '../Alert';
|
||||
import { Spinner } from '../Spinner';
|
||||
import { Button } from '../Button';
|
||||
|
||||
export type LeftPaneSetGroupMetadataPropsType = {
|
||||
groupAvatar: undefined | ArrayBuffer;
|
||||
groupName: string;
|
||||
hasError: boolean;
|
||||
isCreating: boolean;
|
||||
selectedContacts: ReadonlyArray<ContactListItemPropsType>;
|
||||
};
|
||||
|
||||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<
|
||||
LeftPaneSetGroupMetadataPropsType
|
||||
> {
|
||||
private readonly groupAvatar: undefined | ArrayBuffer;
|
||||
|
||||
private readonly groupName: string;
|
||||
|
||||
private readonly hasError: boolean;
|
||||
|
||||
private readonly isCreating: boolean;
|
||||
|
||||
private readonly selectedContacts: ReadonlyArray<ContactListItemPropsType>;
|
||||
|
||||
constructor({
|
||||
groupAvatar,
|
||||
groupName,
|
||||
isCreating,
|
||||
hasError,
|
||||
selectedContacts,
|
||||
}: Readonly<LeftPaneSetGroupMetadataPropsType>) {
|
||||
super();
|
||||
|
||||
this.groupAvatar = groupAvatar;
|
||||
this.groupName = groupName;
|
||||
this.hasError = hasError;
|
||||
this.isCreating = isCreating;
|
||||
this.selectedContacts = selectedContacts;
|
||||
}
|
||||
|
||||
getHeaderContents({
|
||||
i18n,
|
||||
showChooseGroupMembers,
|
||||
}: Readonly<{
|
||||
i18n: LocalizerType;
|
||||
showChooseGroupMembers: () => void;
|
||||
}>): ReactChild {
|
||||
const backButtonLabel = i18n('setGroupMetadata__back-button');
|
||||
|
||||
return (
|
||||
<div className="module-left-pane__header__contents">
|
||||
<button
|
||||
aria-label={backButtonLabel}
|
||||
className="module-left-pane__header__contents__back-button"
|
||||
disabled={this.isCreating}
|
||||
onClick={showChooseGroupMembers}
|
||||
title={backButtonLabel}
|
||||
type="button"
|
||||
/>
|
||||
<div className="module-left-pane__header__contents__text">
|
||||
{i18n('setGroupMetadata__title')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getPreRowsNode({
|
||||
clearGroupCreationError,
|
||||
createGroup,
|
||||
i18n,
|
||||
setComposeGroupAvatar,
|
||||
setComposeGroupName,
|
||||
}: Readonly<{
|
||||
clearGroupCreationError: () => unknown;
|
||||
createGroup: () => unknown;
|
||||
i18n: LocalizerType;
|
||||
setComposeGroupAvatar: (_: undefined | ArrayBuffer) => unknown;
|
||||
setComposeGroupName: (_: string) => unknown;
|
||||
}>): ReactChild {
|
||||
const disabled = this.isCreating;
|
||||
|
||||
return (
|
||||
<form
|
||||
className="module-left-pane__header__form"
|
||||
onSubmit={event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (!this.canCreateGroup()) {
|
||||
return;
|
||||
}
|
||||
|
||||
createGroup();
|
||||
}}
|
||||
>
|
||||
<AvatarInput
|
||||
contextMenuId="left pane group avatar uploader"
|
||||
disabled={disabled}
|
||||
i18n={i18n}
|
||||
onChange={setComposeGroupAvatar}
|
||||
value={this.groupAvatar}
|
||||
/>
|
||||
<input
|
||||
disabled={disabled}
|
||||
className="module-left-pane__compose-input"
|
||||
onChange={event => {
|
||||
setComposeGroupName(event.target.value);
|
||||
}}
|
||||
placeholder={i18n('setGroupMetadata__group-name-placeholder')}
|
||||
ref={focusRef}
|
||||
type="text"
|
||||
value={this.groupName}
|
||||
/>
|
||||
|
||||
{this.hasError && (
|
||||
<Alert
|
||||
body={i18n('setGroupMetadata__error-message')}
|
||||
i18n={i18n}
|
||||
onClose={clearGroupCreationError}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
getFooterContents({
|
||||
createGroup,
|
||||
i18n,
|
||||
}: Readonly<{
|
||||
createGroup: () => unknown;
|
||||
i18n: LocalizerType;
|
||||
}>): ReactChild {
|
||||
return (
|
||||
<Button disabled={!this.canCreateGroup()} onClick={createGroup}>
|
||||
{this.isCreating ? (
|
||||
<Spinner size="20px" svgSize="small" direction="on-avatar" />
|
||||
) : (
|
||||
i18n('setGroupMetadata__create-group')
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
getRowCount(): number {
|
||||
if (!this.selectedContacts.length) {
|
||||
return 0;
|
||||
}
|
||||
return this.selectedContacts.length + 2;
|
||||
}
|
||||
|
||||
getRow(rowIndex: number): undefined | Row {
|
||||
if (!this.selectedContacts.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (rowIndex === 0) {
|
||||
return {
|
||||
type: RowType.Header,
|
||||
i18nKey: 'setGroupMetadata__members-header',
|
||||
};
|
||||
}
|
||||
|
||||
// This puts a blank row for the footer.
|
||||
if (rowIndex === this.selectedContacts.length + 1) {
|
||||
return { type: RowType.Blank };
|
||||
}
|
||||
|
||||
const contact = this.selectedContacts[rowIndex - 1];
|
||||
return contact
|
||||
? {
|
||||
type: RowType.Contact,
|
||||
contact,
|
||||
isClickable: false,
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
private canCreateGroup(): boolean {
|
||||
return !this.isCreating && Boolean(this.groupName.trim());
|
||||
}
|
||||
}
|
||||
|
||||
function focusRef(el: HTMLElement | null) {
|
||||
if (el) {
|
||||
el.focus();
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue