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

380 lines
9.5 KiB
TypeScript
Raw Normal View History

2023-01-03 19:55:46 +00:00
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ReactChild, ChangeEvent } from 'react';
import React from 'react';
import { LeftPaneHelper } from './LeftPaneHelper';
import type { Row } from '../ConversationList';
import { RowType } from '../ConversationList';
2021-11-17 21:11:21 +00:00
import type { ContactListItemConversationType } from '../conversationList/ContactListItem';
import type { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem';
2021-05-11 00:50:43 +00:00
import { SearchInput } from '../SearchInput';
import type { LocalizerType } from '../../types/Util';
import type { ParsedE164Type } from '../../util/libphonenumberInstance';
import { parseAndFormatPhoneNumber } from '../../util/libphonenumberInstance';
2021-03-03 20:09:58 +00:00
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 = {
2021-11-17 21:11:21 +00:00
composeContacts: ReadonlyArray<ContactListItemConversationType>;
2021-10-22 18:12:10 +00:00
composeGroups: ReadonlyArray<ConversationListItemPropsType>;
2021-11-12 01:17:29 +00:00
regionCode: string | undefined;
searchTerm: string;
uuidFetchState: UUIDFetchStateType;
2021-11-12 01:17:29 +00:00
isUsernamesEnabled: boolean;
};
2021-03-03 20:09:58 +00:00
enum TopButton {
None,
CreateNewGroup,
}
2021-04-26 16:38:50 +00:00
export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsType> {
2021-11-17 21:11:21 +00:00
private readonly composeContacts: ReadonlyArray<ContactListItemConversationType>;
private readonly composeGroups: ReadonlyArray<ConversationListItemPropsType>;
private readonly uuidFetchState: UUIDFetchStateType;
2021-11-12 01:17:29 +00:00
private readonly searchTerm: string;
private readonly phoneNumber: ParsedE164Type | undefined;
private readonly isPhoneNumberVisible: boolean;
2022-06-17 00:38:28 +00:00
private readonly username: string | undefined;
private readonly isUsernameVisible: boolean;
constructor({
composeContacts,
composeGroups,
regionCode,
searchTerm,
2021-11-12 01:17:29 +00:00
isUsernamesEnabled,
uuidFetchState,
}: Readonly<LeftPaneComposePropsType>) {
super();
2021-11-12 01:17:29 +00:00
this.composeContacts = composeContacts;
this.composeGroups = composeGroups;
this.searchTerm = searchTerm;
this.uuidFetchState = uuidFetchState;
2022-06-17 00:38:28 +00:00
2022-10-18 17:12:02 +00:00
if (isUsernamesEnabled) {
2022-06-17 00:38:28 +00:00
this.username = getUsernameFromSearch(this.searchTerm);
this.isUsernameVisible =
isUsernamesEnabled &&
Boolean(this.username) &&
this.composeContacts.every(
contact => contact.username !== this.username
);
} else {
this.isUsernameVisible = false;
}
2022-10-18 17:12:02 +00:00
const phoneNumber = parseAndFormatPhoneNumber(searchTerm, regionCode);
if (!this.username && phoneNumber) {
this.phoneNumber = phoneNumber;
this.isPhoneNumberVisible = this.composeContacts.every(
contact => contact.e164 !== phoneNumber.e164
);
} else {
this.isPhoneNumberVisible = false;
}
}
override getHeaderContents({
i18n,
showInbox,
}: Readonly<{
i18n: LocalizerType;
showInbox: () => void;
}>): ReactChild {
return (
<div className="module-left-pane__header__contents">
<button
onClick={this.getBackAction({ showInbox })}
className="module-left-pane__header__contents__back-button"
title={i18n('backToInbox')}
aria-label={i18n('backToInbox')}
type="button"
/>
<div className="module-left-pane__header__contents__text">
{i18n('newConversation')}
</div>
</div>
);
}
override getBackAction({ showInbox }: { showInbox: () => void }): () => void {
return showInbox;
}
2022-01-27 22:12:26 +00:00
override getSearchInput({
i18n,
onChangeComposeSearchTerm,
}: Readonly<{
i18n: LocalizerType;
onChangeComposeSearchTerm: (
event: ChangeEvent<HTMLInputElement>
) => unknown;
}>): ReactChild {
return (
2022-01-27 22:12:26 +00:00
<SearchInput
2022-02-14 17:57:11 +00:00
i18n={i18n}
2022-01-27 22:12:26 +00:00
moduleClassName="module-left-pane__compose-search-form"
onChange={onChangeComposeSearchTerm}
placeholder={i18n('contactSearchPlaceholder')}
ref={focusRef}
value={this.searchTerm}
/>
);
}
2022-01-27 22:12:26 +00:00
override getPreRowsNode({
i18n,
}: Readonly<{
i18n: LocalizerType;
}>): ReactChild | null {
return this.getRowCount() ? null : (
<div className="module-left-pane__compose-no-contacts">
{i18n('noConversationsFound')}
</div>
);
}
getRowCount(): number {
let result = this.composeContacts.length + this.composeGroups.length;
2021-03-03 20:09:58 +00:00
if (this.hasTopButton()) {
result += 1;
}
if (this.hasContactsHeader()) {
result += 1;
}
if (this.hasGroupsHeader()) {
result += 1;
}
2022-06-17 00:38:28 +00:00
if (this.isUsernameVisible) {
2021-11-12 01:17:29 +00:00
result += 2;
}
if (this.isPhoneNumberVisible) {
result += 2;
}
2021-03-03 20:09:58 +00:00
return result;
}
getRow(actualRowIndex: number): undefined | Row {
let virtualRowIndex = actualRowIndex;
if (this.hasTopButton()) {
if (virtualRowIndex === 0) {
const topButton = this.getTopButton();
switch (topButton) {
case TopButton.None:
break;
case TopButton.CreateNewGroup:
return { type: RowType.CreateNewGroup };
default:
throw missingCaseError(topButton);
}
}
virtualRowIndex -= 1;
2021-03-03 20:09:58 +00:00
}
if (this.hasContactsHeader()) {
if (virtualRowIndex === 0) {
return {
type: RowType.Header,
i18nKey: 'contactsHeader',
};
}
virtualRowIndex -= 1;
const contact = this.composeContacts[virtualRowIndex];
if (contact) {
return {
type: RowType.Contact,
contact,
};
}
virtualRowIndex -= this.composeContacts.length;
}
if (this.hasGroupsHeader()) {
if (virtualRowIndex === 0) {
return {
type: RowType.Header,
i18nKey: 'groupsHeader',
};
}
virtualRowIndex -= 1;
const group = this.composeGroups[virtualRowIndex];
2021-11-12 01:17:29 +00:00
if (group) {
return {
type: RowType.Conversation,
conversation: group,
};
}
virtualRowIndex -= this.composeGroups.length;
}
2022-06-17 00:38:28 +00:00
if (this.username && this.isUsernameVisible) {
2021-11-12 01:17:29 +00:00
if (virtualRowIndex === 0) {
return {
type: RowType.Header,
i18nKey: 'findByUsernameHeader',
};
}
virtualRowIndex -= 1;
if (virtualRowIndex === 0) {
return {
type: RowType.UsernameSearchResult,
2022-06-17 00:38:28 +00:00
username: this.username,
isFetchingUsername: isFetchingByUsername(
this.uuidFetchState,
2022-06-17 00:38:28 +00:00
this.username
),
};
}
}
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
),
2021-11-12 01:17:29 +00:00
};
}
}
return 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(
exProps: Readonly<LeftPaneComposePropsType>
): boolean {
const prev = new LeftPaneComposeHelper(exProps);
const currHeaderIndices = this.getHeaderIndices();
const prevHeaderIndices = prev.getHeaderIndices();
2021-03-03 20:09:58 +00:00
return (
currHeaderIndices.top !== prevHeaderIndices.top ||
currHeaderIndices.contact !== prevHeaderIndices.contact ||
2021-11-12 01:17:29 +00:00
currHeaderIndices.group !== prevHeaderIndices.group ||
2022-06-17 00:38:28 +00:00
currHeaderIndices.username !== prevHeaderIndices.username ||
currHeaderIndices.phoneNumber !== prevHeaderIndices.phoneNumber
2021-03-03 20:09:58 +00:00
);
}
private getTopButton(): TopButton {
2021-10-05 17:04:28 +00:00
if (this.searchTerm) {
2021-03-03 20:09:58 +00:00
return TopButton.None;
}
return TopButton.CreateNewGroup;
}
private hasTopButton(): boolean {
return this.getTopButton() !== TopButton.None;
}
private hasContactsHeader(): boolean {
return Boolean(this.composeContacts.length);
}
private hasGroupsHeader(): boolean {
return Boolean(this.composeGroups.length);
}
private getHeaderIndices(): {
top?: number;
contact?: number;
group?: number;
2022-06-17 00:38:28 +00:00
phoneNumber?: number;
2021-11-12 01:17:29 +00:00
username?: number;
} {
let top: number | undefined;
let contact: number | undefined;
let group: number | undefined;
2022-06-17 00:38:28 +00:00
let phoneNumber: number | undefined;
2021-11-12 01:17:29 +00:00
let username: number | undefined;
let rowCount = 0;
2021-11-12 01:17:29 +00:00
if (this.hasTopButton()) {
top = 0;
rowCount += 1;
}
2021-11-12 01:17:29 +00:00
if (this.hasContactsHeader()) {
contact = rowCount;
rowCount += this.composeContacts.length;
}
2021-11-12 01:17:29 +00:00
if (this.hasGroupsHeader()) {
group = rowCount;
2021-11-12 01:17:29 +00:00
rowCount += this.composeContacts.length;
}
2022-06-17 00:38:28 +00:00
if (this.phoneNumber) {
phoneNumber = rowCount;
}
if (this.username) {
2021-11-12 01:17:29 +00:00
username = rowCount;
}
return {
top,
contact,
group,
2022-06-17 00:38:28 +00:00
phoneNumber,
2021-11-12 01:17:29 +00:00
username,
};
}
}
function focusRef(el: HTMLElement | null) {
if (el) {
el.focus();
}
}