Switched ForwardMessageModal to use ListTile

This commit is contained in:
Alvaro 2023-01-25 16:51:08 -07:00 committed by GitHub
parent 257f5e1231
commit d64e0b65c4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 528 additions and 239 deletions

View file

@ -122,6 +122,8 @@ const rules = {
'react/display-name': 'error', 'react/display-name': 'error',
'react/jsx-pascal-case': ['error', {allowNamespace: true}],
// Allow returning values from promise executors for brevity. // Allow returning values from promise executors for brevity.
'no-promise-executor-return': 'off', 'no-promise-executor-return': 'off',

View file

@ -4405,6 +4405,13 @@ button.module-image__border-overlay:focus {
padding-left: 10px; padding-left: 10px;
padding-right: 10px; padding-right: 10px;
// list tiles in choose-group-members and compose extend to the edge
.module-left-pane--mode-choose-group-members &,
.module-left-pane--mode-compose & {
padding-left: 0;
padding-right: 0;
}
&--width-narrow { &--width-narrow {
padding-left: 10px; padding-left: 10px;
padding-right: 10px; padding-right: 10px;

View file

@ -64,4 +64,9 @@
padding: 0; padding: 0;
} }
} }
.module-conversation-list {
// remove horizontal padding so ListTiles extend to the edges
padding: 0;
}
} }

View file

