From d64e0b65c46e946ac190c35b4e00b55648fada24 Mon Sep 17 00:00:00 2001
From: Alvaro <110414366+alvaro-signal@users.noreply.github.com>
Date: Wed, 25 Jan 2023 16:51:08 -0700
Subject: [PATCH] Switched ForwardMessageModal to use ListTile
---
.eslintrc.js | 2 +
stylesheets/_modules.scss | 7 +
.../components/AddGroupMembersModal.scss | 5 +
stylesheets/components/ContactPills.scss | 4 +-
.../components/ForwardMessageModal.scss | 5 +
ts/components/AddUserToAnotherGroupModal.tsx | 98 +++++++----
ts/components/ConversationList.tsx | 3 +
ts/components/ForwardMessageModal.tsx | 10 +-
ts/components/LeftPane.stories.tsx | 16 +-
ts/components/LeftPane.tsx | 6 +-
ts/components/ListTile.tsx | 69 +++++++-
ts/components/conversation/Message.tsx | 2 +
.../ChooseGroupMembersModal.tsx | 165 +++++++++++++-----
.../BaseConversationListItem.tsx | 2 +-
.../conversationList/ContactCheckbox.tsx | 61 ++++---
.../conversationList/ContactListItem.tsx | 53 +++---
.../conversationList/CreateNewGroupButton.tsx | 26 +--
.../conversationList/GroupListItem.tsx | 35 ++--
.../conversationList/PhoneNumberCheckbox.tsx | 61 +++++--
.../conversationList/UsernameCheckbox.tsx | 47 +++--
.../leftPane/LeftPaneComposeHelper.tsx | 10 +-
ts/state/selectors/conversations.ts | 21 ++-
.../helpers/getDefaultConversation.ts | 15 +-
ts/test-mock/pnp/send_gv2_invite_test.ts | 10 +-
.../leftPane/LeftPaneComposeHelper_test.ts | 34 ++--
25 files changed, 528 insertions(+), 239 deletions(-)
diff --git a/.eslintrc.js b/.eslintrc.js
index b1bf52c27d2d..3cfd9896848a 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -122,6 +122,8 @@ const rules = {
'react/display-name': 'error',
+ 'react/jsx-pascal-case': ['error', {allowNamespace: true}],
+
// Allow returning values from promise executors for brevity.
'no-promise-executor-return': 'off',
diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss
index e6e2d78ab12b..a6d4175047b7 100644
--- a/stylesheets/_modules.scss
+++ b/stylesheets/_modules.scss
@@ -4405,6 +4405,13 @@ button.module-image__border-overlay:focus {
padding-left: 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 {
padding-left: 10px;
padding-right: 10px;
diff --git a/stylesheets/components/AddGroupMembersModal.scss b/stylesheets/components/AddGroupMembersModal.scss
index 687d854bfea8..7648cdad3fc7 100644
--- a/stylesheets/components/AddGroupMembersModal.scss
+++ b/stylesheets/components/AddGroupMembersModal.scss
@@ -64,4 +64,9 @@
padding: 0;
}
}
+
+ .module-conversation-list {
+ // remove horizontal padding so ListTiles extend to the edges
+ padding: 0;
+ }
}
diff --git a/stylesheets/components/ContactPills.scss b/stylesheets/components/ContactPills.scss
index a8f8275920c1..ab12b310f34f 100644
--- a/stylesheets/components/ContactPills.scss
+++ b/stylesheets/components/ContactPills.scss
@@ -10,10 +10,10 @@
max-height: 88px;
overflow-x: hidden;
overflow-y: scroll;
- padding-left: 12px;
+ padding: 4px 24px;
+ gap: 8px 12px;
.module-ContactPill {
- margin: 4px 6px;
max-width: calc(
100% - 15px
); // 6px for the right margin and 9px for the scrollbar
diff --git a/stylesheets/components/ForwardMessageModal.scss b/stylesheets/components/ForwardMessageModal.scss
index 775e9f665226..a473724a4e3f 100644
--- a/stylesheets/components/ForwardMessageModal.scss
+++ b/stylesheets/components/ForwardMessageModal.scss
@@ -22,6 +22,11 @@
color: $color-gray-05;
}
+ .module-conversation-list {
+ // remove horizontal padding so ListTiles extend to the edges
+ padding: 0;
+ }
+
&--link-preview {
border-bottom: 1px solid $color-gray-15;
padding: 12px 16px;
diff --git a/ts/components/AddUserToAnotherGroupModal.tsx b/ts/components/AddUserToAnotherGroupModal.tsx
index 16140b3e0bbb..cb4c145aca4a 100644
--- a/ts/components/AddUserToAnotherGroupModal.tsx
+++ b/ts/components/AddUserToAnotherGroupModal.tsx
@@ -1,10 +1,11 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
-import { noop, pick } from 'lodash';
-import React from 'react';
+import { pick } from 'lodash';
+import React, { useCallback } from 'react';
import type { MeasuredComponentProps } from 'react-measure';
import Measure from 'react-measure';
+import type { ListRowProps } from 'react-virtualized';
import type { ConversationType } from '../state/ducks/conversations';
import type {
@@ -15,12 +16,16 @@ import type {
import { ToastType } from '../types/Toast';
import { filterAndSortConversationsByRecent } from '../util/filterAndSortConversations';
import { ConfirmationDialog } from './ConfirmationDialog';
-import type { Row } from './ConversationList';
-import { ConversationList, RowType } from './ConversationList';
-import { DisabledReason } from './conversationList/GroupListItem';
+import type { GroupListItemConversationType } from './conversationList/GroupListItem';
+import {
+ DisabledReason,
+ GroupListItem,
+} from './conversationList/GroupListItem';
import { Modal } from './Modal';
import { SearchInput } from './SearchInput';
import { useRestoreFocus } from '../hooks/useRestoreFocus';
+import { ListView } from './ListView';
+import { ListTile } from './ListTile';
type OwnProps = {
i18n: LocalizerType;
@@ -47,7 +52,6 @@ export type Props = OwnProps & DispatchProps;
export function AddUserToAnotherGroupModal({
i18n,
- theme,
contact,
toggleAddUserToAnotherGroupModal,
addMembersToGroup,
@@ -108,7 +112,7 @@ export function AddUserToAnotherGroupModal({
);
const handleGetRow = React.useCallback(
- (idx: number): Row | undefined => {
+ (idx: number): GroupListItemConversationType => {
const convo = filteredConversations[idx];
// these are always populated in the case of a group
@@ -129,18 +133,36 @@ export function AddUserToAnotherGroupModal({
}
return {
- type: RowType.SelectSingleGroup,
- group: {
- ...pick(convo, 'id', 'avatarPath', 'title', 'unblurredAvatarPath'),
- memberships,
- membersCount,
- disabledReason,
- },
+ ...pick(convo, 'id', 'avatarPath', 'title', 'unblurredAvatarPath'),
+ memberships,
+ membersCount,
+ disabledReason,
};
},
[filteredConversations, contact]
);
+ const renderGroupListItem = useCallback(
+ ({ key, index, style }: ListRowProps) => {
+ const group = handleGetRow(index);
+ return (
+
+
+
+ );
+ },
+ [i18n, handleGetRow]
+ );
+
+ const handleCalculateRowHeight = useCallback(
+ () => ListTile.heightCompact,
+ []
+ );
+
return (
<>
{!selectedGroup && (
@@ -163,30 +185,30 @@ export function AddUserToAnotherGroupModal({
/>
- {({ contentRect, measureRef }: MeasuredComponentProps) => (
-
- undefined}
- getRow={handleGetRow}
- i18n={i18n}
- lookupConversationWithoutUuid={async _ => undefined}
- onClickArchiveButton={noop}
- onClickContactCheckbox={noop}
- onSelectConversation={setSelectedGroupId}
- rowCount={filteredConversations.length}
- setIsFetchingUUID={noop}
- shouldRecomputeRowHeights={false}
- showChooseGroupMembers={noop}
- showConversation={noop}
- showUserNotFoundModal={noop}
- theme={theme}
- />
-
- )}
+ {({ 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;
+ }
+
+ return (
+
+
+
+ );
+ }}
diff --git a/ts/components/ConversationList.tsx b/ts/components/ConversationList.tsx
index f2c0cb44a743..b94b264fb43d 100644
--- a/ts/components/ConversationList.tsx
+++ b/ts/components/ConversationList.tsx
@@ -215,6 +215,9 @@ export function ConversationList({
case RowType.SearchResultsLoadingFakeHeader:
return HEADER_ROW_HEIGHT;
case RowType.SelectSingleGroup:
+ case RowType.ContactCheckbox:
+ case RowType.Contact:
+ case RowType.CreateNewGroup:
return SELECT_ROW_HEIGHT;
default:
return NORMAL_ROW_HEIGHT;
diff --git a/ts/components/ForwardMessageModal.tsx b/ts/components/ForwardMessageModal.tsx
index fc6795b496dd..9d471e714dd6 100644
--- a/ts/components/ForwardMessageModal.tsx
+++ b/ts/components/ForwardMessageModal.tsx
@@ -38,6 +38,7 @@ import {
shouldNeverBeCalled,
asyncShouldNeverBeCalled,
} from '../util/shouldNeverBeCalled';
+import { Emojify } from './conversation/Emojify';
export type DataPropsType = {
attachments?: ReadonlyArray;
@@ -408,8 +409,13 @@ export function ForwardMessageModal({
)}
- {Boolean(selectedContacts.length) &&
- selectedContacts.map(contact => contact.title).join(', ')}
+ {Boolean(selectedContacts.length) && (
+ contact.title)
+ .join(', ')}
+ />
+ )}
{isEditingMessage || !isMessageEditable ? (
diff --git a/ts/components/LeftPane.stories.tsx b/ts/components/LeftPane.stories.tsx
index 85db2782d91c..e426509715a9 100644
--- a/ts/components/LeftPane.stories.tsx
+++ b/ts/components/LeftPane.stories.tsx
@@ -23,14 +23,18 @@ import { setupI18n } from '../util/setupI18n';
import { DurationInSeconds, DAY } from '../util/durations';
import enMessages from '../../_locales/en/messages.json';
import { ThemeType } from '../types/Util';
+import {
+ getDefaultConversation,
+ getDefaultGroupListItem,
+} from '../test-both/helpers/getDefaultConversation';
import { DialogType } from '../types/Dialogs';
import { SocketStatus } from '../types/SocketStatus';
-import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
import { StorybookThemeContext } from '../../.storybook/StorybookThemeContext';
import {
makeFakeLookupConversationWithoutUuid,
useUuidFetchState,
} from '../test-both/helpers/fakeLookupConversationWithoutUuid';
+import type { GroupListItemConversationType } from './conversationList/GroupListItem';
const i18n = setupI18n('en', enMessages);
@@ -62,18 +66,14 @@ const defaultSearchProps = {
startSearchCounter: 0,
};
-const defaultGroups: Array
= [
- getDefaultConversation({
+const defaultGroups: Array = [
+ getDefaultGroupListItem({
id: 'biking-group',
title: 'Mtn Biking Arizona 🚵☀️⛰',
- type: 'group',
- sharedGroupNames: [],
}),
- getDefaultConversation({
+ getDefaultGroupListItem({
id: 'dance-group',
title: 'Are we dancers? 💃',
- type: 'group',
- sharedGroupNames: [],
}),
];
diff --git a/ts/components/LeftPane.tsx b/ts/components/LeftPane.tsx
index 741ab8bf6f83..17e5fed9fd87 100644
--- a/ts/components/LeftPane.tsx
+++ b/ts/components/LeftPane.tsx
@@ -605,7 +605,11 @@ export function LeftPane({
className={classNames(
'module-left-pane',
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 }}
>
diff --git a/ts/components/ListTile.tsx b/ts/components/ListTile.tsx
index cf1a275653f5..2b07e33ff965 100644
--- a/ts/components/ListTile.tsx
+++ b/ts/components/ListTile.tsx
@@ -2,8 +2,10 @@
// SPDX-License-Identifier: AGPL-3.0-only
import classNames from 'classnames';
-import React from 'react';
+import React, { useMemo } from 'react';
+import uuid from 'uuid';
import { getClassNamesFor } from '../util/getClassNamesFor';
+import { CircleCheckbox } from './CircleCheckbox';
export type Props = {
title: string | JSX.Element;
@@ -23,6 +25,7 @@ export type Props = {
variant?: 'item' | 'panelrow';
// defaults to div
rootElement?: 'div' | 'button';
+ testId?: string;
};
const getClassName = getClassNamesFor('ListTile');
@@ -53,8 +56,17 @@ const getClassName = getClassNamesFor('ListTile');
* - panelrow: more horizontal padding, intended for information rows (usually not in
* modals) that tend to occupy more horizontal space
*/
-export const ListTile = React.forwardRef(
- function ListTile(
+export function ListTile(
+ params: Props & React.RefAttributes
+): 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 ;
+}
+
+const ListTileImpl = React.forwardRef(
+ function ListTileImpl(
{
title,
subtitle,
@@ -67,6 +79,7 @@ export const ListTile = React.forwardRef(
disabled = false,
variant = 'item',
rootElement = 'div',
+ testId,
}: Props,
ref
) {
@@ -81,6 +94,7 @@ export const ListTile = React.forwardRef(
onClick,
'aria-disabled': disabled ? true : undefined,
onContextMenu,
+ 'data-testid': testId,
};
const contents = (
@@ -112,3 +126,52 @@ export const ListTile = React.forwardRef(
);
}
);
+
+// 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
);
diff --git a/ts/components/conversationList/BaseConversationListItem.tsx b/ts/components/conversationList/BaseConversationListItem.tsx
index 097e5043077a..926de8bd6dfd 100644
--- a/ts/components/conversationList/BaseConversationListItem.tsx
+++ b/ts/components/conversationList/BaseConversationListItem.tsx
@@ -30,7 +30,7 @@ const MESSAGE_CLASS_NAME = `${CONTENT_CLASS_NAME}__message`;
export const MESSAGE_TEXT_CLASS_NAME = `${MESSAGE_CLASS_NAME}__text`;
const CHECKBOX_CONTAINER_CLASS_NAME = `${BASE_CLASS_NAME}__checkbox--container`;
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 = {
checked?: boolean;
diff --git a/ts/components/conversationList/ContactCheckbox.tsx b/ts/components/conversationList/ContactCheckbox.tsx
index 2e59e9adf534..053a1a2b17a4 100644
--- a/ts/components/conversationList/ContactCheckbox.tsx
+++ b/ts/components/conversationList/ContactCheckbox.tsx
@@ -1,18 +1,17 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
-import type { FunctionComponent, ReactNode } from 'react';
import React from 'react';
+import type { FunctionComponent } from 'react';
-import {
- BaseConversationListItem,
- HEADER_CONTACT_NAME_CLASS_NAME,
-} from './BaseConversationListItem';
+import { HEADER_CONTACT_NAME_CLASS_NAME } from './BaseConversationListItem';
import type { ConversationType } from '../../state/ducks/conversations';
import type { BadgeType } from '../../badges/types';
import type { LocalizerType, ThemeType } from '../../types/Util';
import { ContactName } from '../conversation/ContactName';
import { About } from '../conversation/About';
+import { ListTile } from '../ListTile';
+import { Avatar, AvatarSize } from '../Avatar';
export enum ContactCheckboxDisabledReason {
// We start the enum at 1 because the default starting value of 0 is falsy.
@@ -61,7 +60,6 @@ export const ContactCheckbox: FunctionComponent
= React.memo(
badge,
color,
disabledReason,
- groupId,
i18n,
id,
isChecked,
@@ -74,7 +72,6 @@ export const ContactCheckbox: FunctionComponent = React.memo(
title,
type,
unblurredAvatarPath,
- uuid,
}) {
const disabled = Boolean(disabledReason);
@@ -86,13 +83,11 @@ export const ContactCheckbox: FunctionComponent = React.memo(
);
- let messageText: ReactNode;
+ let messageText: undefined | string | JSX.Element;
if (disabledReason === ContactCheckboxDisabledReason.AlreadyAdded) {
messageText = i18n('alreadyAMember');
} else if (about) {
messageText = ;
- } else {
- messageText = null;
}
const onClickItem = () => {
@@ -100,29 +95,33 @@ export const ContactCheckbox: FunctionComponent = React.memo(
};
return (
-
+ }
+ title={headerName}
+ subtitle={isMe ? undefined : messageText}
+ subtitleMaxLines={1}
onClick={onClickItem}
- phoneNumber={phoneNumber}
- profileName={profileName}
- sharedGroupNames={sharedGroupNames}
- theme={theme}
- title={title}
- unblurredAvatarPath={unblurredAvatarPath}
- uuid={uuid}
/>
);
}
diff --git a/ts/components/conversationList/ContactListItem.tsx b/ts/components/conversationList/ContactListItem.tsx
index fdc2d0dc2614..4d5490164b9a 100644
--- a/ts/components/conversationList/ContactListItem.tsx
+++ b/ts/components/conversationList/ContactListItem.tsx
@@ -4,15 +4,14 @@
import type { FunctionComponent } from 'react';
import React from 'react';
-import {
- BaseConversationListItem,
- HEADER_CONTACT_NAME_CLASS_NAME,
-} from './BaseConversationListItem';
+import { HEADER_CONTACT_NAME_CLASS_NAME } from './BaseConversationListItem';
import type { ConversationType } from '../../state/ducks/conversations';
import type { BadgeType } from '../../badges/types';
import type { LocalizerType, ThemeType } from '../../types/Util';
import { ContactName } from '../conversation/ContactName';
import { About } from '../conversation/About';
+import { ListTile } from '../ListTile';
+import { Avatar, AvatarSize } from '../Avatar';
import { isSignalConversation } from '../../util/isSignalConversation';
export type ContactListItemConversationType = Pick<
@@ -55,7 +54,6 @@ export const ContactListItem: FunctionComponent = React.memo(
avatarPath,
badge,
color,
- groupId,
i18n,
id,
isMe,
@@ -82,30 +80,33 @@ export const ContactListItem: FunctionComponent = React.memo(
);
const messageText =
- about && !isMe ? : null;
+ about && !isMe ? : undefined;
return (
-
+ }
+ title={headerName}
+ subtitle={messageText}
+ subtitleMaxLines={1}
onClick={onClick ? () => onClick(id) : undefined}
- phoneNumber={phoneNumber}
- profileName={profileName}
- sharedGroupNames={sharedGroupNames}
- theme={theme}
- title={title}
- unblurredAvatarPath={unblurredAvatarPath}
- uuid={uuid}
/>
);
}
diff --git a/ts/components/conversationList/CreateNewGroupButton.tsx b/ts/components/conversationList/CreateNewGroupButton.tsx
index 668ffe09040f..4f87b1167bfc 100644
--- a/ts/components/conversationList/CreateNewGroupButton.tsx
+++ b/ts/components/conversationList/CreateNewGroupButton.tsx
@@ -4,8 +4,9 @@
import type { FunctionComponent } from 'react';
import React from 'react';
-import { BaseConversationListItem } from './BaseConversationListItem';
import type { LocalizerType } from '../../types/Util';
+import { ListTile } from '../ListTile';
+import { Avatar, AvatarSize } from '../Avatar';
type PropsType = {
i18n: LocalizerType;
@@ -17,17 +18,22 @@ export const CreateNewGroupButton: FunctionComponent = React.memo(
const title = i18n('createNewGroupButton');
return (
-
+ }
title={title}
+ onClick={onClick}
/>
);
}
diff --git a/ts/components/conversationList/GroupListItem.tsx b/ts/components/conversationList/GroupListItem.tsx
index 260c3048155b..6c7145f210e2 100644
--- a/ts/components/conversationList/GroupListItem.tsx
+++ b/ts/components/conversationList/GroupListItem.tsx
@@ -5,8 +5,9 @@ import React from 'react';
import type { ConversationType } from '../../state/ducks/conversations';
import type { LocalizerType } from '../../types/Util';
import type { UUIDStringType } from '../../types/UUID';
-import { AvatarSize } from '../Avatar';
-import { BaseConversationListItem } from './BaseConversationListItem';
+import { Avatar, AvatarSize } from '../Avatar';
+import { Emojify } from '../conversation/Emojify';
+import { ListTile } from '../ListTile';
export enum DisabledReason {
AlreadyMember = 'already-member',
@@ -49,21 +50,25 @@ export function GroupListItem({
count: group.membersCount,
});
}
+
return (
-
+ }
+ title={}
+ subtitle={}
onClick={() => onSelectGroup(group.id)}
- messageText={messageText}
/>
);
}
diff --git a/ts/components/conversationList/PhoneNumberCheckbox.tsx b/ts/components/conversationList/PhoneNumberCheckbox.tsx
index 7b90226dfa32..7d3acf4dfc22 100644
--- a/ts/components/conversationList/PhoneNumberCheckbox.tsx
+++ b/ts/components/conversationList/PhoneNumberCheckbox.tsx
@@ -6,11 +6,15 @@ import React, { useState } from 'react';
import { ButtonVariant } from '../Button';
import { ConfirmationDialog } from '../ConfirmationDialog';
-import { BaseConversationListItem } from './BaseConversationListItem';
+import { SPINNER_CLASS_NAME } from './BaseConversationListItem';
import type { ParsedE164Type } from '../../util/libphonenumberInstance';
import type { LocalizerType, ThemeType } from '../../types/Util';
import { AvatarColors } from '../../types/Colors';
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 = {
phoneNumber: ParsedE164Type;
@@ -31,7 +35,6 @@ export const PhoneNumberCheckbox: FunctionComponent = React.memo(
phoneNumber,
isChecked,
isFetching,
- theme,
i18n,
lookupConversationWithoutUuid,
showUserNotFoundModal,
@@ -88,24 +91,46 @@ export const PhoneNumberCheckbox: FunctionComponent = React.memo(
);
}
+ const avatar = (
+
+ );
+
+ const title = ;
+
return (
<>
-
+ {isFetching ? (
+
+ }
+ />
+ ) : (
+ }
+ />
+ )}
{modal}
>
);
diff --git a/ts/components/conversationList/UsernameCheckbox.tsx b/ts/components/conversationList/UsernameCheckbox.tsx
index 28c9547032c5..64d9390874bc 100644
--- a/ts/components/conversationList/UsernameCheckbox.tsx
+++ b/ts/components/conversationList/UsernameCheckbox.tsx
@@ -1,13 +1,16 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
-import type { FunctionComponent } from 'react';
import React from 'react';
+import type { FunctionComponent } from 'react';
-import { BaseConversationListItem } from './BaseConversationListItem';
import type { LocalizerType, ThemeType } from '../../types/Util';
import { AvatarColors } from '../../types/Colors';
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 = {
username: string;
@@ -28,7 +31,6 @@ export const UsernameCheckbox: FunctionComponent = React.memo(
username,
isChecked,
isFetching,
- theme,
i18n,
lookupConversationWithoutUuid,
showUserNotFoundModal,
@@ -62,22 +64,41 @@ export const UsernameCheckbox: FunctionComponent = React.memo(
const title = i18n('at-username', { username });
- return (
-
+ );
+
+ return isFetching ? (
+
+ }
+ />
+ ) : (
+
);
}
diff --git a/ts/components/leftPane/LeftPaneComposeHelper.tsx b/ts/components/leftPane/LeftPaneComposeHelper.tsx
index a71b89d2f2dd..9e1091a15741 100644
--- a/ts/components/leftPane/LeftPaneComposeHelper.tsx
+++ b/ts/components/leftPane/LeftPaneComposeHelper.tsx
@@ -8,7 +8,6 @@ import { LeftPaneHelper } from './LeftPaneHelper';
import type { Row } from '../ConversationList';
import { RowType } from '../ConversationList';
import type { ContactListItemConversationType } from '../conversationList/ContactListItem';
-import type { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem';
import { SearchInput } from '../SearchInput';
import type { LocalizerType } from '../../types/Util';
import type { ParsedE164Type } from '../../util/libphonenumberInstance';
@@ -20,10 +19,11 @@ import {
isFetchingByUsername,
isFetchingByE164,
} from '../../util/uuidFetchState';
+import type { GroupListItemConversationType } from '../conversationList/GroupListItem';
export type LeftPaneComposePropsType = {
composeContacts: ReadonlyArray;
- composeGroups: ReadonlyArray;
+ composeGroups: ReadonlyArray;
regionCode: string | undefined;
searchTerm: string;
@@ -39,7 +39,7 @@ enum TopButton {
export class LeftPaneComposeHelper extends LeftPaneHelper {
private readonly composeContacts: ReadonlyArray;
- private readonly composeGroups: ReadonlyArray;
+ private readonly composeGroups: ReadonlyArray;
private readonly uuidFetchState: UUIDFetchStateType;
@@ -224,8 +224,8 @@ export class LeftPaneComposeHelper extends LeftPaneHelper,
regionCode: string | undefined
- ): Array => {
- return filterAndSortConversationsByRecent(groups, searchTerm, regionCode);
+ ): Array<
+ ConversationType & {
+ membersCount: number;
+ disabledReason: undefined;
+ memberships: ReadonlyArray;
+ }
+ > => {
+ 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 ?? [],
+ }));
}
);
diff --git a/ts/test-both/helpers/getDefaultConversation.ts b/ts/test-both/helpers/getDefaultConversation.ts
index de52b5a3f722..8586c21c46b6 100644
--- a/ts/test-both/helpers/getDefaultConversation.ts
+++ b/ts/test-both/helpers/getDefaultConversation.ts
@@ -4,8 +4,9 @@
import casual from 'casual';
import { sample } from 'lodash';
import type { ConversationType } from '../../state/ducks/conversations';
-import { UUID } 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 { ConversationColors } from '../../types/Colors';
import { StorySendMode } from '../../types/Stories';
@@ -46,6 +47,18 @@ export function getDefaultConversation(
};
}
+export function getDefaultGroupListItem(
+ overrideProps: Partial = {}
+): GroupListItemConversationType {
+ return {
+ ...getDefaultGroup(),
+ disabledReason: undefined,
+ membersCount: 24,
+ memberships: [],
+ ...overrideProps,
+ };
+}
+
export function getDefaultGroup(
overrideProps: Partial = {}
): ConversationType {
diff --git a/ts/test-mock/pnp/send_gv2_invite_test.ts b/ts/test-mock/pnp/send_gv2_invite_test.ts
index 496b250c469e..2ae03f9c8509 100644
--- a/ts/test-mock/pnp/send_gv2_invite_test.ts
+++ b/ts/test-mock/pnp/send_gv2_invite_test.ts
@@ -113,9 +113,7 @@ describe('pnp/send gv2 invite', function needsName() {
.locator('.module-left-pane__compose-search-form__input')
.fill('ACI');
- await leftPane
- .locator(`[data-testid="${aciContact.toContact().uuid}"]`)
- .click();
+ await leftPane.locator('.ListTile >> "ACI Contact"').click();
debug('inviting PNI member');
@@ -123,11 +121,7 @@ describe('pnp/send gv2 invite', function needsName() {
.locator('.module-left-pane__compose-search-form__input')
.fill('PNI');
- await leftPane
- .locator(
- `[data-testid="${pniContact.device.getUUIDByKind(UUIDKind.PNI)}"]`
- )
- .click();
+ await leftPane.locator('.ListTile >> "PNI Contact"').click();
await leftPane
.locator('.module-left-pane__footer button >> "Next"')
diff --git a/ts/test-node/components/leftPane/LeftPaneComposeHelper_test.ts b/ts/test-node/components/leftPane/LeftPaneComposeHelper_test.ts
index 6ff65c3513bf..ab1c3eebbf7a 100644
--- a/ts/test-node/components/leftPane/LeftPaneComposeHelper_test.ts
+++ b/ts/test-node/components/leftPane/LeftPaneComposeHelper_test.ts
@@ -5,7 +5,10 @@ import { assert } from 'chai';
import * as sinon from 'sinon';
import { RowType } from '../../../components/ConversationList';
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';
@@ -69,7 +72,7 @@ describe('LeftPaneComposeHelper', () => {
assert.strictEqual(
new LeftPaneComposeHelper({
composeContacts: [getDefaultConversation(), getDefaultConversation()],
- composeGroups: [getDefaultConversation(), getDefaultConversation()],
+ composeGroups: [getDefaultGroupListItem(), getDefaultGroupListItem()],
regionCode: 'US',
searchTerm: '',
isUsernamesEnabled: true,
@@ -83,7 +86,7 @@ describe('LeftPaneComposeHelper', () => {
assert.strictEqual(
new LeftPaneComposeHelper({
composeContacts: [getDefaultConversation(), getDefaultConversation()],
- composeGroups: [getDefaultConversation(), getDefaultConversation()],
+ composeGroups: [getDefaultGroupListItem(), getDefaultGroupListItem()],
regionCode: 'US',
searchTerm: 'someone',
isUsernamesEnabled: true,
@@ -97,7 +100,7 @@ describe('LeftPaneComposeHelper', () => {
assert.strictEqual(
new LeftPaneComposeHelper({
composeContacts: [getDefaultConversation(), getDefaultConversation()],
- composeGroups: [getDefaultConversation(), getDefaultConversation()],
+ composeGroups: [getDefaultGroupListItem(), getDefaultGroupListItem()],
regionCode: 'US',
searchTerm: 'someone',
isUsernamesEnabled: false,
@@ -133,7 +136,7 @@ describe('LeftPaneComposeHelper', () => {
assert.strictEqual(
new LeftPaneComposeHelper({
composeContacts: [getDefaultConversation(), getDefaultConversation()],
- composeGroups: [getDefaultConversation()],
+ composeGroups: [getDefaultGroupListItem()],
regionCode: 'US',
searchTerm: 'foobar',
isUsernamesEnabled: true,
@@ -240,8 +243,8 @@ describe('LeftPaneComposeHelper', () => {
getDefaultConversation(),
];
const composeGroups = [
- getDefaultConversation(),
- getDefaultConversation(),
+ getDefaultGroupListItem(),
+ getDefaultGroupListItem(),
];
const helper = new LeftPaneComposeHelper({
composeContacts,
@@ -255,6 +258,7 @@ describe('LeftPaneComposeHelper', () => {
assert.deepEqual(helper.getRow(0), {
type: RowType.CreateNewGroup,
});
+
assert.deepEqual(helper.getRow(1), {
type: RowType.Header,
i18nKey: 'contactsHeader',
@@ -272,12 +276,12 @@ describe('LeftPaneComposeHelper', () => {
i18nKey: 'groupsHeader',
});
assert.deepEqual(helper.getRow(5), {
- type: RowType.Conversation,
- conversation: composeGroups[0],
+ type: RowType.SelectSingleGroup,
+ group: composeGroups[0],
});
assert.deepEqual(helper.getRow(6), {
- type: RowType.Conversation,
- conversation: composeGroups[1],
+ type: RowType.SelectSingleGroup,
+ group: composeGroups[1],
});
});
@@ -571,7 +575,7 @@ describe('LeftPaneComposeHelper', () => {
assert.isTrue(
helperContacts.shouldRecomputeRowHeights({
composeContacts: [],
- composeGroups: [getDefaultConversation(), getDefaultConversation()],
+ composeGroups: [getDefaultGroupListItem(), getDefaultGroupListItem()],
regionCode: 'US',
searchTerm: 'foo bar',
isUsernamesEnabled: true,
@@ -581,7 +585,7 @@ describe('LeftPaneComposeHelper', () => {
const helperGroups = new LeftPaneComposeHelper({
composeContacts: [],
- composeGroups: [getDefaultConversation(), getDefaultConversation()],
+ composeGroups: [getDefaultGroupListItem(), getDefaultGroupListItem()],
regionCode: 'US',
searchTerm: 'foo bar',
isUsernamesEnabled: true,
@@ -603,7 +607,7 @@ describe('LeftPaneComposeHelper', () => {
it('should be true if the headers are in different row indices as before', () => {
const helperContacts = new LeftPaneComposeHelper({
composeContacts: [getDefaultConversation(), getDefaultConversation()],
- composeGroups: [getDefaultConversation()],
+ composeGroups: [getDefaultGroupListItem()],
regionCode: 'US',
searchTerm: 'soup',
isUsernamesEnabled: true,
@@ -613,7 +617,7 @@ describe('LeftPaneComposeHelper', () => {
assert.isTrue(
helperContacts.shouldRecomputeRowHeights({
composeContacts: [getDefaultConversation()],
- composeGroups: [getDefaultConversation(), getDefaultConversation()],
+ composeGroups: [getDefaultGroupListItem(), getDefaultGroupListItem()],
regionCode: 'US',
searchTerm: 'soup',
isUsernamesEnabled: true,