signal-desktop/ts/components/conversationList/BaseConversationListItem.tsx

249 lines
7 KiB
TypeScript
Raw Normal View History

// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { ReactNode, FunctionComponent, useMemo } from 'react';
import classNames from 'classnames';
import { isBoolean, isNumber } from 'lodash';
import { v4 as uuid } from 'uuid';
import { Avatar, AvatarSize } from '../Avatar';
import { Timestamp } from '../conversation/Timestamp';
import { isConversationUnread } from '../../util/isConversationUnread';
import { cleanId } from '../_util';
import { LocalizerType } from '../../types/Util';
import { ConversationType } from '../../state/ducks/conversations';
const BASE_CLASS_NAME =
'module-conversation-list__item--contact-or-conversation';
const CONTENT_CLASS_NAME = `${BASE_CLASS_NAME}__content`;
const HEADER_CLASS_NAME = `${CONTENT_CLASS_NAME}__header`;
2021-10-14 15:48:48 +00:00
export const HEADER_NAME_CLASS_NAME = `${HEADER_CLASS_NAME}__name`;
export const HEADER_CONTACT_NAME_CLASS_NAME = `${HEADER_NAME_CLASS_NAME}__contact-name`;
export const DATE_CLASS_NAME = `${HEADER_CLASS_NAME}__date`;
const TIMESTAMP_CLASS_NAME = `${DATE_CLASS_NAME}__timestamp`;
const MESSAGE_CLASS_NAME = `${CONTENT_CLASS_NAME}__message`;
export const MESSAGE_TEXT_CLASS_NAME = `${MESSAGE_CLASS_NAME}__text`;
2021-03-03 20:09:58 +00:00
const CHECKBOX_CLASS_NAME = `${BASE_CLASS_NAME}__checkbox`;
type PropsType = {
2021-03-03 20:09:58 +00:00
checked?: boolean;
conversationType: 'group' | 'direct';
2021-03-03 20:09:58 +00:00
disabled?: boolean;
headerDate?: number;
headerName: ReactNode;
id?: string;
i18n: LocalizerType;
isNoteToSelf?: boolean;
isSelected: boolean;
markedUnread?: boolean;
messageId?: string;
messageStatusIcon?: ReactNode;
messageText?: ReactNode;
messageTextIsAlwaysFullSize?: boolean;
2021-03-03 20:09:58 +00:00
onClick?: () => void;
unreadCount?: number;
} & Pick<
ConversationType,
| 'acceptedMessageRequest'
| 'avatarPath'
| 'color'
| 'isMe'
| 'markedUnread'
| 'name'
| 'phoneNumber'
| 'profileName'
| 'sharedGroupNames'
| 'title'
| 'unblurredAvatarPath'
>;
export const BaseConversationListItem: FunctionComponent<PropsType> = React.memo(
2021-08-11 19:29:07 +00:00
function BaseConversationListItem({
acceptedMessageRequest,
avatarPath,
2021-03-03 20:09:58 +00:00
checked,
color,
conversationType,
2021-03-03 20:09:58 +00:00
disabled,
headerDate,
headerName,
i18n,
id,
isMe,
isNoteToSelf,
isSelected,
markedUnread,
messageStatusIcon,
messageText,
messageTextIsAlwaysFullSize,
name,
onClick,
phoneNumber,
profileName,
sharedGroupNames,
title,
unblurredAvatarPath,
unreadCount,
2021-08-11 19:29:07 +00:00
}) {
const identifier = id ? cleanId(id) : undefined;
const htmlId = useMemo(() => uuid(), []);
const isUnread = isConversationUnread({ markedUnread, unreadCount });
const isAvatarNoteToSelf = isBoolean(isNoteToSelf)
? isNoteToSelf
: Boolean(isMe);
2021-03-03 20:09:58 +00:00
const isCheckbox = isBoolean(checked);
let checkboxNode: ReactNode;
if (isCheckbox) {
let ariaLabel: string;
if (disabled) {
ariaLabel = i18n('cannotSelectContact', [title]);
2021-03-03 20:09:58 +00:00
} else if (checked) {
ariaLabel = i18n('deselectContact', [title]);
2021-03-03 20:09:58 +00:00
} else {
ariaLabel = i18n('selectContact', [title]);
2021-03-03 20:09:58 +00:00
}
checkboxNode = (
<input
aria-label={ariaLabel}
checked={checked}
className={CHECKBOX_CLASS_NAME}
disabled={disabled}
id={htmlId}
2021-03-03 20:09:58 +00:00
onChange={onClick}
2021-03-11 21:29:31 +00:00
onKeyDown={event => {
if (onClick && !disabled && event.key === 'Enter') {
onClick();
}
}}
2021-03-03 20:09:58 +00:00
type="checkbox"
/>
);
}
const contents = (
<>
<Avatar
acceptedMessageRequest={acceptedMessageRequest}
avatarPath={avatarPath}
color={color}
conversationType={conversationType}
noteToSelf={isAvatarNoteToSelf}
i18n={i18n}
isMe={isMe}
name={name}
phoneNumber={phoneNumber}
profileName={profileName}
title={title}
sharedGroupNames={sharedGroupNames}
size={AvatarSize.FORTY_EIGHT}
unblurredAvatarPath={unblurredAvatarPath}
/>
2021-03-03 20:09:58 +00:00
<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) && (
<div className={DATE_CLASS_NAME}>
<Timestamp
timestamp={headerDate}
extended={false}
module={TIMESTAMP_CLASS_NAME}
i18n={i18n}
/>
</div>
)}
</div>
{messageText || isUnread ? (
<div className={MESSAGE_CLASS_NAME}>
{Boolean(messageText) && (
<div
dir="auto"
className={classNames(
MESSAGE_TEXT_CLASS_NAME,
messageTextIsAlwaysFullSize &&
`${MESSAGE_TEXT_CLASS_NAME}--always-full-size`
)}
>
{messageText}
</div>
)}
{messageStatusIcon}
2021-10-14 20:21:10 +00:00
{isUnread && <UnreadCount count={unreadCount} />}
</div>
) : null}
</div>
2021-03-03 20:09:58 +00:00
{checkboxNode}
</>
);
const commonClassNames = classNames(BASE_CLASS_NAME, {
[`${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={identifier}
htmlFor={htmlId}
2021-03-03 20:09:58 +00:00
// `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
aria-label={i18n('BaseConversationListItem__aria-label', { title })}
2021-03-03 20:09:58 +00:00
className={classNames(
commonClassNames,
`${BASE_CLASS_NAME}--is-button`
)}
data-id={identifier}
2021-03-03 20:09:58 +00:00
disabled={disabled}
onClick={onClick}
type="button"
>
{contents}
</button>
);
}
return (
<div className={commonClassNames} data-id={identifier}>
2021-03-03 20:09:58 +00:00
{contents}
</div>
);
}
);
2021-10-14 20:21:10 +00:00
function UnreadCount({ count = 0 }: Readonly<{ count?: number }>) {
return (
<div
2021-10-19 13:53:30 +00:00
className={classNames(`${BASE_CLASS_NAME}__unread-count`, {
[`${BASE_CLASS_NAME}__unread-count--multiple-digits`]:
count > 9 && count <= 99,
[`${BASE_CLASS_NAME}__unread-count--many`]: count > 99,
})}
2021-10-14 20:21:10 +00:00
>
{Boolean(count) && Math.min(count, 99)}
</div>
);
}