Add "new conversation" composer for direct messages
This commit is contained in:
parent
84dc166b63
commit
06fb4fd0bc
61 changed files with 5960 additions and 3887 deletions
113
ts/components/leftPane/LeftPaneArchiveHelper.tsx
Normal file
113
ts/components/leftPane/LeftPaneArchiveHelper.tsx
Normal file
|
@ -0,0 +1,113 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { ReactChild } from 'react';
|
||||
import { last } from 'lodash';
|
||||
|
||||
import { LeftPaneHelper, ToFindType } from './LeftPaneHelper';
|
||||
import { getConversationInDirection } from './getConversationInDirection';
|
||||
import { Row, RowType } from '../ConversationList';
|
||||
import { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
export type LeftPaneArchivePropsType = {
|
||||
archivedConversations: ReadonlyArray<ConversationListItemPropsType>;
|
||||
};
|
||||
|
||||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
export class LeftPaneArchiveHelper extends LeftPaneHelper<
|
||||
LeftPaneArchivePropsType
|
||||
> {
|
||||
private readonly archivedConversations: ReadonlyArray<
|
||||
ConversationListItemPropsType
|
||||
>;
|
||||
|
||||
constructor({ archivedConversations }: Readonly<LeftPaneArchivePropsType>) {
|
||||
super();
|
||||
|
||||
this.archivedConversations = archivedConversations;
|
||||
}
|
||||
|
||||
getHeaderContents({
|
||||
i18n,
|
||||
showInbox,
|
||||
}: Readonly<{
|
||||
i18n: LocalizerType;
|
||||
showInbox: () => void;
|
||||
}>): ReactChild {
|
||||
return (
|
||||
<div className="module-left-pane__header__contents">
|
||||
<button
|
||||
onClick={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('archivedConversations')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getPreRowsNode({ i18n }: Readonly<{ i18n: LocalizerType }>): ReactChild {
|
||||
return (
|
||||
<div className="module-left-pane__archive-helper-text">
|
||||
{i18n('archiveHelperText')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getRowCount(): number {
|
||||
return this.archivedConversations.length;
|
||||
}
|
||||
|
||||
getRow(rowIndex: number): undefined | Row {
|
||||
const conversation = this.archivedConversations[rowIndex];
|
||||
return conversation
|
||||
? {
|
||||
type: RowType.Conversation,
|
||||
conversation,
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
getRowIndexToScrollTo(
|
||||
selectedConversationId: undefined | string
|
||||
): undefined | number {
|
||||
if (!selectedConversationId) {
|
||||
return undefined;
|
||||
}
|
||||
const result = this.archivedConversations.findIndex(
|
||||
conversation => conversation.id === selectedConversationId
|
||||
);
|
||||
return result === -1 ? undefined : result;
|
||||
}
|
||||
|
||||
getConversationAndMessageAtIndex(
|
||||
conversationIndex: number
|
||||
): undefined | { conversationId: string } {
|
||||
const { archivedConversations } = this;
|
||||
const conversation =
|
||||
archivedConversations[conversationIndex] || last(archivedConversations);
|
||||
return conversation ? { conversationId: conversation.id } : undefined;
|
||||
}
|
||||
|
||||
getConversationAndMessageInDirection(
|
||||
toFind: Readonly<ToFindType>,
|
||||
selectedConversationId: undefined | string,
|
||||
_selectedMessageId: unknown
|
||||
): undefined | { conversationId: string } {
|
||||
return getConversationInDirection(
|
||||
this.archivedConversations,
|
||||
toFind,
|
||||
selectedConversationId
|
||||
);
|
||||
}
|
||||
|
||||
shouldRecomputeRowHeights(_old: unknown): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
171
ts/components/leftPane/LeftPaneComposeHelper.tsx
Normal file
171
ts/components/leftPane/LeftPaneComposeHelper.tsx
Normal file
|
@ -0,0 +1,171 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { ReactChild, ChangeEvent } from 'react';
|
||||
import { PhoneNumber } from 'google-libphonenumber';
|
||||
|
||||
import { LeftPaneHelper } from './LeftPaneHelper';
|
||||
import { Row, RowType } from '../ConversationList';
|
||||
import { PropsDataType as ContactListItemPropsType } from '../conversationList/ContactListItem';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import {
|
||||
instance as phoneNumberInstance,
|
||||
PhoneNumberFormat,
|
||||
} from '../../util/libphonenumberInstance';
|
||||
|
||||
export type LeftPaneComposePropsType = {
|
||||
composeContacts: ReadonlyArray<ContactListItemPropsType>;
|
||||
regionCode: string;
|
||||
searchTerm: string;
|
||||
};
|
||||
|
||||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
export class LeftPaneComposeHelper extends LeftPaneHelper<
|
||||
LeftPaneComposePropsType
|
||||
> {
|
||||
private readonly composeContacts: ReadonlyArray<ContactListItemPropsType>;
|
||||
|
||||
private readonly searchTerm: string;
|
||||
|
||||
private readonly phoneNumber: undefined | PhoneNumber;
|
||||
|
||||
constructor({
|
||||
composeContacts,
|
||||
regionCode,
|
||||
searchTerm,
|
||||
}: Readonly<LeftPaneComposePropsType>) {
|
||||
super();
|
||||
|
||||
this.composeContacts = composeContacts;
|
||||
this.searchTerm = searchTerm;
|
||||
this.phoneNumber = parsePhoneNumber(searchTerm, regionCode);
|
||||
}
|
||||
|
||||
getHeaderContents({
|
||||
i18n,
|
||||
showInbox,
|
||||
}: Readonly<{
|
||||
i18n: LocalizerType;
|
||||
showInbox: () => void;
|
||||
}>): ReactChild {
|
||||
return (
|
||||
<div className="module-left-pane__header__contents">
|
||||
<button
|
||||
onClick={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>
|
||||
);
|
||||
}
|
||||
|
||||
getPreRowsNode({
|
||||
i18n,
|
||||
onChangeComposeSearchTerm,
|
||||
}: Readonly<{
|
||||
i18n: LocalizerType;
|
||||
onChangeComposeSearchTerm: (
|
||||
event: ChangeEvent<HTMLInputElement>
|
||||
) => unknown;
|
||||
}>): ReactChild {
|
||||
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>
|
||||
|
||||
{this.getRowCount() ? null : (
|
||||
<div className="module-left-pane__compose-no-contacts">
|
||||
{i18n('newConversationNoContacts')}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
getRowCount(): number {
|
||||
return this.composeContacts.length + (this.phoneNumber ? 1 : 0);
|
||||
}
|
||||
|
||||
getRow(rowIndex: number): undefined | Row {
|
||||
let contactIndex = rowIndex;
|
||||
|
||||
if (this.phoneNumber) {
|
||||
if (rowIndex === 0) {
|
||||
return {
|
||||
type: RowType.StartNewConversation,
|
||||
phoneNumber: phoneNumberInstance.format(
|
||||
this.phoneNumber,
|
||||
PhoneNumberFormat.E164
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
contactIndex -= 1;
|
||||
}
|
||||
|
||||
const contact = this.composeContacts[contactIndex];
|
||||
return contact
|
||||
? {
|
||||
type: RowType.Contact,
|
||||
contact,
|
||||
}
|
||||
: 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;
|
||||
}
|
||||
}
|
||||
|
||||
function focusRef(el: HTMLElement | null) {
|
||||
if (el) {
|
||||
el.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function parsePhoneNumber(
|
||||
str: string,
|
||||
regionCode: string
|
||||
): undefined | PhoneNumber {
|
||||
let result: PhoneNumber;
|
||||
try {
|
||||
result = phoneNumberInstance.parse(str, regionCode);
|
||||
} catch (err) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!phoneNumberInstance.isValidNumber(result)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
67
ts/components/leftPane/LeftPaneHelper.tsx
Normal file
67
ts/components/leftPane/LeftPaneHelper.tsx
Normal file
|
@ -0,0 +1,67 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { ChangeEvent, ReactChild } from 'react';
|
||||
|
||||
import { Row } from '../ConversationList';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
export enum FindDirection {
|
||||
Up,
|
||||
Down,
|
||||
}
|
||||
|
||||
export type ToFindType = {
|
||||
direction: FindDirection;
|
||||
unreadOnly: boolean;
|
||||
};
|
||||
|
||||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
export abstract class LeftPaneHelper<T> {
|
||||
getHeaderContents(
|
||||
_: Readonly<{
|
||||
i18n: LocalizerType;
|
||||
showInbox: () => void;
|
||||
}>
|
||||
): null | ReactChild {
|
||||
return null;
|
||||
}
|
||||
|
||||
shouldRenderNetworkStatusAndUpdateDialog(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
getPreRowsNode(
|
||||
_: Readonly<{
|
||||
i18n: LocalizerType;
|
||||
onChangeComposeSearchTerm: (
|
||||
event: ChangeEvent<HTMLInputElement>
|
||||
) => unknown;
|
||||
}>
|
||||
): null | ReactChild {
|
||||
return null;
|
||||
}
|
||||
|
||||
abstract getRowCount(): number;
|
||||
|
||||
abstract getRow(rowIndex: number): undefined | Row;
|
||||
|
||||
getRowIndexToScrollTo(
|
||||
_selectedConversationId: undefined | string
|
||||
): undefined | number {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
abstract getConversationAndMessageAtIndex(
|
||||
conversationIndex: number
|
||||
): undefined | { conversationId: string; messageId?: string };
|
||||
|
||||
abstract getConversationAndMessageInDirection(
|
||||
toFind: Readonly<ToFindType>,
|
||||
selectedConversationId: undefined | string,
|
||||
selectedMessageId: undefined | string
|
||||
): undefined | { conversationId: string; messageId?: string };
|
||||
|
||||
abstract shouldRecomputeRowHeights(old: Readonly<T>): boolean;
|
||||
}
|
192
ts/components/leftPane/LeftPaneInboxHelper.ts
Normal file
192
ts/components/leftPane/LeftPaneInboxHelper.ts
Normal file
|
@ -0,0 +1,192 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { last } from 'lodash';
|
||||
|
||||
import { LeftPaneHelper, ToFindType } from './LeftPaneHelper';
|
||||
import { getConversationInDirection } from './getConversationInDirection';
|
||||
import { Row, RowType } from '../ConversationList';
|
||||
import { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem';
|
||||
|
||||
export type LeftPaneInboxPropsType = {
|
||||
conversations: ReadonlyArray<ConversationListItemPropsType>;
|
||||
archivedConversations: ReadonlyArray<ConversationListItemPropsType>;
|
||||
pinnedConversations: ReadonlyArray<ConversationListItemPropsType>;
|
||||
};
|
||||
|
||||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
export class LeftPaneInboxHelper extends LeftPaneHelper<
|
||||
LeftPaneInboxPropsType
|
||||
> {
|
||||
private readonly conversations: ReadonlyArray<ConversationListItemPropsType>;
|
||||
|
||||
private readonly archivedConversations: ReadonlyArray<
|
||||
ConversationListItemPropsType
|
||||
>;
|
||||
|
||||
private readonly pinnedConversations: ReadonlyArray<
|
||||
ConversationListItemPropsType
|
||||
>;
|
||||
|
||||
constructor({
|
||||
conversations,
|
||||
archivedConversations,
|
||||
pinnedConversations,
|
||||
}: Readonly<LeftPaneInboxPropsType>) {
|
||||
super();
|
||||
|
||||
this.conversations = conversations;
|
||||
this.archivedConversations = archivedConversations;
|
||||
this.pinnedConversations = pinnedConversations;
|
||||
}
|
||||
|
||||
shouldRenderNetworkStatusAndUpdateDialog(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
getRowCount(): number {
|
||||
const headerCount = this.hasPinnedAndNonpinned() ? 2 : 0;
|
||||
const buttonCount = this.archivedConversations.length ? 1 : 0;
|
||||
return (
|
||||
headerCount +
|
||||
this.pinnedConversations.length +
|
||||
this.conversations.length +
|
||||
buttonCount
|
||||
);
|
||||
}
|
||||
|
||||
getRow(rowIndex: number): undefined | Row {
|
||||
const { conversations, archivedConversations, pinnedConversations } = this;
|
||||
|
||||
const archivedConversationsCount = archivedConversations.length;
|
||||
|
||||
if (this.hasPinnedAndNonpinned()) {
|
||||
switch (rowIndex) {
|
||||
case 0:
|
||||
return {
|
||||
type: RowType.Header,
|
||||
i18nKey: 'LeftPane--pinned',
|
||||
};
|
||||
case pinnedConversations.length + 1:
|
||||
return {
|
||||
type: RowType.Header,
|
||||
i18nKey: 'LeftPane--chats',
|
||||
};
|
||||
case pinnedConversations.length + conversations.length + 2:
|
||||
if (archivedConversationsCount) {
|
||||
return {
|
||||
type: RowType.ArchiveButton,
|
||||
archivedConversationsCount,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
default: {
|
||||
const pinnedConversation = pinnedConversations[rowIndex - 1];
|
||||
if (pinnedConversation) {
|
||||
return {
|
||||
type: RowType.Conversation,
|
||||
conversation: pinnedConversation,
|
||||
};
|
||||
}
|
||||
const conversation =
|
||||
conversations[rowIndex - pinnedConversations.length - 2];
|
||||
return conversation
|
||||
? {
|
||||
type: RowType.Conversation,
|
||||
conversation,
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onlyConversations = pinnedConversations.length
|
||||
? pinnedConversations
|
||||
: conversations;
|
||||
if (rowIndex < onlyConversations.length) {
|
||||
const conversation = onlyConversations[rowIndex];
|
||||
return conversation
|
||||
? {
|
||||
type: RowType.Conversation,
|
||||
conversation,
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
if (rowIndex === onlyConversations.length && archivedConversationsCount) {
|
||||
return {
|
||||
type: RowType.ArchiveButton,
|
||||
archivedConversationsCount,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getRowIndexToScrollTo(
|
||||
selectedConversationId: undefined | string
|
||||
): undefined | number {
|
||||
if (!selectedConversationId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const isConversationSelected = (
|
||||
conversation: Readonly<ConversationListItemPropsType>
|
||||
) => conversation.id === selectedConversationId;
|
||||
const hasHeaders = this.hasPinnedAndNonpinned();
|
||||
|
||||
const pinnedConversationIndex = this.pinnedConversations.findIndex(
|
||||
isConversationSelected
|
||||
);
|
||||
if (pinnedConversationIndex !== -1) {
|
||||
const headerOffset = hasHeaders ? 1 : 0;
|
||||
return pinnedConversationIndex + headerOffset;
|
||||
}
|
||||
|
||||
const conversationIndex = this.conversations.findIndex(
|
||||
isConversationSelected
|
||||
);
|
||||
if (conversationIndex !== -1) {
|
||||
const pinnedOffset = this.pinnedConversations.length;
|
||||
const headerOffset = hasHeaders ? 2 : 0;
|
||||
return conversationIndex + pinnedOffset + headerOffset;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
shouldRecomputeRowHeights(old: Readonly<LeftPaneInboxPropsType>): boolean {
|
||||
return old.pinnedConversations.length !== this.pinnedConversations.length;
|
||||
}
|
||||
|
||||
getConversationAndMessageAtIndex(
|
||||
conversationIndex: number
|
||||
): undefined | { conversationId: string } {
|
||||
const { conversations, pinnedConversations } = this;
|
||||
const conversation =
|
||||
pinnedConversations[conversationIndex] ||
|
||||
conversations[conversationIndex - pinnedConversations.length] ||
|
||||
last(conversations) ||
|
||||
last(pinnedConversations);
|
||||
return conversation ? { conversationId: conversation.id } : undefined;
|
||||
}
|
||||
|
||||
getConversationAndMessageInDirection(
|
||||
toFind: Readonly<ToFindType>,
|
||||
selectedConversationId: undefined | string,
|
||||
_selectedMessageId: unknown
|
||||
): undefined | { conversationId: string } {
|
||||
return getConversationInDirection(
|
||||
[...this.pinnedConversations, ...this.conversations],
|
||||
toFind,
|
||||
selectedConversationId
|
||||
);
|
||||
}
|
||||
|
||||
private hasPinnedAndNonpinned(): boolean {
|
||||
return Boolean(
|
||||
this.pinnedConversations.length && this.conversations.length
|
||||
);
|
||||
}
|
||||
}
|
240
ts/components/leftPane/LeftPaneSearchHelper.tsx
Normal file
240
ts/components/leftPane/LeftPaneSearchHelper.tsx
Normal file
|
@ -0,0 +1,240 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { ReactChild } from 'react';
|
||||
|
||||
import { LeftPaneHelper, ToFindType } from './LeftPaneHelper';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import { Row, RowType } from '../ConversationList';
|
||||
import { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem';
|
||||
|
||||
import { Intl } from '../Intl';
|
||||
import { Emojify } from '../conversation/Emojify';
|
||||
|
||||
type MaybeLoadedSearchResultsType<T> =
|
||||
| { isLoading: true }
|
||||
| { isLoading: false; results: Array<T> };
|
||||
|
||||
export type LeftPaneSearchPropsType = {
|
||||
conversationResults: MaybeLoadedSearchResultsType<
|
||||
ConversationListItemPropsType
|
||||
>;
|
||||
contactResults: MaybeLoadedSearchResultsType<ConversationListItemPropsType>;
|
||||
messageResults: MaybeLoadedSearchResultsType<{
|
||||
id: string;
|
||||
conversationId: string;
|
||||
}>;
|
||||
searchConversationName?: string;
|
||||
searchTerm: string;
|
||||
};
|
||||
|
||||
const searchResultKeys: Array<
|
||||
'conversationResults' | 'contactResults' | 'messageResults'
|
||||
> = ['conversationResults', 'contactResults', 'messageResults'];
|
||||
|
||||
export class LeftPaneSearchHelper extends LeftPaneHelper<
|
||||
LeftPaneSearchPropsType
|
||||
> {
|
||||
private readonly conversationResults: MaybeLoadedSearchResultsType<
|
||||
ConversationListItemPropsType
|
||||
>;
|
||||
|
||||
private readonly contactResults: MaybeLoadedSearchResultsType<
|
||||
ConversationListItemPropsType
|
||||
>;
|
||||
|
||||
private readonly messageResults: MaybeLoadedSearchResultsType<{
|
||||
id: string;
|
||||
conversationId: string;
|
||||
}>;
|
||||
|
||||
private readonly searchConversationName?: string;
|
||||
|
||||
private readonly searchTerm: string;
|
||||
|
||||
constructor({
|
||||
conversationResults,
|
||||
contactResults,
|
||||
messageResults,
|
||||
searchConversationName,
|
||||
searchTerm,
|
||||
}: Readonly<LeftPaneSearchPropsType>) {
|
||||
super();
|
||||
|
||||
this.conversationResults = conversationResults;
|
||||
this.contactResults = contactResults;
|
||||
this.messageResults = messageResults;
|
||||
this.searchConversationName = searchConversationName;
|
||||
this.searchTerm = searchTerm;
|
||||
}
|
||||
|
||||
getPreRowsNode({
|
||||
i18n,
|
||||
}: Readonly<{ i18n: LocalizerType }>): null | ReactChild {
|
||||
const mightHaveSearchResults = this.allResults().some(
|
||||
searchResult => searchResult.isLoading || searchResult.results.length
|
||||
);
|
||||
if (mightHaveSearchResults) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { searchConversationName, searchTerm } = this;
|
||||
|
||||
return !searchConversationName || searchTerm ? (
|
||||
<div
|
||||
// We need this for Ctrl-T shortcut cycling through parts of app
|
||||
tabIndex={-1}
|
||||
className="module-left-pane__no-search-results"
|
||||
key={searchTerm}
|
||||
>
|
||||
{searchConversationName ? (
|
||||
<Intl
|
||||
id="noSearchResultsInConversation"
|
||||
i18n={i18n}
|
||||
components={{
|
||||
searchTerm,
|
||||
conversationName: (
|
||||
<Emojify key="item-1" text={searchConversationName} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
i18n('noSearchResults', [searchTerm])
|
||||
)}
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
|
||||
getRowCount(): number {
|
||||
return this.allResults().reduce(
|
||||
(result: number, searchResults) =>
|
||||
result + getRowCountForSearchResult(searchResults),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
// This is currently unimplemented. See DESKTOP-1170.
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
getRowIndexToScrollTo(
|
||||
_selectedConversationId: undefined | string
|
||||
): undefined | number {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getRow(rowIndex: number): undefined | Row {
|
||||
const { conversationResults, contactResults, messageResults } = this;
|
||||
|
||||
const conversationRowCount = getRowCountForSearchResult(
|
||||
conversationResults
|
||||
);
|
||||
const contactRowCount = getRowCountForSearchResult(contactResults);
|
||||
const messageRowCount = getRowCountForSearchResult(messageResults);
|
||||
|
||||
if (rowIndex < conversationRowCount) {
|
||||
if (rowIndex === 0) {
|
||||
return {
|
||||
type: RowType.Header,
|
||||
i18nKey: 'conversationsHeader',
|
||||
};
|
||||
}
|
||||
if (conversationResults.isLoading) {
|
||||
return { type: RowType.Spinner };
|
||||
}
|
||||
const conversation = conversationResults.results[rowIndex - 1];
|
||||
return conversation
|
||||
? {
|
||||
type: RowType.Conversation,
|
||||
conversation,
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
if (rowIndex < conversationRowCount + contactRowCount) {
|
||||
const localIndex = rowIndex - conversationRowCount;
|
||||
if (localIndex === 0) {
|
||||
return {
|
||||
type: RowType.Header,
|
||||
i18nKey: 'contactsHeader',
|
||||
};
|
||||
}
|
||||
if (contactResults.isLoading) {
|
||||
return { type: RowType.Spinner };
|
||||
}
|
||||
const conversation = contactResults.results[localIndex - 1];
|
||||
return conversation
|
||||
? {
|
||||
type: RowType.Conversation,
|
||||
conversation,
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
if (rowIndex >= conversationRowCount + contactRowCount + messageRowCount) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const localIndex = rowIndex - conversationRowCount - contactRowCount;
|
||||
if (localIndex === 0) {
|
||||
return {
|
||||
type: RowType.Header,
|
||||
i18nKey: 'messagesHeader',
|
||||
};
|
||||
}
|
||||
if (messageResults.isLoading) {
|
||||
return { type: RowType.Spinner };
|
||||
}
|
||||
const message = messageResults.results[localIndex - 1];
|
||||
return message
|
||||
? {
|
||||
type: RowType.MessageSearchResult,
|
||||
messageId: message.id,
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
shouldRecomputeRowHeights(old: Readonly<LeftPaneSearchPropsType>): boolean {
|
||||
return searchResultKeys.some(
|
||||
key =>
|
||||
getRowCountForSearchResult(old[key]) !==
|
||||
getRowCountForSearchResult(this[key])
|
||||
);
|
||||
}
|
||||
|
||||
// This is currently unimplemented. See DESKTOP-1170.
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
getConversationAndMessageAtIndex(
|
||||
_conversationIndex: number
|
||||
): undefined | { conversationId: string; messageId?: string } {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// This is currently unimplemented. See DESKTOP-1170.
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
getConversationAndMessageInDirection(
|
||||
_toFind: Readonly<ToFindType>,
|
||||
_selectedConversationId: undefined | string,
|
||||
_selectedMessageId: unknown
|
||||
): undefined | { conversationId: string } {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private allResults() {
|
||||
return [this.conversationResults, this.contactResults, this.messageResults];
|
||||
}
|
||||
}
|
||||
|
||||
function getRowCountForSearchResult(
|
||||
searchResults: Readonly<MaybeLoadedSearchResultsType<unknown>>
|
||||
): number {
|
||||
let hasHeader: boolean;
|
||||
let resultRows: number;
|
||||
if (searchResults.isLoading) {
|
||||
hasHeader = true;
|
||||
resultRows = 1; // For the spinner.
|
||||
} else {
|
||||
const resultCount = searchResults.results.length;
|
||||
hasHeader = Boolean(resultCount);
|
||||
resultRows = resultCount;
|
||||
}
|
||||
return (hasHeader ? 1 : 0) + resultRows;
|
||||
}
|
63
ts/components/leftPane/getConversationInDirection.ts
Normal file
63
ts/components/leftPane/getConversationInDirection.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { find as findFirst, findLast, first, last } from 'lodash';
|
||||
|
||||
import { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem';
|
||||
import { isConversationUnread } from '../../util/isConversationUnread';
|
||||
import { FindDirection, ToFindType } from './LeftPaneHelper';
|
||||
|
||||
/**
|
||||
* This will look up or down in an array of conversations for the next one to select.
|
||||
* Refer to the tests for the intended behavior.
|
||||
*/
|
||||
export const getConversationInDirection = (
|
||||
conversations: ReadonlyArray<ConversationListItemPropsType>,
|
||||
toFind: Readonly<ToFindType>,
|
||||
selectedConversationId: undefined | string
|
||||
): undefined | { conversationId: string } => {
|
||||
// As an optimization, we don't need to search if no conversation is selected.
|
||||
const selectedConversationIndex = selectedConversationId
|
||||
? conversations.findIndex(({ id }) => id === selectedConversationId)
|
||||
: -1;
|
||||
|
||||
let conversation: ConversationListItemPropsType | undefined;
|
||||
|
||||
if (selectedConversationIndex < 0) {
|
||||
if (toFind.unreadOnly) {
|
||||
conversation =
|
||||
toFind.direction === FindDirection.Up
|
||||
? findLast(conversations, isConversationUnread)
|
||||
: findFirst(conversations, isConversationUnread);
|
||||
} else {
|
||||
conversation =
|
||||
toFind.direction === FindDirection.Up
|
||||
? last(conversations)
|
||||
: first(conversations);
|
||||
}
|
||||
} else if (toFind.unreadOnly) {
|
||||
conversation =
|
||||
toFind.direction === FindDirection.Up
|
||||
? findLast(
|
||||
conversations.slice(0, selectedConversationIndex),
|
||||
isConversationUnread
|
||||
)
|
||||
: findFirst(
|
||||
conversations.slice(selectedConversationIndex + 1),
|
||||
isConversationUnread
|
||||
);
|
||||
} else {
|
||||
const newIndex =
|
||||
selectedConversationIndex +
|
||||
(toFind.direction === FindDirection.Up ? -1 : 1);
|
||||
if (newIndex < 0) {
|
||||
conversation = last(conversations);
|
||||
} else if (newIndex >= conversations.length) {
|
||||
conversation = first(conversations);
|
||||
} else {
|
||||
conversation = conversations[newIndex];
|
||||
}
|
||||
}
|
||||
|
||||
return conversation ? { conversationId: conversation.id } : undefined;
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue