// Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { ReactNode, FunctionComponent } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import classNames from 'classnames'; import { isBoolean, isNumber } from 'lodash'; import { Avatar, AvatarSize } from '../Avatar'; import type { BadgeType } from '../../badges/types'; import { isConversationUnread } from '../../util/isConversationUnread'; import { cleanId } from '../_util'; import type { LocalizerType, ThemeType } from '../../types/Util'; import type { ConversationType } from '../../state/ducks/conversations'; import { Spinner } from '../Spinner'; import { Time } from '../Time'; import { formatDateTimeShort } from '../../util/timestamp'; import * as durations from '../../util/durations'; import { UUID } from '../../types/UUID'; const BASE_CLASS_NAME = 'module-conversation-list__item--contact-or-conversation'; const AVATAR_CONTAINER_CLASS_NAME = `${BASE_CLASS_NAME}__avatar-container`; const CONTENT_CLASS_NAME = `${BASE_CLASS_NAME}__content`; const HEADER_CLASS_NAME = `${CONTENT_CLASS_NAME}__header`; 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 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`; export const SPINNER_CLASS_NAME = `${BASE_CLASS_NAME}__spinner`; type PropsType = { checked?: boolean; conversationType: 'group' | 'direct'; disabled?: boolean; headerDate?: number; headerName: ReactNode; id?: string; i18n: LocalizerType; isNoteToSelf?: boolean; isSelected: boolean; isUsernameSearchResult?: boolean; markedUnread?: boolean; messageId?: string; messageStatusIcon?: ReactNode; messageText?: ReactNode; messageTextIsAlwaysFullSize?: boolean; onClick?: () => void; shouldShowSpinner?: boolean; unreadCount?: number; avatarSize?: AvatarSize; testId?: string; } & Pick< ConversationType, | 'acceptedMessageRequest' | 'avatarPath' | 'color' | 'groupId' | 'isMe' | 'markedUnread' | 'phoneNumber' | 'profileName' | 'sharedGroupNames' | 'title' | 'unblurredAvatarPath' | 'uuid' > & ( | { badge?: undefined; theme?: ThemeType } | { badge: BadgeType; theme: ThemeType } ); export const BaseConversationListItem: FunctionComponent = React.memo(function BaseConversationListItem(props) { const { acceptedMessageRequest, avatarPath, avatarSize, checked, color, conversationType, disabled, groupId, headerDate, headerName, i18n, id, isMe, isNoteToSelf, isUsernameSearchResult, isSelected, markedUnread, messageStatusIcon, messageText, messageTextIsAlwaysFullSize, onClick, phoneNumber, profileName, sharedGroupNames, shouldShowSpinner, testId: overrideTestId, title, unblurredAvatarPath, unreadCount, uuid, } = props; const identifier = id ? cleanId(id) : undefined; const htmlId = useMemo(() => UUID.generate().toString(), []); const testId = overrideTestId || groupId || uuid; const isUnread = isConversationUnread({ markedUnread, unreadCount }); const isAvatarNoteToSelf = isBoolean(isNoteToSelf) ? isNoteToSelf : Boolean(isMe); const isCheckbox = isBoolean(checked); let actionNode: ReactNode; if (shouldShowSpinner) { actionNode = ( ); } else if (isCheckbox) { let ariaLabel: string; if (disabled) { ariaLabel = i18n('cannotSelectContact', [title]); } else if (checked) { ariaLabel = i18n('deselectContact', [title]); } else { ariaLabel = i18n('selectContact', [title]); } actionNode = (
{ if (onClick && !disabled && event.key === 'Enter') { onClick(); } }} type="checkbox" />
); } const contents = ( <>
{headerName}
{messageText || isUnread ? (
{Boolean(messageText) && (
{messageText}
)} {messageStatusIcon}
) : null}
{actionNode} ); const commonClassNames = classNames(BASE_CLASS_NAME, { [`${BASE_CLASS_NAME}--is-selected`]: isSelected, }); if (isCheckbox) { return ( ); } if (onClick) { return ( ); } return (
{contents}
); }); function Timestamp({ i18n, timestamp, }: Readonly<{ i18n: LocalizerType; timestamp?: number }>) { const getText = useCallback( () => (isNumber(timestamp) ? formatDateTimeShort(i18n, timestamp) : ''), [i18n, timestamp] ); const [text, setText] = useState(getText()); useEffect(() => { const update = () => setText(getText()); update(); const interval = setInterval(update, durations.MINUTE); return () => { clearInterval(interval); }; }, [getText]); if (!isNumber(timestamp)) { return null; } return ( ); } function UnreadIndicator({ count = 0, isUnread, }: Readonly<{ count?: number; isUnread: boolean }>) { if (!isUnread) { return null; } return (
{Boolean(count) && count}
); }