@ -10,10 +10,10 @@
max-height: 88px; max-height: 88px;
overflow-x: hidden; overflow-x: hidden;
overflow-y: scroll; overflow-y: scroll;
padding-left: 12px; padding: 4px 24px;
gap: 8px 12px;
.module-ContactPill { .module-ContactPill {
margin: 4px 6px;
max-width: calc( max-width: calc(
100% - 15px 100% - 15px
); // 6px for the right margin and 9px for the scrollbar ); // 6px for the right margin and 9px for the scrollbar

View file

@ -22,6 +22,11 @@
color: $color-gray-05; color: $color-gray-05;
} }
.module-conversation-list {
// remove horizontal padding so ListTiles extend to the edges
padding: 0;
}
&--link-preview { &--link-preview {
border-bottom: 1px solid $color-gray-15; border-bottom: 1px solid $color-gray-15;
padding: 12px 16px; padding: 12px 16px;

View file

@ -1,10 +1,11 @@
// Copyright 2022 Signal Messenger, LLC // Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { noop, pick } from 'lodash'; import { pick } from 'lodash';
import React from 'react'; import React, { useCallback } from 'react';
import type { MeasuredComponentProps } from 'react-measure'; import type { MeasuredComponentProps } from 'react-measure';
import Measure from 'react-measure'; import Measure from 'react-measure';
import type { ListRowProps } from 'react-virtualized';
import type { ConversationType } from '../state/ducks/conversations'; import type { ConversationType } from '../state/ducks/conversations';
import type { import type {
@ -15,12 +16,16 @@ import type {
import { ToastType } from '../types/Toast'; import { ToastType } from '../types/Toast';
import { filterAndSortConversationsByRecent } from '../util/filterAndSortConversations'; import { filterAndSortConversationsByRecent } from '../util/filterAndSortConversations';
import { ConfirmationDialog } from './ConfirmationDialog'; import { ConfirmationDialog } from './ConfirmationDialog';
import type { Row } from './ConversationList'; import type { GroupListItemConversationType } from './conversationList/GroupListItem';
import { ConversationList, RowType } from './ConversationList'; import {
import { DisabledReason } from './conversationList/GroupListItem'; DisabledReason,
GroupListItem,
} from './conversationList/GroupListItem';
import { Modal } from './Modal'; import { Modal } from './Modal';
import { SearchInput } from './SearchInput'; import { SearchInput } from './SearchInput';
import { useRestoreFocus } from '../hooks/useRestoreFocus'; import { useRestoreFocus } from '../hooks/useRestoreFocus';
import { ListView } from './ListView';
import { ListTile } from './ListTile';
type OwnProps = { type OwnProps = {
i18n: LocalizerType; i18n: LocalizerType;
@ -47,7 +52,6 @@ export type Props = OwnProps & DispatchProps;
export function AddUserToAnotherGroupModal({ export function AddUserToAnotherGroupModal({
i18n, i18n,
theme,
contact, contact,
toggleAddUserToAnotherGroupModal, toggleAddUserToAnotherGroupModal,
addMembersToGroup, addMembersToGroup,
@ -108,7 +112,7 @@ export function AddUserToAnotherGroupModal({
); );
const handleGetRow = React.useCallback( const handleGetRow = React.useCallback(
(idx: number): Row | undefined => { (idx: number): GroupListItemConversationType => {
const convo = filteredConversations[idx]; const convo = filteredConversations[idx];
// these are always populated in the case of a group // these are always populated in the case of a group
@ -129,18 +133,36 @@ export function AddUserToAnotherGroupModal({
} }
return { return {
type: RowType.SelectSingleGroup, ...pick(convo, 'id', 'avatarPath', 'title', 'unblurredAvatarPath'),
group: { memberships,
...pick(convo, 'id', 'avatarPath', 'title', 'unblurredAvatarPath'), membersCount,
memberships, disabledReason,
membersCount,
disabledReason,
},
}; };
}, },
[filteredConversations, contact] [filteredConversations, contact]
); );
const renderGroupListItem = useCallback(
({ key, index, style }: ListRowProps) => {
const group = handleGetRow(index);
return (
<div key={key} style={style}>
<GroupListItem
i18n={i18n}
group={group}
onSelectGroup={setSelectedGroupId}
/>
</div>
);
},
[i18n, handleGetRow]
);
const handleCalculateRowHeight = useCallback(
() => ListTile.heightCompact,
[]
);
return ( return (
<> <>
{!selectedGroup && ( {!selectedGroup && (
@ -163,30 +185,30 @@ export function AddUserToAnotherGroupModal({
/> />
<Measure bounds> <Measure bounds>
{({ contentRect, measureRef }: MeasuredComponentProps) => ( {({ contentRect, measureRef }: MeasuredComponentProps) => {
<div // Though `width` and `height` are required properties, we want to be
className="AddUserToAnotherGroupModal__list-wrapper" // careful in case the caller sends bogus data. Notably, react-measure's
ref={measureRef} // types seem to be inaccurate.
> const { width = 100, height = 100 } = contentRect.bounds || {};
<ConversationList if (!width || !height) {
dimensions={contentRect.bounds} return null;
getPreferredBadge={() => undefined} }
getRow={handleGetRow}
i18n={i18n} return (
lookupConversationWithoutUuid={async _ => undefined} <div
onClickArchiveButton={noop} className="AddUserToAnotherGroupModal__list-wrapper"
onClickContactCheckbox={noop} ref={measureRef}
onSelectConversation={setSelectedGroupId} >
rowCount={filteredConversations.length} <ListView
setIsFetchingUUID={noop} width={width}
shouldRecomputeRowHeights={false} height={height}
showChooseGroupMembers={noop} rowCount={filteredConversations.length}
showConversation={noop} calculateRowHeight={handleCalculateRowHeight}
showUserNotFoundModal={noop} rowRenderer={renderGroupListItem}
theme={theme} />
/> </div>
</div> );
)} }}
</Measure> </Measure>
</div> </div>
</Modal> </Modal>

View file

@ -215,6 +215,9 @@ export function ConversationList({
case RowType.SearchResultsLoadingFakeHeader: case RowType.SearchResultsLoadingFakeHeader:
return HEADER_ROW_HEIGHT; return HEADER_ROW_HEIGHT;
case RowType.SelectSingleGroup: case RowType.SelectSingleGroup:
case RowType.ContactCheckbox:
case RowType.Contact:
case RowType.CreateNewGroup:
return SELECT_ROW_HEIGHT; return SELECT_ROW_HEIGHT;
default: default:
return NORMAL_ROW_HEIGHT; return NORMAL_ROW_HEIGHT;

View file

@ -38,6 +38,7 @@ import {
shouldNeverBeCalled, shouldNeverBeCalled,
asyncShouldNeverBeCalled, asyncShouldNeverBeCalled,
} from '../util/shouldNeverBeCalled'; } from '../util/shouldNeverBeCalled';
import { Emojify } from './conversation/Emojify';
export type DataPropsType = { export type DataPropsType = {
attachments?: ReadonlyArray<AttachmentType>; attachments?: ReadonlyArray<AttachmentType>;
@ -408,8 +409,13 @@ export function ForwardMessageModal({
)} )}
<div className="module-ForwardMessageModal__footer"> <div className="module-ForwardMessageModal__footer">
<div> <div>
{Boolean(selectedContacts.length) && {Boolean(selectedContacts.length) && (
selectedContacts.map(contact => contact.title).join(', ')} <Emojify
text={selectedContacts
.map(contact => contact.title)
.join(', ')}
/>
)}
</div> </div>
<div> <div>
{isEditingMessage || !isMessageEditable ? ( {isEditingMessage || !isMessageEditable ? (

View file

@ -23,14 +23,18 @@ import { setupI18n } from '../util/setupI18n';
import { DurationInSeconds, DAY } from '../util/durations'; import { DurationInSeconds, DAY } from '../util/durations';
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
import { ThemeType } from '../types/Util'; import { ThemeType } from '../types/Util';
import {
getDefaultConversation,
getDefaultGroupListItem,
} from '../test-both/helpers/getDefaultConversation';
import { DialogType } from '../types/Dialogs'; import { DialogType } from '../types/Dialogs';
import { SocketStatus } from '../types/SocketStatus'; import { SocketStatus } from '../types/SocketStatus';
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
import { StorybookThemeContext } from '../../.storybook/StorybookThemeContext'; import { StorybookThemeContext } from '../../.storybook/StorybookThemeContext';
import { import {
makeFakeLookupConversationWithoutUuid, makeFakeLookupConversationWithoutUuid,
useUuidFetchState, useUuidFetchState,
} from '../test-both/helpers/fakeLookupConversationWithoutUuid'; } from '../test-both/helpers/fakeLookupConversationWithoutUuid';
import type { GroupListItemConversationType } from './conversationList/GroupListItem';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
@ -62,18 +66,14 @@ const defaultSearchProps = {
startSearchCounter: 0, startSearchCounter: 0,
}; };
const defaultGroups: Array<ConversationType> = [ const defaultGroups: Array<GroupListItemConversationType> = [
getDefaultConversation({ getDefaultGroupListItem({
id: 'biking-group', id: 'biking-group',
title: 'Mtn Biking Arizona 🚵☀️⛰', title: 'Mtn Biking Arizona 🚵☀️⛰',
type: 'group',
sharedGroupNames: [],
}), }),
getDefaultConversation({ getDefaultGroupListItem({
id: 'dance-group', id: 'dance-group',
title: 'Are we dancers? 💃', title: 'Are we dancers? 💃',
type: 'group',
sharedGroupNames: [],
}), }),
]; ];

View file

@ -605,7 +605,11 @@ export function LeftPane({
className={classNames( className={classNames(
'module-left-pane', 'module-left-pane',
isResizing && 'module-left-pane--is-resizing', isResizing && 'module-left-pane--is-resizing',
`module-left-pane--width-${widthBreakpoint}` `module-left-pane--width-${widthBreakpoint}`,
modeSpecificProps.mode === LeftPaneMode.ChooseGroupMembers &&
'module-left-pane--mode-choose-group-members',
modeSpecificProps.mode === LeftPaneMode.Compose &&
'module-left-pane--mode-compose'
)} )}
style={{ width }} style={{ width }}
> >

View file

@ -2,8 +2,10 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import classNames from 'classnames'; import classNames from 'classnames';
import React from 'react'; import React, { useMemo } from 'react';
import uuid from 'uuid';
import { getClassNamesFor } from '../util/getClassNamesFor'; import { getClassNamesFor } from '../util/getClassNamesFor';
import { CircleCheckbox } from './CircleCheckbox';
export type Props = { export type Props = {
title: string | JSX.Element; title: string | JSX.Element;
@ -23,6 +25,7 @@ export type Props = {
variant?: 'item' | 'panelrow'; variant?: 'item' | 'panelrow';
// defaults to div // defaults to div
rootElement?: 'div' | 'button'; rootElement?: 'div' | 'button';
testId?: string;
}; };
const getClassName = getClassNamesFor('ListTile'); const getClassName = getClassNamesFor('ListTile');
@ -53,8 +56,17 @@ const getClassName = getClassNamesFor('ListTile');
* - panelrow: more horizontal padding, intended for information rows (usually not in * - panelrow: more horizontal padding, intended for information rows (usually not in
* modals) that tend to occupy more horizontal space * modals) that tend to occupy more horizontal space
*/ */
export const ListTile = React.forwardRef<HTMLButtonElement, Props>( export function ListTile(
function ListTile( params: Props & React.RefAttributes<HTMLButtonElement>
): JSX.Element {
// forwardRef makes it impossible to add extra static fields to the function type so
// we have to create this inner implementation that can be wrapped with a non-arrow
// function. A bit weird, but looks fine at call-site.
return <ListTileImpl {...params} />;
}
const ListTileImpl = React.forwardRef<HTMLButtonElement, Props>(
function ListTileImpl(
{ {
title, title,
subtitle, subtitle,
@ -67,6 +79,7 @@ export const ListTile = React.forwardRef<HTMLButtonElement, Props>(
disabled = false, disabled = false,
variant = 'item', variant = 'item',
rootElement = 'div', rootElement = 'div',
testId,
}: Props, }: Props,
ref ref
) { ) {
@ -81,6 +94,7 @@ export const ListTile = React.forwardRef<HTMLButtonElement, Props>(
onClick, onClick,
'aria-disabled': disabled ? true : undefined, 'aria-disabled': disabled ? true : undefined,
onContextMenu, onContextMenu,
'data-testid': testId,
}; };
const contents = ( const contents = (
@ -112,3 +126,52 @@ export const ListTile = React.forwardRef<HTMLButtonElement, Props>(
); );
} }
); );
// although these heights are not required for ListTile (which sizes itself based on
// content), they are useful as constants for ListView.calculateRowHeight
/** Effective ListTile height for an avatar (leading) size 36 */
ListTile.heightFull = 64;
/** Effective ListTile height for an avatar (leading) size 48 */
ListTile.heightCompact = 52;
/**
* ListTile with a trailing checkbox.
*
* It also wraps the ListTile with a <label> to get typical click-label-to-check behavior
*
* Same API except for:
* - no "trailing" param since it is populated by the checkbox
* - isChecked
*/
ListTile.checkbox = (
props: Omit<Props, 'trailing'> & { isChecked: boolean }
) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const htmlId: string = useMemo(() => uuid(), []);
const { onClick, disabled, isChecked, ...otherProps } = props;
return (
<label
htmlFor={htmlId}
// `onClick` is will double-fire if we're enabled. We want it to fire when we're
// disabled so we can show any "can't add contact" modals, etc. This won't
// work for keyboard users, though, because labels are not tabbable.
{...(disabled ? { onClick } : {})}
>
<ListTile
{...otherProps}
disabled={disabled}
trailing={
<CircleCheckbox
id={htmlId}
checked={isChecked}
onChange={onClick}
disabled={disabled}
/>
}
/>
</label>
);
};

View file

@ -1,6 +1,8 @@
// Copyright 2018 Signal Messenger, LLC // Copyright 2018 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable react/jsx-pascal-case */
import type { ReactNode, RefObject } from 'react'; import type { ReactNode, RefObject } from 'react';
import React from 'react'; import React from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';

View file

@ -1,6 +1,8 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable local-rules/valid-i18n-keys */
import React, { import React, {
useEffect, useEffect,
useMemo, useMemo,
@ -8,13 +10,14 @@ import React, {
useRef, useRef,
useCallback, useCallback,
} from 'react'; } from 'react';
import { omit } from 'lodash'; import { noop, omit } from 'lodash';
import type { MeasuredComponentProps } from 'react-measure'; import type { MeasuredComponentProps } from 'react-measure';
import Measure from 'react-measure'; import Measure from 'react-measure';
import type { ListRowProps } from 'react-virtualized';
import type { LocalizerType, ThemeType } from '../../../../types/Util'; import type { LocalizerType, ThemeType } from '../../../../types/Util';
import { getUsernameFromSearch } from '../../../../types/Username'; import { getUsernameFromSearch } from '../../../../types/Username';
import { strictAssert } from '../../../../util/assert'; import { strictAssert, assertDev } from '../../../../util/assert';
import { refMerger } from '../../../../util/refMerger'; import { refMerger } from '../../../../util/refMerger';
import { useRestoreFocus } from '../../../../hooks/useRestoreFocus'; import { useRestoreFocus } from '../../../../hooks/useRestoreFocus';
import { missingCaseError } from '../../../../util/missingCaseError'; import { missingCaseError } from '../../../../util/missingCaseError';
@ -36,11 +39,16 @@ import { ModalHost } from '../../../ModalHost';
import { ContactPills } from '../../../ContactPills'; import { ContactPills } from '../../../ContactPills';
import { ContactPill } from '../../../ContactPill'; import { ContactPill } from '../../../ContactPill';
import type { Row } from '../../../ConversationList'; import type { Row } from '../../../ConversationList';
import { ConversationList, RowType } from '../../../ConversationList'; import { RowType } from '../../../ConversationList';
import { ContactCheckboxDisabledReason } from '../../../conversationList/ContactCheckbox'; import {
ContactCheckbox,
ContactCheckboxDisabledReason,
} from '../../../conversationList/ContactCheckbox';
import { Button, ButtonVariant } from '../../../Button'; import { Button, ButtonVariant } from '../../../Button';
import { SearchInput } from '../../../SearchInput'; import { SearchInput } from '../../../SearchInput';
import { shouldNeverBeCalled } from '../../../../util/shouldNeverBeCalled'; import { ListView } from '../../../ListView';
import { UsernameCheckbox } from '../../../conversationList/UsernameCheckbox';
import { PhoneNumberCheckbox } from '../../../conversationList/PhoneNumberCheckbox';
export type StatePropsType = { export type StatePropsType = {
regionCode: string | undefined; regionCode: string | undefined;
@ -77,7 +85,6 @@ export function ChooseGroupMembersModal({
candidateContacts, candidateContacts,
confirmAdds, confirmAdds,
conversationIdsAlreadyInGroup, conversationIdsAlreadyInGroup,
getPreferredBadge,
i18n, i18n,
maxGroupSize, maxGroupSize,
onClose, onClose,
@ -278,6 +285,94 @@ export function ChooseGroupMembersModal({
return undefined; return undefined;
}; };
const handleContactClick = (
conversationId: string,
disabledReason: undefined | ContactCheckboxDisabledReason
) => {
switch (disabledReason) {
case undefined:
toggleSelectedContact(conversationId);
break;
case ContactCheckboxDisabledReason.AlreadyAdded:
case ContactCheckboxDisabledReason.MaximumContactsSelected:
// These are no-ops.
break;
default:
throw missingCaseError(disabledReason);
}
};
const renderItem = ({ key, index, style }: ListRowProps) => {
const row = getRow(index);
let item;
switch (row?.type) {
case RowType.Header:
item = (
<div
className="module-conversation-list__item--header"
aria-label={i18n(row.i18nKey)}
>
{i18n(row.i18nKey)}
</div>
);
break;
case RowType.ContactCheckbox:
item = (
<ContactCheckbox
i18n={i18n}
theme={theme}
{...row.contact}
onClick={handleContactClick}
isChecked={row.isChecked}
badge={undefined}
/>
);
break;
case RowType.UsernameCheckbox:
item = (
<UsernameCheckbox
i18n={i18n}
theme={theme}
username={row.username}
isChecked={row.isChecked}
isFetching={row.isFetching}
toggleConversationInChooseMembers={conversationId =>
handleContactClick(conversationId, undefined)
}
showUserNotFoundModal={noop}
setIsFetchingUUID={setIsFetchingUUID}
lookupConversationWithoutUuid={() => Promise.resolve(undefined)}
/>
);
break;
case RowType.PhoneNumberCheckbox:
item = (
<PhoneNumberCheckbox
phoneNumber={row.phoneNumber}
lookupConversationWithoutUuid={lookupConversationWithoutUuid}
showUserNotFoundModal={showUserNotFoundModal}
setIsFetchingUUID={setIsFetchingUUID}
toggleConversationInChooseMembers={conversationId =>
handleContactClick(conversationId, undefined)
}
isChecked={row.isChecked}
isFetching={row.isFetching}
i18n={i18n}
theme={theme}
/>
);
break;
default:
}
return (
<div key={key} style={style}>
{item}
</div>
);
};
return ( return (
<ModalHost <ModalHost
modalName="AddGroupMembersModal.ChooseGroupMembersModal" modalName="AddGroupMembersModal.ChooseGroupMembersModal"
@ -335,6 +430,14 @@ export function ChooseGroupMembersModal({
{rowCount ? ( {rowCount ? (
<Measure bounds> <Measure bounds>
{({ contentRect, measureRef }: MeasuredComponentProps) => { {({ contentRect, measureRef }: MeasuredComponentProps) => {
// Though `width` and `height` are required properties, we want to be
// careful in case the caller sends bogus data. Notably, react-measure's
// types seem to be inaccurate.
const { width = 100, height = 100 } = contentRect.bounds || {};
if (!width || !height) {
return null;
}
// We disable this ESLint rule because we're capturing a bubbled keydown // We disable this ESLint rule because we're capturing a bubbled keydown
// event. See [this note in the jsx-a11y docs][0]. // event. See [this note in the jsx-a11y docs][0].
// //
@ -350,43 +453,25 @@ export function ChooseGroupMembersModal({
} }
}} }}
> >
<ConversationList <ListView
dimensions={contentRect.bounds} width={width}
getPreferredBadge={getPreferredBadge} height={height}
getRow={getRow} rowCount={rowCount}
i18n={i18n} calculateRowHeight={index => {
onClickArchiveButton={shouldNeverBeCalled} const row = getRow(index);
onClickContactCheckbox={( if (!row) {
conversationId: string, assertDev(false, `Expected a row at index ${index}`);
disabledReason: undefined | ContactCheckboxDisabledReason return 52;
) => { }
switch (disabledReason) {
case undefined: switch (row.type) {
toggleSelectedContact(conversationId); case RowType.Header:
break; return 40;
case ContactCheckboxDisabledReason.AlreadyAdded:
case ContactCheckboxDisabledReason.MaximumContactsSelected:
// These are no-ops.
break;
default: default:
throw missingCaseError(disabledReason); return 52;
} }
}} }}
lookupConversationWithoutUuid={ rowRenderer={renderItem}
lookupConversationWithoutUuid
}
showUserNotFoundModal={showUserNotFoundModal}
setIsFetchingUUID={setIsFetchingUUID}
showConversation={shouldNeverBeCalled}
onSelectConversation={shouldNeverBeCalled}
renderMessageSearchResult={() => {
shouldNeverBeCalled();
return <div />;
}}
rowCount={rowCount}
shouldRecomputeRowHeights={false}
showChooseGroupMembers={shouldNeverBeCalled}
theme={theme}
/> />
</div> </div>
); );

View file

@ -30,7 +30,7 @@ const MESSAGE_CLASS_NAME = `${CONTENT_CLASS_NAME}__message`;
export const MESSAGE_TEXT_CLASS_NAME = `${MESSAGE_CLASS_NAME}__text`; export const MESSAGE_TEXT_CLASS_NAME = `${MESSAGE_CLASS_NAME}__text`;
const CHECKBOX_CONTAINER_CLASS_NAME = `${BASE_CLASS_NAME}__checkbox--container`; const CHECKBOX_CONTAINER_CLASS_NAME = `${BASE_CLASS_NAME}__checkbox--container`;
const CHECKBOX_CLASS_NAME = `${BASE_CLASS_NAME}__checkbox`; const CHECKBOX_CLASS_NAME = `${BASE_CLASS_NAME}__checkbox`;
const SPINNER_CLASS_NAME = `${BASE_CLASS_NAME}__spinner`; export const SPINNER_CLASS_NAME = `${BASE_CLASS_NAME}__spinner`;
type PropsType = { type PropsType = {
checked?: boolean; checked?: boolean;

View file

@ -1,18 +1,17 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { FunctionComponent, ReactNode } from 'react';
import React from 'react'; import React from 'react';
import type { FunctionComponent } from 'react';
import { import { HEADER_CONTACT_NAME_CLASS_NAME } from './BaseConversationListItem';
BaseConversationListItem,
HEADER_CONTACT_NAME_CLASS_NAME,
} from './BaseConversationListItem';
import type { ConversationType } from '../../state/ducks/conversations'; import type { ConversationType } from '../../state/ducks/conversations';
import type { BadgeType } from '../../badges/types'; import type { BadgeType } from '../../badges/types';
import type { LocalizerType, ThemeType } from '../../types/Util'; import type { LocalizerType, ThemeType } from '../../types/Util';
import { ContactName } from '../conversation/ContactName'; import { ContactName } from '../conversation/ContactName';
import { About } from '../conversation/About'; import { About } from '../conversation/About';
import { ListTile } from '../ListTile';
import { Avatar, AvatarSize } from '../Avatar';
export enum ContactCheckboxDisabledReason { export enum ContactCheckboxDisabledReason {
// We start the enum at 1 because the default starting value of 0 is falsy. // We start the enum at 1 because the default starting value of 0 is falsy.
@ -61,7 +60,6 @@ export const ContactCheckbox: FunctionComponent<PropsType> = React.memo(
badge, badge,
color, color,
disabledReason, disabledReason,
groupId,
i18n, i18n,
id, id,
isChecked, isChecked,
@ -74,7 +72,6 @@ export const ContactCheckbox: FunctionComponent<PropsType> = React.memo(
title, title,
type, type,
unblurredAvatarPath, unblurredAvatarPath,
uuid,
}) { }) {
const disabled = Boolean(disabledReason); const disabled = Boolean(disabledReason);
@ -86,13 +83,11 @@ export const ContactCheckbox: FunctionComponent<PropsType> = React.memo(
<ContactName module={HEADER_CONTACT_NAME_CLASS_NAME} title={title} /> <ContactName module={HEADER_CONTACT_NAME_CLASS_NAME} title={title} />
); );
let messageText: ReactNode; let messageText: undefined | string | JSX.Element;
if (disabledReason === ContactCheckboxDisabledReason.AlreadyAdded) { if (disabledReason === ContactCheckboxDisabledReason.AlreadyAdded) {
messageText = i18n('alreadyAMember'); messageText = i18n('alreadyAMember');
} else if (about) { } else if (about) {
messageText = <About className="" text={about} />; messageText = <About className="" text={about} />;
} else {
messageText = null;
} }
const onClickItem = () => { const onClickItem = () => {
@ -100,29 +95,33 @@ export const ContactCheckbox: FunctionComponent<PropsType> = React.memo(
}; };
return ( return (
<BaseConversationListItem <ListTile.checkbox
acceptedMessageRequest={acceptedMessageRequest} clickable
avatarPath={avatarPath}
badge={badge}
checked={isChecked}
color={color}
conversationType={type}
disabled={disabled} disabled={disabled}
groupId={groupId} isChecked={isChecked}
headerName={headerName} leading={
i18n={i18n} <Avatar
id={id} acceptedMessageRequest={acceptedMessageRequest}
isMe={isMe} avatarPath={avatarPath}
isSelected={false} color={color}
messageText={messageText} conversationType={type}
noteToSelf={Boolean(isMe)}
i18n={i18n}
isMe={isMe}
phoneNumber={phoneNumber}
profileName={profileName}
title={title}
sharedGroupNames={sharedGroupNames}
size={AvatarSize.THIRTY_TWO}
unblurredAvatarPath={unblurredAvatarPath}
// appease the type checker.
{...(badge ? { badge, theme } : { badge: undefined })}
/>
}
title={headerName}
subtitle={isMe ? undefined : messageText}
subtitleMaxLines={1}
onClick={onClickItem} onClick={onClickItem}
phoneNumber={phoneNumber}
profileName={profileName}
sharedGroupNames={sharedGroupNames}
theme={theme}
title={title}
unblurredAvatarPath={unblurredAvatarPath}
uuid={uuid}
/> />
); );
} }

View file

@ -4,15 +4,14 @@
import type { FunctionComponent } from 'react'; import type { FunctionComponent } from 'react';
import React from 'react'; import React from 'react';
import { import { HEADER_CONTACT_NAME_CLASS_NAME } from './BaseConversationListItem';
BaseConversationListItem,
HEADER_CONTACT_NAME_CLASS_NAME,
} from './BaseConversationListItem';
import type { ConversationType } from '../../state/ducks/conversations'; import type { ConversationType } from '../../state/ducks/conversations';
import type { BadgeType } from '../../badges/types'; import type { BadgeType } from '../../badges/types';
import type { LocalizerType, ThemeType } from '../../types/Util'; import type { LocalizerType, ThemeType } from '../../types/Util';
import { ContactName } from '../conversation/ContactName'; import { ContactName } from '../conversation/ContactName';
import { About } from '../conversation/About'; import { About } from '../conversation/About';
import { ListTile } from '../ListTile';
import { Avatar, AvatarSize } from '../Avatar';
import { isSignalConversation } from '../../util/isSignalConversation'; import { isSignalConversation } from '../../util/isSignalConversation';
export type ContactListItemConversationType = Pick< export type ContactListItemConversationType = Pick<
@ -55,7 +54,6 @@ export const ContactListItem: FunctionComponent<PropsType> = React.memo(
avatarPath, avatarPath,
badge, badge,
color, color,
groupId,
i18n, i18n,
id, id,
isMe, isMe,
@ -82,30 +80,33 @@ export const ContactListItem: FunctionComponent<PropsType> = React.memo(
); );
const messageText = const messageText =
about && !isMe ? <About className="" text={about} /> : null; about && !isMe ? <About className="" text={about} /> : undefined;
return ( return (
<BaseConversationListItem <ListTile
acceptedMessageRequest={acceptedMessageRequest} leading={
avatarPath={avatarPath} <Avatar
badge={badge} acceptedMessageRequest={acceptedMessageRequest}
color={color} avatarPath={avatarPath}
conversationType={type} color={color}
groupId={groupId} conversationType={type}
headerName={headerName} noteToSelf={Boolean(isMe)}
i18n={i18n} i18n={i18n}
id={id} isMe={isMe}
isMe={isMe} phoneNumber={phoneNumber}
isSelected={false} profileName={profileName}
messageText={messageText} title={title}
sharedGroupNames={sharedGroupNames}
size={AvatarSize.THIRTY_TWO}
unblurredAvatarPath={unblurredAvatarPath}
// This is here to appease the type checker.
{...(badge ? { badge, theme } : { badge: undefined })}
/>
}
title={headerName}
subtitle={messageText}
subtitleMaxLines={1}
onClick={onClick ? () => onClick(id) : undefined} onClick={onClick ? () => onClick(id) : undefined}
phoneNumber={phoneNumber}
profileName={profileName}
sharedGroupNames={sharedGroupNames}
theme={theme}
title={title}
unblurredAvatarPath={unblurredAvatarPath}
uuid={uuid}
/> />
); );
} }

View file

@ -4,8 +4,9 @@
import type { FunctionComponent } from 'react'; import type { FunctionComponent } from 'react';
import React from 'react'; import React from 'react';
import { BaseConversationListItem } from './BaseConversationListItem';
import type { LocalizerType } from '../../types/Util'; import type { LocalizerType } from '../../types/Util';
import { ListTile } from '../ListTile';
import { Avatar, AvatarSize } from '../Avatar';
type PropsType = { type PropsType = {
i18n: LocalizerType; i18n: LocalizerType;
@ -17,17 +18,22 @@ export const CreateNewGroupButton: FunctionComponent<PropsType> = React.memo(
const title = i18n('createNewGroupButton'); const title = i18n('createNewGroupButton');
return ( return (
<BaseConversationListItem <ListTile
acceptedMessageRequest={false}
conversationType="group"
headerName={title}
i18n={i18n}
isMe={false}
isSelected={false}
onClick={onClick}
sharedGroupNames={[]}
testId="CreateNewGroupButton" testId="CreateNewGroupButton"
leading={
<Avatar
acceptedMessageRequest={false}
conversationType="group"
i18n={i18n}
isMe={false}
title={title}
sharedGroupNames={[]}
size={AvatarSize.THIRTY_TWO}
badge={undefined}
/>
}
title={title} title={title}
onClick={onClick}
/> />
); );
} }

View file

@ -5,8 +5,9 @@ import React from 'react';
import type { ConversationType } from '../../state/ducks/conversations'; import type { ConversationType } from '../../state/ducks/conversations';
import type { LocalizerType } from '../../types/Util'; import type { LocalizerType } from '../../types/Util';
import type { UUIDStringType } from '../../types/UUID'; import type { UUIDStringType } from '../../types/UUID';
import { AvatarSize } from '../Avatar'; import { Avatar, AvatarSize } from '../Avatar';
import { BaseConversationListItem } from './BaseConversationListItem'; import { Emojify } from '../conversation/Emojify';
import { ListTile } from '../ListTile';
export enum DisabledReason { export enum DisabledReason {
AlreadyMember = 'already-member', AlreadyMember = 'already-member',
@ -49,21 +50,25 @@ export function GroupListItem({
count: group.membersCount, count: group.membersCount,
}); });
} }
return ( return (
<BaseConversationListItem <ListTile
disabled={group.disabledReason !== undefined} leading={
conversationType="group" <Avatar
title={group.title} acceptedMessageRequest
avatarSize={AvatarSize.THIRTY_TWO} avatarPath={group.avatarPath}
avatarPath={group.avatarPath} conversationType="group"
acceptedMessageRequest i18n={i18n}
isMe={false} isMe={false}
sharedGroupNames={[]} title={group.title}
headerName={group.title} sharedGroupNames={[]}
i18n={i18n} size={AvatarSize.THIRTY_TWO}
isSelected={false} badge={undefined}
/>
}
title={<Emojify text={group.title} />}
subtitle={<Emojify text={messageText} />}
onClick={() => onSelectGroup(group.id)} onClick={() => onSelectGroup(group.id)}
messageText={messageText}
/> />
); );
} }

View file

@ -6,11 +6,15 @@ import React, { useState } from 'react';
import { ButtonVariant } from '../Button'; import { ButtonVariant } from '../Button';
import { ConfirmationDialog } from '../ConfirmationDialog'; import { ConfirmationDialog } from '../ConfirmationDialog';
import { BaseConversationListItem } from './BaseConversationListItem'; import { SPINNER_CLASS_NAME } from './BaseConversationListItem';
import type { ParsedE164Type } from '../../util/libphonenumberInstance'; import type { ParsedE164Type } from '../../util/libphonenumberInstance';
import type { LocalizerType, ThemeType } from '../../types/Util'; import type { LocalizerType, ThemeType } from '../../types/Util';
import { AvatarColors } from '../../types/Colors'; import { AvatarColors } from '../../types/Colors';
import type { LookupConversationWithoutUuidActionsType } from '../../util/lookupConversationWithoutUuid'; import type { LookupConversationWithoutUuidActionsType } from '../../util/lookupConversationWithoutUuid';
import { ListTile } from '../ListTile';
import { Emojify } from '../conversation/Emojify';
import { Avatar, AvatarSize } from '../Avatar';
import { Spinner } from '../Spinner';
export type PropsDataType = { export type PropsDataType = {
phoneNumber: ParsedE164Type; phoneNumber: ParsedE164Type;
@ -31,7 +35,6 @@ export const PhoneNumberCheckbox: FunctionComponent<PropsType> = React.memo(
phoneNumber, phoneNumber,
isChecked, isChecked,
isFetching, isFetching,
theme,
i18n, i18n,
lookupConversationWithoutUuid, lookupConversationWithoutUuid,
showUserNotFoundModal, showUserNotFoundModal,
@ -88,24 +91,46 @@ export const PhoneNumberCheckbox: FunctionComponent<PropsType> = React.memo(
); );
} }
const avatar = (
<Avatar
acceptedMessageRequest={false}
color={AvatarColors[0]}
conversationType="direct"
i18n={i18n}
isMe={false}
phoneNumber={phoneNumber.userInput}
title={phoneNumber.userInput}
sharedGroupNames={[]}
size={AvatarSize.THIRTY_TWO}
badge={undefined}
/>
);
const title = <Emojify text={phoneNumber.userInput} />;
return ( return (
<> <>
<BaseConversationListItem {isFetching ? (
acceptedMessageRequest={false} <ListTile
checked={isChecked} leading={avatar}
color={AvatarColors[0]} title={title}
conversationType="direct" trailing={
headerName={phoneNumber.userInput} <Spinner
i18n={i18n} size="20px"
isMe={false} svgSize="small"
isSelected={false} moduleClassName={SPINNER_CLASS_NAME}
onClick={onClickItem} direction="on-progress-dialog"
phoneNumber={phoneNumber.userInput} />
shouldShowSpinner={isFetching} }
theme={theme} />
sharedGroupNames={[]} ) : (
title={phoneNumber.userInput} <ListTile.checkbox
/> isChecked={isChecked}
onClick={onClickItem}
leading={avatar}
title={<Emojify text={phoneNumber.userInput} />}
/>
)}
{modal} {modal}
</> </>
); );

View file

@ -1,13 +1,16 @@
// Copyright 2022 Signal Messenger, LLC // Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { FunctionComponent } from 'react';
import React from 'react'; import React from 'react';
import type { FunctionComponent } from 'react';
import { BaseConversationListItem } from './BaseConversationListItem';
import type { LocalizerType, ThemeType } from '../../types/Util'; import type { LocalizerType, ThemeType } from '../../types/Util';
import { AvatarColors } from '../../types/Colors'; import { AvatarColors } from '../../types/Colors';
import type { LookupConversationWithoutUuidActionsType } from '../../util/lookupConversationWithoutUuid'; import type { LookupConversationWithoutUuidActionsType } from '../../util/lookupConversationWithoutUuid';
import { ListTile } from '../ListTile';
import { Avatar, AvatarSize } from '../Avatar';
import { Spinner } from '../Spinner';
import { SPINNER_CLASS_NAME } from './BaseConversationListItem';
export type PropsDataType = { export type PropsDataType = {
username: string; username: string;
@ -28,7 +31,6 @@ export const UsernameCheckbox: FunctionComponent<PropsType> = React.memo(
username, username,
isChecked, isChecked,
isFetching, isFetching,
theme,
i18n, i18n,
lookupConversationWithoutUuid, lookupConversationWithoutUuid,
showUserNotFoundModal, showUserNotFoundModal,
@ -62,22 +64,41 @@ export const UsernameCheckbox: FunctionComponent<PropsType> = React.memo(
const title = i18n('at-username', { username }); const title = i18n('at-username', { username });
return ( const avatar = (
<BaseConversationListItem <Avatar
acceptedMessageRequest={false} acceptedMessageRequest={false}
checked={isChecked}
color={AvatarColors[0]} color={AvatarColors[0]}
conversationType="direct" conversationType="direct"
headerName={title} searchResult
i18n={i18n} i18n={i18n}
isMe={false} isMe={false}
isSelected={false}
isUsernameSearchResult
onClick={onClickItem}
shouldShowSpinner={isFetching}
theme={theme}
sharedGroupNames={[]}
title={title} title={title}
sharedGroupNames={[]}
size={AvatarSize.THIRTY_TWO}
badge={undefined}
/>
);
return isFetching ? (
<ListTile
leading={avatar}
title={title}
trailing={
<Spinner
size="20px"
svgSize="small"
moduleClassName={SPINNER_CLASS_NAME}
direction="on-progress-dialog"
/>
}
/>
) : (
<ListTile.checkbox
leading={avatar}
title={title}
isChecked={isChecked}
onClick={onClickItem}
clickable
/> />
); );
} }

View file

@ -8,7 +8,6 @@ import { LeftPaneHelper } from './LeftPaneHelper';
import type { Row } from '../ConversationList'; import type { Row } from '../ConversationList';
import { RowType } from '../ConversationList'; import { RowType } from '../ConversationList';
import type { ContactListItemConversationType } from '../conversationList/ContactListItem'; import type { ContactListItemConversationType } from '../conversationList/ContactListItem';
import type { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem';
import { SearchInput } from '../SearchInput'; import { SearchInput } from '../SearchInput';
import type { LocalizerType } from '../../types/Util'; import type { LocalizerType } from '../../types/Util';
import type { ParsedE164Type } from '../../util/libphonenumberInstance'; import type { ParsedE164Type } from '../../util/libphonenumberInstance';
@ -20,10 +19,11 @@ import {
isFetchingByUsername, isFetchingByUsername,
isFetchingByE164, isFetchingByE164,
} from '../../util/uuidFetchState'; } from '../../util/uuidFetchState';
import type { GroupListItemConversationType } from '../conversationList/GroupListItem';
export type LeftPaneComposePropsType = { export type LeftPaneComposePropsType = {
composeContacts: ReadonlyArray<ContactListItemConversationType>; composeContacts: ReadonlyArray<ContactListItemConversationType>;
composeGroups: ReadonlyArray<ConversationListItemPropsType>; composeGroups: ReadonlyArray<GroupListItemConversationType>;
regionCode: string | undefined; regionCode: string | undefined;
searchTerm: string; searchTerm: string;
@ -39,7 +39,7 @@ enum TopButton {
export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsType> { export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsType> {
private readonly composeContacts: ReadonlyArray<ContactListItemConversationType>; private readonly composeContacts: ReadonlyArray<ContactListItemConversationType>;
private readonly composeGroups: ReadonlyArray<ConversationListItemPropsType>; private readonly composeGroups: ReadonlyArray<GroupListItemConversationType>;
private readonly uuidFetchState: UUIDFetchStateType; private readonly uuidFetchState: UUIDFetchStateType;
@ -224,8 +224,8 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
const group = this.composeGroups[virtualRowIndex]; const group = this.composeGroups[virtualRowIndex];
if (group) { if (group) {
return { return {
type: RowType.Conversation, type: RowType.SelectSingleGroup,
conversation: group, group,
}; };
} }

View file

@ -613,8 +613,25 @@ export const getFilteredComposeGroups = createSelector(
searchTerm: string, searchTerm: string,
groups: ReadonlyArray<ConversationType>, groups: ReadonlyArray<ConversationType>,
regionCode: string | undefined regionCode: string | undefined
): Array<ConversationType> => { ): Array<
return filterAndSortConversationsByRecent(groups, searchTerm, regionCode); ConversationType & {
membersCount: number;
disabledReason: undefined;
memberships: ReadonlyArray<unknown>;
}
> => {
return filterAndSortConversationsByRecent(
groups,
searchTerm,
regionCode
).map(group => ({
...group,
// we don't disable groups when composing, already filtered
disabledReason: undefined,
// should always be populated for a group
membersCount: group.membersCount ?? 0,
memberships: group.memberships ?? [],
}));
} }
); );

View file

@ -4,8 +4,9 @@
import casual from 'casual'; import casual from 'casual';
import { sample } from 'lodash'; import { sample } from 'lodash';
import type { ConversationType } from '../../state/ducks/conversations'; import type { ConversationType } from '../../state/ducks/conversations';
import { UUID } from '../../types/UUID';
import type { UUIDStringType } from '../../types/UUID'; import type { UUIDStringType } from '../../types/UUID';
import { UUID } from '../../types/UUID';
import type { GroupListItemConversationType } from '../../components/conversationList/GroupListItem';
import { getRandomColor } from './getRandomColor'; import { getRandomColor } from './getRandomColor';
import { ConversationColors } from '../../types/Colors'; import { ConversationColors } from '../../types/Colors';
import { StorySendMode } from '../../types/Stories'; import { StorySendMode } from '../../types/Stories';
@ -46,6 +47,18 @@ export function getDefaultConversation(
}; };
} }
export function getDefaultGroupListItem(
overrideProps: Partial<GroupListItemConversationType> = {}
): GroupListItemConversationType {
return {
...getDefaultGroup(),
disabledReason: undefined,
membersCount: 24,
memberships: [],
...overrideProps,
};
}
export function getDefaultGroup( export function getDefaultGroup(
overrideProps: Partial<ConversationType> = {} overrideProps: Partial<ConversationType> = {}
): ConversationType { ): ConversationType {

View file

@ -113,9 +113,7 @@ describe('pnp/send gv2 invite', function needsName() {
.locator('.module-left-pane__compose-search-form__input') .locator('.module-left-pane__compose-search-form__input')
.fill('ACI'); .fill('ACI');
await leftPane await leftPane.locator('.ListTile >> "ACI Contact"').click();
.locator(`[data-testid="${aciContact.toContact().uuid}"]`)
.click();
debug('inviting PNI member'); debug('inviting PNI member');
@ -123,11 +121,7 @@ describe('pnp/send gv2 invite', function needsName() {
.locator('.module-left-pane__compose-search-form__input') .locator('.module-left-pane__compose-search-form__input')
.fill('PNI'); .fill('PNI');
await leftPane await leftPane.locator('.ListTile >> "PNI Contact"').click();
.locator(
`[data-testid="${pniContact.device.getUUIDByKind(UUIDKind.PNI)}"]`
)
.click();
await leftPane await leftPane
.locator('.module-left-pane__footer button >> "Next"') .locator('.module-left-pane__footer button >> "Next"')

View file

@ -5,7 +5,10 @@ import { assert } from 'chai';
import * as sinon from 'sinon'; import * as sinon from 'sinon';
import { RowType } from '../../../components/ConversationList'; import { RowType } from '../../../components/ConversationList';
import { FindDirection } from '../../../components/leftPane/LeftPaneHelper'; import { FindDirection } from '../../../components/leftPane/LeftPaneHelper';
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation'; import {
getDefaultConversation,
getDefaultGroupListItem,
} from '../../../test-both/helpers/getDefaultConversation';
import { LeftPaneComposeHelper } from '../../../components/leftPane/LeftPaneComposeHelper'; import { LeftPaneComposeHelper } from '../../../components/leftPane/LeftPaneComposeHelper';
@ -69,7 +72,7 @@ describe('LeftPaneComposeHelper', () => {
assert.strictEqual( assert.strictEqual(
new LeftPaneComposeHelper({ new LeftPaneComposeHelper({
composeContacts: [getDefaultConversation(), getDefaultConversation()], composeContacts: [getDefaultConversation(), getDefaultConversation()],
composeGroups: [getDefaultConversation(), getDefaultConversation()], composeGroups: [getDefaultGroupListItem(), getDefaultGroupListItem()],
regionCode: 'US', regionCode: 'US',
searchTerm: '', searchTerm: '',
isUsernamesEnabled: true, isUsernamesEnabled: true,
@ -83,7 +86,7 @@ describe('LeftPaneComposeHelper', () => {
assert.strictEqual( assert.strictEqual(
new LeftPaneComposeHelper({ new LeftPaneComposeHelper({
composeContacts: [getDefaultConversation(), getDefaultConversation()], composeContacts: [getDefaultConversation(), getDefaultConversation()],
composeGroups: [getDefaultConversation(), getDefaultConversation()], composeGroups: [getDefaultGroupListItem(), getDefaultGroupListItem()],
regionCode: 'US', regionCode: 'US',
searchTerm: 'someone', searchTerm: 'someone',
isUsernamesEnabled: true, isUsernamesEnabled: true,
@ -97,7 +100,7 @@ describe('LeftPaneComposeHelper', () => {
assert.strictEqual( assert.strictEqual(
new LeftPaneComposeHelper({ new LeftPaneComposeHelper({
composeContacts: [getDefaultConversation(), getDefaultConversation()], composeContacts: [getDefaultConversation(), getDefaultConversation()],
composeGroups: [getDefaultConversation(), getDefaultConversation()], composeGroups: [getDefaultGroupListItem(), getDefaultGroupListItem()],
regionCode: 'US', regionCode: 'US',
searchTerm: 'someone', searchTerm: 'someone',
isUsernamesEnabled: false, isUsernamesEnabled: false,
@ -133,7 +136,7 @@ describe('LeftPaneComposeHelper', () => {
assert.strictEqual( assert.strictEqual(
new LeftPaneComposeHelper({ new LeftPaneComposeHelper({
composeContacts: [getDefaultConversation(), getDefaultConversation()], composeContacts: [getDefaultConversation(), getDefaultConversation()],
composeGroups: [getDefaultConversation()], composeGroups: [getDefaultGroupListItem()],
regionCode: 'US', regionCode: 'US',
searchTerm: 'foobar', searchTerm: 'foobar',
isUsernamesEnabled: true, isUsernamesEnabled: true,
@ -240,8 +243,8 @@ describe('LeftPaneComposeHelper', () => {
getDefaultConversation(), getDefaultConversation(),
]; ];
const composeGroups = [ const composeGroups = [
getDefaultConversation(), getDefaultGroupListItem(),
getDefaultConversation(), getDefaultGroupListItem(),
]; ];
const helper = new LeftPaneComposeHelper({ const helper = new LeftPaneComposeHelper({
composeContacts, composeContacts,
@ -255,6 +258,7 @@ describe('LeftPaneComposeHelper', () => {
assert.deepEqual(helper.getRow(0), { assert.deepEqual(helper.getRow(0), {
type: RowType.CreateNewGroup, type: RowType.CreateNewGroup,
}); });
assert.deepEqual(helper.getRow(1), { assert.deepEqual(helper.getRow(1), {
type: RowType.Header, type: RowType.Header,
i18nKey: 'contactsHeader', i18nKey: 'contactsHeader',
@ -272,12 +276,12 @@ describe('LeftPaneComposeHelper', () => {
i18nKey: 'groupsHeader', i18nKey: 'groupsHeader',
}); });
assert.deepEqual(helper.getRow(5), { assert.deepEqual(helper.getRow(5), {
type: RowType.Conversation, type: RowType.SelectSingleGroup,
conversation: composeGroups[0], group: composeGroups[0],
}); });
assert.deepEqual(helper.getRow(6), { assert.deepEqual(helper.getRow(6), {
type: RowType.Conversation, type: RowType.SelectSingleGroup,
conversation: composeGroups[1], group: composeGroups[1],
}); });
}); });
@ -571,7 +575,7 @@ describe('LeftPaneComposeHelper', () => {
assert.isTrue( assert.isTrue(
helperContacts.shouldRecomputeRowHeights({ helperContacts.shouldRecomputeRowHeights({
composeContacts: [], composeContacts: [],
composeGroups: [getDefaultConversation(), getDefaultConversation()], composeGroups: [getDefaultGroupListItem(), getDefaultGroupListItem()],
regionCode: 'US', regionCode: 'US',
searchTerm: 'foo bar', searchTerm: 'foo bar',
isUsernamesEnabled: true, isUsernamesEnabled: true,
@ -581,7 +585,7 @@ describe('LeftPaneComposeHelper', () => {
const helperGroups = new LeftPaneComposeHelper({ const helperGroups = new LeftPaneComposeHelper({
composeContacts: [], composeContacts: [],
composeGroups: [getDefaultConversation(), getDefaultConversation()], composeGroups: [getDefaultGroupListItem(), getDefaultGroupListItem()],
regionCode: 'US', regionCode: 'US',
searchTerm: 'foo bar', searchTerm: 'foo bar',
isUsernamesEnabled: true, isUsernamesEnabled: true,
@ -603,7 +607,7 @@ describe('LeftPaneComposeHelper', () => {
it('should be true if the headers are in different row indices as before', () => { it('should be true if the headers are in different row indices as before', () => {
const helperContacts = new LeftPaneComposeHelper({ const helperContacts = new LeftPaneComposeHelper({
composeContacts: [getDefaultConversation(), getDefaultConversation()], composeContacts: [getDefaultConversation(), getDefaultConversation()],
composeGroups: [getDefaultConversation()], composeGroups: [getDefaultGroupListItem()],
regionCode: 'US', regionCode: 'US',
searchTerm: 'soup', searchTerm: 'soup',
isUsernamesEnabled: true, isUsernamesEnabled: true,
@ -613,7 +617,7 @@ describe('LeftPaneComposeHelper', () => {
assert.isTrue( assert.isTrue(
helperContacts.shouldRecomputeRowHeights({ helperContacts.shouldRecomputeRowHeights({
composeContacts: [getDefaultConversation()], composeContacts: [getDefaultConversation()],
composeGroups: [getDefaultConversation(), getDefaultConversation()], composeGroups: [getDefaultGroupListItem(), getDefaultGroupListItem()],
regionCode: 'US', regionCode: 'US',
searchTerm: 'soup', searchTerm: 'soup',
isUsernamesEnabled: true, isUsernamesEnabled: true,