import React, { useCallback, useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components'; // tslint:disable-next-line: no-submodule-imports import useKey from 'react-use/lib/useKey'; import { PropsForAttachment, closeRightPanel } from '../../../../../state/ducks/conversations'; import { resetRightOverlayMode, setRightOverlayMode } from '../../../../../state/ducks/section'; import { getMessageInfoId } from '../../../../../state/selectors/conversations'; import { Flex } from '../../../../basic/Flex'; import { Header, HeaderTitle, StyledScrollContainer } from '../components'; import { Data } from '../../../../../data/data'; import { useRightOverlayMode } from '../../../../../hooks/useUI'; import { replyToMessage, resendMessage, } from '../../../../../interactions/conversationInteractions'; import { deleteMessagesById } from '../../../../../interactions/conversations/unsendingInteractions'; import { useMessageAttachments, useMessageDirection, useMessageIsDeletable, useMessageQuote, useMessageSender, useMessageServerTimestamp, useMessageText, useMessageTimestamp, } from '../../../../../state/selectors'; import { useSelectedConversationKey } from '../../../../../state/selectors/selectedConversation'; import { canDisplayImage } from '../../../../../types/Attachment'; import { isAudio } from '../../../../../types/MIME'; import { getAudioDuration, getVideoDuration, } from '../../../../../types/attachments/VisualAttachment'; import { GoogleChrome } from '../../../../../util'; import { saveAttachmentToDisk } from '../../../../../util/attachmentsUtil'; import { SpacerLG, SpacerMD, SpacerXL } from '../../../../basic/Text'; import { PanelButtonGroup, PanelIconButton } from '../../../../buttons'; import { Message } from '../../../message/message-item/Message'; import { AttachmentInfo, MessageInfo } from './components'; import { AttachmentCarousel } from './components/AttachmentCarousel'; // NOTE we override the default max-widths when in the detail isDetailView const StyledMessageBody = styled.div` padding-bottom: var(--margins-lg); .module-message { pointer-events: none; max-width: 100%; @media (min-width: 1200px) { max-width: 100%; } } `; const MessageBody = ({ messageId, supportsAttachmentCarousel, }: { messageId: string; supportsAttachmentCarousel: boolean; }) => { const quote = useMessageQuote(messageId); const text = useMessageText(messageId); // NOTE we don't want to render the message body if it's empty and the attachments carousel can render it instead if (supportsAttachmentCarousel && !text && !quote) { return null; } return ( ); }; const StyledMessageDetailContainer = styled.div` height: calc(100% - 48px); width: 100%; overflow-y: auto; z-index: 2; `; const StyledMessageDetail = styled.div` max-width: 650px; margin-inline-start: auto; margin-inline-end: auto; padding: var(--margins-sm) var(--margins-2xl) var(--margins-lg); `; type MessageInfoProps = { errors: Array; attachments: Array; }; async function getPropsForMessageInfo( messageId: string | undefined, attachments: Array ): Promise { if (!messageId) { return null; } const found = await Data.getMessageById(messageId); const attachmentsWithMediaDetails: Array = []; if (found) { // process attachments so we have the fileSize, url and screenshots for (let i = 0; i < attachments.length; i++) { const props = found.getPropsForAttachment(attachments[i]); if ( props?.contentType && GoogleChrome.isVideoTypeSupported(props?.contentType) && !props.duration && props.url ) { // eslint-disable-next-line no-await-in-loop const duration = await getVideoDuration({ objectUrl: props.url, contentType: props.contentType, }); attachmentsWithMediaDetails.push({ ...props, duration, }); } else if (props?.contentType && isAudio(props.contentType) && !props.duration && props.url) { // eslint-disable-next-line no-await-in-loop const duration = await getAudioDuration({ objectUrl: props.url, contentType: props.contentType, }); attachmentsWithMediaDetails.push({ ...props, duration, }); } else if (props) { attachmentsWithMediaDetails.push(props); } } // This will make the error message for outgoing key errors a bit nicer const errors = (found.get('errors') || []).map((error: any) => { return error; }); const toRet: MessageInfoProps = { errors, attachments: attachmentsWithMediaDetails, }; return toRet; } return null; } function useMessageInfo(messageId: string | undefined) { const [details, setDetails] = useState(null); const fromState = useMessageAttachments(messageId); // this is not ideal, but also doesn't seem to create any performance issue at the moment. // TODO: ideally, we'd want to save the attachment duration anytime we save one to the disk (incoming/outgoing), and just retrieve it from the redux state here. useEffect(() => { let mounted = true; // eslint-disable-next-line more/no-then void getPropsForMessageInfo(messageId, fromState || []) .then(result => { if (mounted) { setDetails(result); } }) .catch(window.log.error); return () => { mounted = false; }; }, [fromState, messageId]); return details; } export const OverlayMessageInfo = () => { const dispatch = useDispatch(); const rightOverlayMode = useRightOverlayMode(); const messageId = useSelector(getMessageInfoId); const messageInfo = useMessageInfo(messageId); const isDeletable = useMessageIsDeletable(messageId); const direction = useMessageDirection(messageId); const timestamp = useMessageTimestamp(messageId); const serverTimestamp = useMessageServerTimestamp(messageId); const sender = useMessageSender(messageId); // we close the right panel when switching conversation so the convoId of that message is always the selectedConversationKey // is always the currently selected conversation const convoId = useSelectedConversationKey(); const closePanel = useCallback(() => { dispatch(closeRightPanel()); dispatch(resetRightOverlayMode()); }, [dispatch]); useKey('Escape', closePanel); // close the panel if the messageInfo is associated with a deleted message useEffect(() => { if (!sender) { closePanel(); } }, [sender, closePanel]); if (!rightOverlayMode || !messageInfo || !convoId || !messageId || !sender) { return null; } const { params } = rightOverlayMode; const visibleAttachmentIndex = params?.visibleAttachmentIndex || 0; const { errors, attachments } = messageInfo; const hasAttachments = attachments && attachments.length > 0; const supportsAttachmentCarousel = canDisplayImage(attachments); const hasErrors = errors && errors.length > 0; const handleChangeAttachment = (changeDirection: 1 | -1) => { if (!hasAttachments) { return; } const newVisibleIndex = visibleAttachmentIndex + changeDirection; if (newVisibleIndex > attachments.length - 1) { return; } if (newVisibleIndex < 0) { return; } if (attachments[newVisibleIndex]) { dispatch( setRightOverlayMode({ type: 'message_info', params: { messageId, visibleAttachmentIndex: newVisibleIndex }, }) ); } }; return (
{window.i18n('messageInfo')}
{hasAttachments && ( <> {supportsAttachmentCarousel && ( <> { handleChangeAttachment(1); }} previousAction={() => { handleChangeAttachment(-1); }} /> )} )} { // eslint-disable-next-line more/no-then void replyToMessage(messageId).then(foundIt => { if (foundIt) { dispatch(closeRightPanel()); dispatch(resetRightOverlayMode()); } }); }} dataTestId="reply-to-msg-from-details" /> {hasErrors && direction === 'outgoing' && ( { void resendMessage(messageId); dispatch(closeRightPanel()); dispatch(resetRightOverlayMode()); }} dataTestId="resend-msg-from-details" /> )} {hasAttachments && ( { if (hasAttachments) { void saveAttachmentToDisk({ conversationId: convoId, messageSender: sender, messageTimestamp: serverTimestamp || timestamp || Date.now(), attachment: attachments[visibleAttachmentIndex], index: visibleAttachmentIndex, }); } }} /> )} {isDeletable && ( { void deleteMessagesById([messageId], convoId); }} /> )}
); };