// Copyright 2018-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { FunctionComponent, ReactNode } from 'react'; import React, { useCallback } from 'react'; import classNames from 'classnames'; import { BaseConversationListItem, HEADER_NAME_CLASS_NAME, HEADER_CONTACT_NAME_CLASS_NAME, MESSAGE_TEXT_CLASS_NAME, } from './BaseConversationListItem'; import { MessageBody } from '../conversation/MessageBody'; import { ContactName } from '../conversation/ContactName'; import { TypingAnimation } from '../conversation/TypingAnimation'; import type { LocalizerType, ThemeType } from '../../types/Util'; import type { ConversationType } from '../../state/ducks/conversations'; import type { BadgeType } from '../../badges/types'; const MESSAGE_STATUS_ICON_CLASS_NAME = `${MESSAGE_TEXT_CLASS_NAME}__status-icon`; export const MessageStatuses = [ 'sending', 'sent', 'delivered', 'read', 'paused', 'error', 'partial-sent', ] as const; export type MessageStatusType = typeof MessageStatuses[number]; export type PropsData = Pick< ConversationType, | 'acceptedMessageRequest' | 'avatarPath' | 'badges' | 'color' | 'draftPreview' | 'id' | 'isMe' // NOTE: Passed for CI, not used for rendering | 'isPinned' | 'isSelected' | 'lastMessage' | 'lastUpdated' | 'markedUnread' | 'muteExpiresAt' | 'phoneNumber' | 'profileName' | 'sharedGroupNames' | 'shouldShowDraft' | 'title' | 'type' | 'typingContactId' | 'unblurredAvatarPath' | 'unreadCount' > & { badge?: BadgeType; }; type PropsHousekeeping = { i18n: LocalizerType; onClick: (id: string) => void; theme: ThemeType; }; export type Props = PropsData & PropsHousekeeping; export const ConversationListItem: FunctionComponent = React.memo( function ConversationListItem({ acceptedMessageRequest, avatarPath, badge, color, draftPreview, i18n, id, isMe, isSelected, lastMessage, lastUpdated, markedUnread, muteExpiresAt, onClick, phoneNumber, profileName, sharedGroupNames, shouldShowDraft, theme, title, type, typingContactId, unblurredAvatarPath, unreadCount, }) { const isMuted = Boolean(muteExpiresAt && Date.now() < muteExpiresAt); const headerName = ( <> {isMe ? ( {i18n('noteToSelf')} ) : ( )} {isMuted &&
} ); let messageText: ReactNode = null; let messageStatusIcon: ReactNode = null; if (!acceptedMessageRequest) { messageText = ( {i18n('ConversationListItem--message-request')} ); } else if (typingContactId) { messageText = ; } else if (shouldShowDraft && draftPreview) { messageText = ( <> {i18n('ConversationListItem--draft-prefix')} ); } else if (lastMessage?.deletedForEveryone) { messageText = ( {i18n('message--deletedForEveryone')} ); } else if (lastMessage) { messageText = ( ); if (lastMessage.status) { messageStatusIcon = (
); } } const onClickItem = useCallback(() => onClick(id), [onClick, id]); return ( ); } ); // This takes `unknown` because, sometimes, values from the database don't match our // types. In the long term, we should fix that. In the short term, this smooths over the // problem. function truncateMessageText(text: unknown): string { if (typeof text !== 'string') { return ''; } return text.replace(/(?:\r?\n)+/g, ' '); }