2023-01-03 19:55:46 +00:00
|
|
|
// Copyright 2021 Signal Messenger, LLC
|
2021-02-23 20:34:28 +00:00
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
2021-10-26 19:15:33 +00:00
|
|
|
import type { ReactChild, ChangeEvent } from 'react';
|
|
|
|
import React from 'react';
|
2021-02-23 20:34:28 +00:00
|
|
|
|
|
|
|
import { LeftPaneHelper } from './LeftPaneHelper';
|
2021-10-26 19:15:33 +00:00
|
|
|
import type { Row } from '../ConversationList';
|
|
|
|
import { RowType } from '../ConversationList';
|
2021-11-17 21:11:21 +00:00
|
|
|
import type { ContactListItemConversationType } from '../conversationList/ContactListItem';
|
2021-10-26 19:15:33 +00:00
|
|
|
import type { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem';
|
2021-05-11 00:50:43 +00:00
|
|
|
import { SearchInput } from '../SearchInput';
|
2021-10-26 19:15:33 +00:00
|
|
|
import type { LocalizerType } from '../../types/Util';
|
2022-04-05 00:38:22 +00:00
|
|
|
import type { ParsedE164Type } from '../../util/libphonenumberInstance';
|
|
|
|
import { parseAndFormatPhoneNumber } from '../../util/libphonenumberInstance';
|
2021-03-03 20:09:58 +00:00
|
|
|
import { missingCaseError } from '../../util/missingCaseError';
|
2022-10-24 20:46:36 +00:00
|
|
|
import { getUsernameFromSearch } from '../../types/Username';
|
2022-04-05 00:38:22 +00:00
|
|
|
import type { UUIDFetchStateType } from '../../util/uuidFetchState';
|
|
|
|
import {
|
|
|
|
isFetchingByUsername,
|
|
|
|
isFetchingByE164,
|
|
|
|
} from '../../util/uuidFetchState';
|
2021-02-23 20:34:28 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
2022-02-23 18:48:40 +00:00
|
|
|
regionCode: string | undefined;
|
2021-02-23 20:34:28 +00:00
|
|
|
searchTerm: string;
|
2022-04-05 00:38:22 +00:00
|
|
|
uuidFetchState: UUIDFetchStateType;
|
2021-11-12 01:17:29 +00:00
|
|
|
isUsernamesEnabled: boolean;
|
2021-02-23 20:34:28 +00:00
|
|
|
};
|
|
|
|
|
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>;
|
2021-02-23 20:34:28 +00:00
|
|
|
|
2021-04-20 23:16:49 +00:00
|
|
|
private readonly composeGroups: ReadonlyArray<ConversationListItemPropsType>;
|
|
|
|
|
2022-04-05 00:38:22 +00:00
|
|
|
private readonly uuidFetchState: UUIDFetchStateType;
|
2021-11-12 01:17:29 +00:00
|
|
|
|
2021-02-23 20:34:28 +00:00
|
|
|
private readonly searchTerm: string;
|
|
|
|
|
2022-04-05 00:38:22 +00:00
|
|
|
private readonly phoneNumber: ParsedE164Type | undefined;
|
|
|
|
|
|
|
|
private readonly isPhoneNumberVisible: boolean;
|
2021-02-23 20:34:28 +00:00
|
|
|
|
2022-06-17 00:38:28 +00:00
|
|
|
private readonly username: string | undefined;
|
|
|
|
|
|
|
|
private readonly isUsernameVisible: boolean;
|
|
|
|
|
2021-02-23 20:34:28 +00:00
|
|
|
constructor({
|
|
|
|
composeContacts,
|
2021-04-20 23:16:49 +00:00
|
|
|
composeGroups,
|
2021-02-23 20:34:28 +00:00
|
|
|
regionCode,
|
|
|
|
searchTerm,
|
2021-11-12 01:17:29 +00:00
|
|
|
isUsernamesEnabled,
|
2022-04-05 00:38:22 +00:00
|
|
|
uuidFetchState,
|
2021-02-23 20:34:28 +00:00
|
|
|
}: Readonly<LeftPaneComposePropsType>) {
|
|
|
|
super();
|
|
|
|
|
2021-11-12 01:17:29 +00:00
|
|
|
this.composeContacts = composeContacts;
|
|
|
|
this.composeGroups = composeGroups;
|
2021-02-23 20:34:28 +00:00
|
|
|
this.searchTerm = searchTerm;
|
2022-04-05 00:38:22 +00:00
|
|
|
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;
|
|
|
|
}
|
2021-02-23 20:34:28 +00:00
|
|
|
}
|
|
|
|
|
2021-11-12 23:44:20 +00:00
|
|
|
override getHeaderContents({
|
2021-02-23 20:34:28 +00:00
|
|
|
i18n,
|
|
|
|
showInbox,
|
|
|
|
}: Readonly<{
|
|
|
|
i18n: LocalizerType;
|
|
|
|
showInbox: () => void;
|
|
|
|
}>): ReactChild {
|
|
|
|
return (
|
|
|
|
<div className="module-left-pane__header__contents">
|
|
|
|
<button
|
2021-04-02 21:43:39 +00:00
|
|
|
onClick={this.getBackAction({ showInbox })}
|
2021-02-23 20:34:28 +00:00
|
|
|
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>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-11-12 23:44:20 +00:00
|
|
|
override getBackAction({ showInbox }: { showInbox: () => void }): () => void {
|
2021-04-02 21:43:39 +00:00
|
|
|
return showInbox;
|
|
|
|
}
|
|
|
|
|
2022-01-27 22:12:26 +00:00
|
|
|
override getSearchInput({
|
2021-02-23 20:34:28 +00:00
|
|
|
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}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
2021-02-23 20:34:28 +00:00
|
|
|
|
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>
|
2021-02-23 20:34:28 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
getRowCount(): number {
|
2021-04-20 23:16:49 +00:00
|
|
|
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;
|
|
|
|
}
|
2021-04-20 23:16:49 +00:00
|
|
|
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;
|
|
|
|
}
|
2022-04-05 00:38:22 +00:00
|
|
|
if (this.isPhoneNumberVisible) {
|
|
|
|
result += 2;
|
|
|
|
}
|
2021-04-20 23:16:49 +00:00
|
|
|
|
2021-03-03 20:09:58 +00:00
|
|
|
return result;
|
2021-02-23 20:34:28 +00:00
|
|
|
}
|
|
|
|
|
2021-04-20 23:16:49 +00:00
|
|
|
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);
|
|
|
|
}
|
2021-02-23 20:34:28 +00:00
|
|
|
}
|
|
|
|
|
2021-04-20 23:16:49 +00:00
|
|
|
virtualRowIndex -= 1;
|
2021-03-03 20:09:58 +00:00
|
|
|
}
|
|
|
|
|
2021-04-20 23:16:49 +00:00
|
|
|
if (this.hasContactsHeader()) {
|
|
|
|
if (virtualRowIndex === 0) {
|
|
|
|
return {
|
|
|
|
type: RowType.Header,
|
|
|
|
i18nKey: 'contactsHeader',
|
|
|
|
};
|
|
|
|
}
|
2021-02-23 20:34:28 +00:00
|
|
|
|
2021-04-20 23:16:49 +00:00
|
|
|
virtualRowIndex -= 1;
|
|
|
|
|
|
|
|
const contact = this.composeContacts[virtualRowIndex];
|
|
|
|
if (contact) {
|
|
|
|
return {
|
2021-02-23 20:34:28 +00:00
|
|
|
type: RowType.Contact,
|
|
|
|
contact,
|
2021-04-20 23:16:49 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
2022-04-05 00:38:22 +00:00
|
|
|
isFetchingUsername: isFetchingByUsername(
|
|
|
|
this.uuidFetchState,
|
2022-06-17 00:38:28 +00:00
|
|
|
this.username
|
2022-04-05 00:38:22 +00:00
|
|
|
),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
};
|
|
|
|
}
|
2021-04-20 23:16:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return undefined;
|
2021-02-23 20:34:28 +00: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;
|
|
|
|
}
|
|
|
|
|
2021-04-20 23:16:49 +00:00
|
|
|
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 (
|
2021-04-20 23:16:49 +00:00
|
|
|
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 {
|
2021-04-20 23:16:49 +00:00
|
|
|
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;
|
2021-04-20 23:16:49 +00:00
|
|
|
} {
|
|
|
|
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;
|
|
|
|
|
2021-04-20 23:16:49 +00:00
|
|
|
let rowCount = 0;
|
2021-11-12 01:17:29 +00:00
|
|
|
|
2021-04-20 23:16:49 +00:00
|
|
|
if (this.hasTopButton()) {
|
|
|
|
top = 0;
|
|
|
|
rowCount += 1;
|
|
|
|
}
|
2021-11-12 01:17:29 +00:00
|
|
|
if (this.hasContactsHeader()) {
|
2021-04-20 23:16:49 +00:00
|
|
|
contact = rowCount;
|
|
|
|
rowCount += this.composeContacts.length;
|
|
|
|
}
|
2021-11-12 01:17:29 +00:00
|
|
|
if (this.hasGroupsHeader()) {
|
2021-04-20 23:16:49 +00:00
|
|
|
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;
|
2021-04-20 23:16:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
top,
|
|
|
|
contact,
|
|
|
|
group,
|
2022-06-17 00:38:28 +00:00
|
|
|
phoneNumber,
|
2021-11-12 01:17:29 +00:00
|
|
|
username,
|
2021-04-20 23:16:49 +00:00
|
|
|
};
|
2021-02-23 20:34:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function focusRef(el: HTMLElement | null) {
|
|
|
|
if (el) {
|
|
|
|
el.focus();
|
|
|
|
}
|
|
|
|
}
|