import React, { FC, memo, useCallback, useLayoutEffect, useRef, } from '../../../lib/teact/teact'; import { withGlobal } from '../../../lib/teact/teactn'; import useLang, { LangFn } from '../../../hooks/useLang'; import { GlobalActions, MessageListType } from '../../../global/types'; import { ApiChat, ApiUser, ApiMessage, ApiMessageOutgoingStatus, ApiFormattedText, MAIN_THREAD_ID, } from '../../../api/types'; import { ANIMATION_END_DELAY } from '../../../config'; import { IS_MOBILE_SCREEN } from '../../../util/environment'; import { getChatTitle, isChatPrivate, isActionMessage, getPrivateChatUserId, getMessageAction, getMessageSenderName, isChatChannel, getMessageMediaHash, getMessageSummaryText, getMessageMediaThumbDataUri, getMessageVideo, getMessageSticker, } from '../../../modules/helpers'; import { selectChat, selectUser, selectChatMessage, selectOutgoingStatus, selectDraft, selectCurrentMessageList, } from '../../../modules/selectors'; import { renderActionMessageText } from '../../common/helpers/renderActionMessageText'; import renderText from '../../common/helpers/renderText'; import { fastRaf } from '../../../util/schedulers'; import buildClassName from '../../../util/buildClassName'; import { pick } from '../../../util/iteratees'; import useEnsureMessage from '../../../hooks/useEnsureMessage'; import useChatContextActions from '../../../hooks/useChatContextActions'; import useFlag from '../../../hooks/useFlag'; import useMedia from '../../../hooks/useMedia'; import { ChatAnimationTypes } from './hooks'; import Avatar from '../../common/Avatar'; import VerifiedIcon from '../../common/VerifiedIcon'; import TypingStatus from '../../common/TypingStatus'; import LastMessageMeta from '../../common/LastMessageMeta'; import DeleteChatModal from '../../common/DeleteChatModal'; import ListItem from '../../ui/ListItem'; import Badge from './Badge'; import './Chat.scss'; type OwnProps = { style?: string; chatId: number; folderId?: number; orderDiff: number; animationType: ChatAnimationTypes; isSelected: boolean; isPinned?: boolean; }; type StateProps = { chat?: ApiChat; privateChatUser?: ApiUser; actionTargetUser?: ApiUser; actionTargetMessage?: ApiMessage; actionTargetChatId?: number; lastMessageSender?: ApiUser; lastMessageOutgoingStatus?: ApiMessageOutgoingStatus; draft?: ApiFormattedText; messageListType?: MessageListType; animationLevel?: number; lastSyncTime?: number; }; type DispatchProps = Pick; const ANIMATION_DURATION = 200; const Chat: FC = ({ style, chatId, folderId, orderDiff, animationType, isSelected, isPinned, chat, privateChatUser, actionTargetUser, lastMessageSender, lastMessageOutgoingStatus, actionTargetMessage, actionTargetChatId, draft, messageListType, animationLevel, lastSyncTime, openChat, focusLastMessage, }) => { // eslint-disable-next-line no-null/no-null const ref = useRef(null); const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag(); const { lastMessage, typingStatus } = chat || {}; const isAction = lastMessage && isActionMessage(lastMessage); useEnsureMessage(chatId, isAction ? lastMessage!.replyToMessageId : undefined, actionTargetMessage); const mediaThumbnail = lastMessage && !getMessageSticker(lastMessage) ? getMessageMediaThumbDataUri(lastMessage) : undefined; const mediaBlobUrl = useMedia(lastMessage ? getMessageMediaHash(lastMessage, 'micro') : undefined); // Sets animation excess values when `orderDiff` changes and then resets excess values to animate. useLayoutEffect(() => { if (animationLevel === 0) { return; } const element = ref.current!; // TODO Refactor animation: create `useListAnimation` that owns `orderDiff` and `animationType` if (animationType === ChatAnimationTypes.Opacity) { element.style.opacity = '0'; fastRaf(() => { element.classList.add('animate-opacity'); element.style.opacity = '1'; }); } else if (animationType === ChatAnimationTypes.Move) { element.style.transform = `translate3d(0, ${-orderDiff * 100}%, 0)`; fastRaf(() => { element.classList.add('animate-transform'); element.style.transform = ''; }); } else { return; } setTimeout(() => { fastRaf(() => { element.classList.remove('animate-opacity', 'animate-transform'); element.style.opacity = ''; element.style.transform = ''; }); }, ANIMATION_DURATION + ANIMATION_END_DELAY); }, [animationLevel, orderDiff, animationType]); const handleClick = useCallback(() => { openChat({ id: chatId }); if (isSelected && messageListType === 'thread') { focusLastMessage(); } }, [ isSelected, messageListType, openChat, chatId, focusLastMessage, ]); const contextActions = useChatContextActions({ chat, privateChatUser, handleDelete: openDeleteModal, folderId, isPinned, }); const lang = useLang(); if (!chat) { return undefined; } function renderLastMessageOrTyping() { if (typingStatus && lastMessage && typingStatus.timestamp > lastMessage.date * 1000) { return ; } if (draft && draft.text.length) { return (

{lang('Draft')} {renderText(draft.text)}

); } if (!lastMessage) { return undefined; } if (isAction) { const actionOrigin = chat && (isChatChannel(chat) || lastMessage.senderId === lastMessage.chatId) ? chat : lastMessageSender; return (

{renderText(renderActionMessageText( lang, lastMessage, actionOrigin, actionTargetUser, actionTargetMessage, actionTargetChatId, { asPlain: true }, ) as string)}

); } const senderName = getMessageSenderName(chatId, lastMessageSender); return (

{senderName && ( {renderText(senderName)} )} {renderMessageSummary(lang, lastMessage!, mediaBlobUrl || mediaThumbnail)}

); } const className = buildClassName( 'Chat chat-item-clickable', isChatPrivate(chatId) ? 'private' : 'group', isSelected && !IS_MOBILE_SCREEN && 'selected', ); return (

{renderText(getChatTitle(chat, privateChatUser))}

{chat.isVerified && } {chat.isMuted && } {chat.lastMessage && ( )}
{renderLastMessageOrTyping()}
); }; function renderMessageSummary(lang: LangFn, message: ApiMessage, blobUrl?: string) { if (!blobUrl) { return renderText(getMessageSummaryText(lang, message)); } return ( {getMessageVideo(message) && } {renderText(getMessageSummaryText(lang, message, true))} ); } export default memo(withGlobal( (global, { chatId }): StateProps => { const chat = selectChat(global, chatId); if (!chat || !chat.lastMessage) { return {}; } const { senderId, replyToMessageId, isOutgoing } = chat.lastMessage; const lastMessageSender = senderId ? selectUser(global, senderId) : undefined; const lastMessageAction = getMessageAction(chat.lastMessage); const actionTargetMessage = lastMessageAction && replyToMessageId ? selectChatMessage(global, chat.id, replyToMessageId) : undefined; const { targetUserId: actionTargetUserId, targetChatId: actionTargetChatId } = lastMessageAction || {}; const privateChatUserId = getPrivateChatUserId(chat); const { type: messageListType } = selectCurrentMessageList(global) || {}; return { chat, lastMessageSender, ...(isOutgoing && { lastMessageOutgoingStatus: selectOutgoingStatus(global, chat.lastMessage) }), ...(privateChatUserId && { privateChatUser: selectUser(global, privateChatUserId) }), ...(actionTargetUserId && { actionTargetUser: selectUser(global, actionTargetUserId) }), actionTargetChatId, actionTargetMessage, draft: selectDraft(global, chatId, MAIN_THREAD_ID), messageListType, animationLevel: global.settings.byKey.animationLevel, lastSyncTime: global.lastSyncTime, }; }, (setGlobal, actions): DispatchProps => pick(actions, [ 'openChat', 'focusLastMessage', ]), )(Chat));