import { useMutation } from '@tanstack/react-query'; import clsx from 'clsx'; import escape from 'lodash/escape'; import React, { useMemo, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import DropdownMenu from 'pl-fe/components/dropdown-menu'; import { ParsedContent } from 'pl-fe/components/parsed-content'; import HStack from 'pl-fe/components/ui/hstack'; import Icon from 'pl-fe/components/ui/icon'; import Stack from 'pl-fe/components/ui/stack'; import Text from 'pl-fe/components/ui/text'; import { MediaGallery } from 'pl-fe/features/ui/util/async-components'; import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; import { ChatKeys, useChatActions } from 'pl-fe/queries/chats'; import { queryClient } from 'pl-fe/queries/client'; import { useModalsActions } from 'pl-fe/stores/modals'; import { stripHTML } from 'pl-fe/utils/html'; import { onlyEmoji } from 'pl-fe/utils/rich-content'; import type { Chat } from 'pl-api'; import type { Menu as IMenu } from 'pl-fe/components/dropdown-menu'; import type { ChatMessage as ChatMessageEntity } from 'pl-fe/normalizers/chat-message'; const messages = defineMessages({ copy: { id: 'chats.actions.copy', defaultMessage: 'Copy' }, delete: { id: 'chats.actions.delete', defaultMessage: 'Delete for both' }, deleteForMe: { id: 'chats.actions.delete_for_me', defaultMessage: 'Delete for me' }, more: { id: 'chats.actions.more', defaultMessage: 'More' }, }); const BIG_EMOJI_LIMIT = 3; const parsePendingContent = (content: string) => escape(content).replace(/(?:\r\n|\r|\n)/g, '
'); const parseContent = (chatMessage: ChatMessageEntity) => { const content = chatMessage.content || ''; const pending = chatMessage.pending; const deleting = chatMessage.deleting; const formatted = (pending && !deleting) ? parsePendingContent(content) : content; return formatted; }; interface IChatMessage { chat: Chat; chatMessage: ChatMessageEntity; } const ChatMessage = (props: IChatMessage) => { const { chat, chatMessage } = props; const { openModal } = useModalsActions(); const intl = useIntl(); const me = useAppSelector((state) => state.me); const { deleteChatMessage } = useChatActions(chat.id); const [isMenuOpen, setIsMenuOpen] = useState(false); const handleDeleteMessage = useMutation({ mutationFn: (chatMessageId: string) => deleteChatMessage(chatMessageId), onSettled: () => { queryClient.invalidateQueries({ queryKey: ChatKeys.chatMessages(chat.id), }); }, }); const content = parseContent(chatMessage); const isMyMessage = chatMessage.account_id === me; const isOnlyEmoji = useMemo(() => { const hiddenEl = document.createElement('div'); hiddenEl.innerHTML = content; return onlyEmoji(hiddenEl, BIG_EMOJI_LIMIT, false); }, []); const onOpenMedia = (media: any, index: number) => { openModal('MEDIA', { media, index }); }; const maybeRenderMedia = (chatMessage: ChatMessageEntity) => { if (!chatMessage.attachment) return null; return ( ); }; const handleCopyText = (chatMessage: ChatMessageEntity) => { if (navigator.clipboard) { const text = stripHTML(chatMessage.content); navigator.clipboard.writeText(text); } }; const setBubbleRef = (c: HTMLDivElement) => { if (!c) return; const links = c.querySelectorAll('a[rel="ugc"]'); links.forEach(link => { link.classList.add('chat-link'); link.setAttribute('rel', 'ugc nofollow noopener'); link.setAttribute('target', '_blank'); }); }; const getFormattedTimestamp = (chatMessage: ChatMessageEntity) => intl.formatDate(new Date(chatMessage.created_at), { hour12: false, year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit', }); const menu = useMemo(() => { const menu: IMenu = []; if (navigator.clipboard && chatMessage.content) { menu.push({ text: intl.formatMessage(messages.copy), action: () => handleCopyText(chatMessage), icon: require('@phosphor-icons/core/regular/clipboard.svg'), }); } if (isMyMessage) { menu.push({ text: intl.formatMessage(messages.delete), action: () => handleDeleteMessage.mutate(chatMessage.id), icon: require('@phosphor-icons/core/regular/trash.svg'), destructive: true, }); } else { menu.push({ text: intl.formatMessage(messages.deleteForMe), action: () => handleDeleteMessage.mutate(chatMessage.id), icon: require('@phosphor-icons/core/regular/trash.svg'), destructive: true, }); } return menu; }, [chatMessage, chat]); return (
{menu.length > 0 && ( setIsMenuOpen(true)} onClose={() => setIsMenuOpen(false)} > )}
{maybeRenderMedia(chatMessage)} {content && (
)}
{intl.formatTime(chatMessage.created_at)}
); }; export { ChatMessage as default };