You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
351 lines
11 KiB
TypeScript
351 lines
11 KiB
TypeScript
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 (
|
|
<StyledMessageBody>
|
|
<Message messageId={messageId} isDetailView={true} />
|
|
</StyledMessageBody>
|
|
);
|
|
};
|
|
|
|
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<Error>;
|
|
attachments: Array<PropsForAttachment>;
|
|
};
|
|
|
|
async function getPropsForMessageInfo(
|
|
messageId: string | undefined,
|
|
attachments: Array<PropsForAttachment>
|
|
): Promise<MessageInfoProps | null> {
|
|
if (!messageId) {
|
|
return null;
|
|
}
|
|
const found = await Data.getMessageById(messageId);
|
|
const attachmentsWithMediaDetails: Array<PropsForAttachment> = [];
|
|
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<MessageInfoProps | null>(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 (
|
|
<StyledScrollContainer>
|
|
<Flex container={true} flexDirection={'column'} alignItems={'center'}>
|
|
<Header hideBackButton={true} closeButtonOnClick={closePanel}>
|
|
<HeaderTitle>{window.i18n('messageInfo')}</HeaderTitle>
|
|
</Header>
|
|
<StyledMessageDetailContainer>
|
|
<StyledMessageDetail>
|
|
<MessageBody
|
|
messageId={messageId}
|
|
supportsAttachmentCarousel={supportsAttachmentCarousel}
|
|
/>
|
|
{hasAttachments && (
|
|
<>
|
|
{supportsAttachmentCarousel && (
|
|
<>
|
|
<AttachmentCarousel
|
|
messageId={messageId}
|
|
attachments={attachments}
|
|
visibleIndex={visibleAttachmentIndex}
|
|
nextAction={() => {
|
|
handleChangeAttachment(1);
|
|
}}
|
|
previousAction={() => {
|
|
handleChangeAttachment(-1);
|
|
}}
|
|
/>
|
|
<SpacerXL />
|
|
</>
|
|
)}
|
|
<AttachmentInfo attachment={attachments[visibleAttachmentIndex]} />
|
|
<SpacerMD />
|
|
</>
|
|
)}
|
|
<MessageInfo messageId={messageId} errors={messageInfo.errors} />
|
|
<SpacerLG />
|
|
<PanelButtonGroup style={{ margin: '0' }}>
|
|
<PanelIconButton
|
|
text={window.i18n('replyToMessage')}
|
|
iconType="reply"
|
|
onClick={() => {
|
|
// 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' && (
|
|
<PanelIconButton
|
|
text={window.i18n('resend')}
|
|
iconType="resend"
|
|
onClick={() => {
|
|
void resendMessage(messageId);
|
|
dispatch(closeRightPanel());
|
|
dispatch(resetRightOverlayMode());
|
|
}}
|
|
dataTestId="resend-msg-from-details"
|
|
/>
|
|
)}
|
|
{hasAttachments && (
|
|
<PanelIconButton
|
|
text={window.i18n('save')}
|
|
iconType="saveToDisk"
|
|
dataTestId="save-attachment-from-details"
|
|
onClick={() => {
|
|
if (hasAttachments) {
|
|
void saveAttachmentToDisk({
|
|
conversationId: convoId,
|
|
messageSender: sender,
|
|
messageTimestamp: serverTimestamp || timestamp || Date.now(),
|
|
attachment: attachments[visibleAttachmentIndex],
|
|
index: visibleAttachmentIndex,
|
|
});
|
|
}
|
|
}}
|
|
/>
|
|
)}
|
|
{isDeletable && (
|
|
<PanelIconButton
|
|
text={window.i18n('delete')}
|
|
iconType="delete"
|
|
color={'var(--danger-color)'}
|
|
dataTestId="delete-from-details"
|
|
onClick={() => {
|
|
void deleteMessagesById([messageId], convoId);
|
|
}}
|
|
/>
|
|
)}
|
|
</PanelButtonGroup>
|
|
<SpacerXL />
|
|
</StyledMessageDetail>
|
|
</StyledMessageDetailContainer>
|
|
</Flex>
|
|
</StyledScrollContainer>
|
|
);
|
|
};
|