Support for creating New Groups
This commit is contained in:
parent
1934120e46
commit
5de4babc0d
56 changed files with 6222 additions and 526 deletions
|
@ -20,11 +20,14 @@ export const DATE_CLASS_NAME = `${HEADER_CLASS_NAME}__date`;
|
|||
const TIMESTAMP_CLASS_NAME = `${DATE_CLASS_NAME}__timestamp`;
|
||||
export const MESSAGE_CLASS_NAME = `${CONTENT_CLASS_NAME}__message`;
|
||||
export const MESSAGE_TEXT_CLASS_NAME = `${MESSAGE_CLASS_NAME}__text`;
|
||||
const CHECKBOX_CLASS_NAME = `${BASE_CLASS_NAME}__checkbox`;
|
||||
|
||||
type PropsType = {
|
||||
avatarPath?: string;
|
||||
checked?: boolean;
|
||||
color?: ColorType;
|
||||
conversationType: 'group' | 'direct';
|
||||
disabled?: boolean;
|
||||
headerDate?: number;
|
||||
headerName: ReactNode;
|
||||
i18n: LocalizerType;
|
||||
|
@ -37,7 +40,7 @@ type PropsType = {
|
|||
messageStatusIcon?: ReactNode;
|
||||
messageText?: ReactNode;
|
||||
name?: string;
|
||||
onClick: () => void;
|
||||
onClick?: () => void;
|
||||
phoneNumber?: string;
|
||||
profileName?: string;
|
||||
style: CSSProperties;
|
||||
|
@ -48,8 +51,10 @@ type PropsType = {
|
|||
export const BaseConversationListItem: FunctionComponent<PropsType> = React.memo(
|
||||
({
|
||||
avatarPath,
|
||||
checked,
|
||||
color,
|
||||
conversationType,
|
||||
disabled,
|
||||
headerDate,
|
||||
headerName,
|
||||
i18n,
|
||||
|
@ -74,17 +79,32 @@ export const BaseConversationListItem: FunctionComponent<PropsType> = React.memo
|
|||
? isNoteToSelf
|
||||
: Boolean(isMe);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
style={style}
|
||||
className={classNames(BASE_CLASS_NAME, {
|
||||
[`${BASE_CLASS_NAME}--has-unread`]: isUnread,
|
||||
[`${BASE_CLASS_NAME}--is-selected`]: isSelected,
|
||||
})}
|
||||
data-id={id ? cleanId(id) : undefined}
|
||||
>
|
||||
const isCheckbox = isBoolean(checked);
|
||||
|
||||
let checkboxNode: ReactNode;
|
||||
if (isCheckbox) {
|
||||
let ariaLabel: string;
|
||||
if (disabled) {
|
||||
ariaLabel = i18n('cannotSelectContact');
|
||||
} else if (checked) {
|
||||
ariaLabel = i18n('deselectContact');
|
||||
} else {
|
||||
ariaLabel = i18n('selectContact');
|
||||
}
|
||||
checkboxNode = (
|
||||
<input
|
||||
aria-label={ariaLabel}
|
||||
checked={checked}
|
||||
className={CHECKBOX_CLASS_NAME}
|
||||
disabled={disabled}
|
||||
onChange={onClick}
|
||||
type="checkbox"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const contents = (
|
||||
<>
|
||||
<div className={`${BASE_CLASS_NAME}__avatar-container`}>
|
||||
<Avatar
|
||||
avatarPath={avatarPath}
|
||||
|
@ -104,7 +124,12 @@ export const BaseConversationListItem: FunctionComponent<PropsType> = React.memo
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={CONTENT_CLASS_NAME}>
|
||||
<div
|
||||
className={classNames(
|
||||
CONTENT_CLASS_NAME,
|
||||
disabled && `${CONTENT_CLASS_NAME}--disabled`
|
||||
)}
|
||||
>
|
||||
<div className={HEADER_CLASS_NAME}>
|
||||
<div className={`${HEADER_CLASS_NAME}__name`}>{headerName}</div>
|
||||
{isNumber(headerDate) && (
|
||||
|
@ -137,7 +162,61 @@ export const BaseConversationListItem: FunctionComponent<PropsType> = React.memo
|
|||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</button>
|
||||
{checkboxNode}
|
||||
</>
|
||||
);
|
||||
|
||||
const commonClassNames = classNames(BASE_CLASS_NAME, {
|
||||
[`${BASE_CLASS_NAME}--has-unread`]: isUnread,
|
||||
[`${BASE_CLASS_NAME}--is-selected`]: isSelected,
|
||||
});
|
||||
|
||||
if (isCheckbox) {
|
||||
return (
|
||||
<label
|
||||
className={classNames(
|
||||
commonClassNames,
|
||||
`${BASE_CLASS_NAME}--is-checkbox`,
|
||||
{ [`${BASE_CLASS_NAME}--is-checkbox--disabled`]: disabled }
|
||||
)}
|
||||
data-id={id ? cleanId(id) : undefined}
|
||||
style={style}
|
||||
// `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 } : {})}
|
||||
>
|
||||
{contents}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
if (onClick) {
|
||||
return (
|
||||
<button
|
||||
className={classNames(
|
||||
commonClassNames,
|
||||
`${BASE_CLASS_NAME}--is-button`
|
||||
)}
|
||||
data-id={id ? cleanId(id) : undefined}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
style={style}
|
||||
type="button"
|
||||
>
|
||||
{contents}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={commonClassNames}
|
||||
data-id={id ? cleanId(id) : undefined}
|
||||
style={style}
|
||||
>
|
||||
{contents}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
97
ts/components/conversationList/ContactCheckbox.tsx
Normal file
97
ts/components/conversationList/ContactCheckbox.tsx
Normal file
|
@ -0,0 +1,97 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { CSSProperties, FunctionComponent } from 'react';
|
||||
|
||||
import { BaseConversationListItem } from './BaseConversationListItem';
|
||||
import { ColorType } from '../../types/Colors';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import { ContactName } from '../conversation/ContactName';
|
||||
import { About } from '../conversation/About';
|
||||
|
||||
export enum ContactCheckboxDisabledReason {
|
||||
// We start the enum at 1 because the default starting value of 0 is falsy.
|
||||
MaximumContactsSelected = 1,
|
||||
NotCapable,
|
||||
}
|
||||
|
||||
export type PropsDataType = {
|
||||
about?: string;
|
||||
avatarPath?: string;
|
||||
color?: ColorType;
|
||||
disabledReason?: ContactCheckboxDisabledReason;
|
||||
id: string;
|
||||
isChecked: boolean;
|
||||
name?: string;
|
||||
phoneNumber?: string;
|
||||
profileName?: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
type PropsHousekeepingType = {
|
||||
i18n: LocalizerType;
|
||||
style: CSSProperties;
|
||||
onClick: (
|
||||
id: string,
|
||||
disabledReason: undefined | ContactCheckboxDisabledReason
|
||||
) => void;
|
||||
};
|
||||
|
||||
type PropsType = PropsDataType & PropsHousekeepingType;
|
||||
|
||||
export const ContactCheckbox: FunctionComponent<PropsType> = React.memo(
|
||||
({
|
||||
about,
|
||||
avatarPath,
|
||||
color,
|
||||
disabledReason,
|
||||
i18n,
|
||||
id,
|
||||
isChecked,
|
||||
name,
|
||||
onClick,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
style,
|
||||
title,
|
||||
}) => {
|
||||
const disabled = Boolean(disabledReason);
|
||||
|
||||
const headerName = (
|
||||
<ContactName
|
||||
phoneNumber={phoneNumber}
|
||||
name={name}
|
||||
profileName={profileName}
|
||||
title={title}
|
||||
i18n={i18n}
|
||||
/>
|
||||
);
|
||||
|
||||
const messageText = about ? <About className="" text={about} /> : null;
|
||||
|
||||
const onClickItem = () => {
|
||||
onClick(id, disabledReason);
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseConversationListItem
|
||||
avatarPath={avatarPath}
|
||||
checked={isChecked}
|
||||
color={color}
|
||||
conversationType="direct"
|
||||
disabled={disabled}
|
||||
headerName={headerName}
|
||||
i18n={i18n}
|
||||
id={id}
|
||||
isSelected={false}
|
||||
messageText={messageText}
|
||||
name={name}
|
||||
onClick={onClickItem}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
style={style}
|
||||
title={title}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useCallback, CSSProperties, FunctionComponent } from 'react';
|
||||
import React, { CSSProperties, FunctionComponent } from 'react';
|
||||
|
||||
import { BaseConversationListItem } from './BaseConversationListItem';
|
||||
import { ColorType } from '../../types/Colors';
|
||||
|
@ -25,7 +25,7 @@ export type PropsDataType = {
|
|||
type PropsHousekeepingType = {
|
||||
i18n: LocalizerType;
|
||||
style: CSSProperties;
|
||||
onClick: (id: string) => void;
|
||||
onClick?: (id: string) => void;
|
||||
};
|
||||
|
||||
type PropsType = PropsDataType & PropsHousekeepingType;
|
||||
|
@ -61,8 +61,6 @@ export const ContactListItem: FunctionComponent<PropsType> = React.memo(
|
|||
const messageText =
|
||||
about && !isMe ? <About className="" text={about} /> : null;
|
||||
|
||||
const onClickItem = useCallback(() => onClick(id), [onClick, id]);
|
||||
|
||||
return (
|
||||
<BaseConversationListItem
|
||||
avatarPath={avatarPath}
|
||||
|
@ -75,7 +73,7 @@ export const ContactListItem: FunctionComponent<PropsType> = React.memo(
|
|||
isSelected={false}
|
||||
messageText={messageText}
|
||||
name={name}
|
||||
onClick={onClickItem}
|
||||
onClick={onClick ? () => onClick(id) : undefined}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
style={style}
|
||||
|
|
32
ts/components/conversationList/CreateNewGroupButton.tsx
Normal file
32
ts/components/conversationList/CreateNewGroupButton.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { CSSProperties, FunctionComponent } from 'react';
|
||||
|
||||
import { BaseConversationListItem } from './BaseConversationListItem';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
onClick: () => void;
|
||||
style: CSSProperties;
|
||||
};
|
||||
|
||||
export const CreateNewGroupButton: FunctionComponent<PropsType> = React.memo(
|
||||
({ i18n, onClick, style }) => {
|
||||
const title = i18n('createNewGroupButton');
|
||||
|
||||
return (
|
||||
<BaseConversationListItem
|
||||
color="grey"
|
||||
conversationType="group"
|
||||
headerName={title}
|
||||
i18n={i18n}
|
||||
isSelected={false}
|
||||
onClick={onClick}
|
||||
style={style}
|
||||
title={title}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
Loading…
Add table
Add a link
Reference in a new issue