feat: wip work
parent
c9a8ea2b81
commit
1f52b9620b
@ -0,0 +1,265 @@
|
|||||||
|
import React 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 {
|
||||||
|
deleteMessagesById,
|
||||||
|
deleteMessagesByIdForEveryone,
|
||||||
|
} from '../../../../../interactions/conversations/unsendingInteractions';
|
||||||
|
import { closeMessageDetailsView, closeRightPanel } from '../../../../../state/ducks/conversations';
|
||||||
|
import { resetRightOverlayMode, setRightOverlayMode } from '../../../../../state/ducks/section';
|
||||||
|
import { getMessageDetailsViewProps } from '../../../../../state/selectors/conversations';
|
||||||
|
import { Flex } from '../../../../basic/Flex';
|
||||||
|
import { Header, HeaderTitle, StyledScrollContainer } from '../components';
|
||||||
|
|
||||||
|
import {
|
||||||
|
replyToMessage,
|
||||||
|
resendMessage,
|
||||||
|
} from '../../../../../interactions/conversationInteractions';
|
||||||
|
import {
|
||||||
|
useMessageIsDeletable,
|
||||||
|
useMessageIsDeletableForEveryone,
|
||||||
|
useMessageQuote,
|
||||||
|
useMessageText,
|
||||||
|
} from '../../../../../state/selectors';
|
||||||
|
import { getRightOverlayMode } from '../../../../../state/selectors/section';
|
||||||
|
import { canDisplayImage } from '../../../../../types/Attachment';
|
||||||
|
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-lg) var(--margins-lg);
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const OverlayMessageInfo = () => {
|
||||||
|
const rightOverlayMode = useSelector(getRightOverlayMode);
|
||||||
|
const messageDetailProps = useSelector(getMessageDetailsViewProps);
|
||||||
|
const isDeletable = useMessageIsDeletable(messageDetailProps?.messageId);
|
||||||
|
const isDeletableForEveryone = useMessageIsDeletableForEveryone(messageDetailProps?.messageId);
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
useKey('Escape', () => {
|
||||||
|
dispatch(closeRightPanel());
|
||||||
|
dispatch(resetRightOverlayMode());
|
||||||
|
dispatch(closeMessageDetailsView());
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!rightOverlayMode || !messageDetailProps) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { params } = rightOverlayMode;
|
||||||
|
const visibleAttachmentIndex = params?.visibleAttachmentIndex || 0;
|
||||||
|
|
||||||
|
const {
|
||||||
|
convoId,
|
||||||
|
messageId,
|
||||||
|
sender,
|
||||||
|
attachments,
|
||||||
|
timestamp,
|
||||||
|
serverTimestamp,
|
||||||
|
errors,
|
||||||
|
direction,
|
||||||
|
} = messageDetailProps;
|
||||||
|
|
||||||
|
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={() => {
|
||||||
|
dispatch(closeRightPanel());
|
||||||
|
dispatch(resetRightOverlayMode());
|
||||||
|
dispatch(closeMessageDetailsView());
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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 />
|
||||||
|
<SpacerLG />
|
||||||
|
<PanelButtonGroup>
|
||||||
|
<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[0],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isDeletable && (
|
||||||
|
<PanelIconButton
|
||||||
|
text={window.i18n('deleteJustForMe')}
|
||||||
|
iconType="delete"
|
||||||
|
color={'var(--danger-color)'}
|
||||||
|
dataTestId="delete-for-me-from-details"
|
||||||
|
onClick={() => {
|
||||||
|
void deleteMessagesById([messageId], convoId);
|
||||||
|
dispatch(closeRightPanel());
|
||||||
|
dispatch(resetRightOverlayMode());
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isDeletableForEveryone && (
|
||||||
|
<PanelIconButton
|
||||||
|
text={window.i18n('deleteForEveryone')}
|
||||||
|
iconType="delete"
|
||||||
|
color={'var(--danger-color)'}
|
||||||
|
dataTestId="delete-for-everyone-from-details"
|
||||||
|
onClick={() => {
|
||||||
|
void deleteMessagesByIdForEveryone([messageId], convoId);
|
||||||
|
dispatch(closeRightPanel());
|
||||||
|
dispatch(resetRightOverlayMode());
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</PanelButtonGroup>
|
||||||
|
<SpacerXL />
|
||||||
|
</StyledMessageDetail>
|
||||||
|
</StyledMessageDetailContainer>
|
||||||
|
</Flex>
|
||||||
|
</StyledScrollContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,140 @@
|
|||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import styled, { CSSProperties } from 'styled-components';
|
||||||
|
import { PropsForAttachment } from '../../../../../../state/ducks/conversations';
|
||||||
|
import { getAlt, getThumbnailUrl, isVideoAttachment } from '../../../../../../types/Attachment';
|
||||||
|
import { Flex } from '../../../../../basic/Flex';
|
||||||
|
import { SessionIconButton } from '../../../../../icon';
|
||||||
|
import { Image } from '../../../../Image';
|
||||||
|
import {
|
||||||
|
StyledSubtitleDotMenu,
|
||||||
|
SubtitleDotMenu,
|
||||||
|
} from '../../../../header/ConversationHeaderSubtitle';
|
||||||
|
import { showLightboxFromAttachmentProps } from '../../../../message/message-content/MessageAttachment';
|
||||||
|
|
||||||
|
const CarouselButton = (props: { visible: boolean; rotation: number; onClick: () => void }) => {
|
||||||
|
return (
|
||||||
|
<SessionIconButton
|
||||||
|
iconSize={'huge'}
|
||||||
|
iconType={'chevron'}
|
||||||
|
iconRotation={props.rotation}
|
||||||
|
onClick={props.onClick}
|
||||||
|
iconPadding={'var(--margins-xs)'}
|
||||||
|
style={{
|
||||||
|
visibility: props.visible ? 'visible' : 'hidden',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledFullscreenButton = styled.div``;
|
||||||
|
|
||||||
|
const FullscreenButton = (props: { onClick: () => void; style?: CSSProperties }) => {
|
||||||
|
return (
|
||||||
|
<StyledFullscreenButton style={props.style}>
|
||||||
|
<SessionIconButton
|
||||||
|
iconSize={'large'}
|
||||||
|
iconColor={'var(--button-icon-stroke-hover-color)'}
|
||||||
|
iconType={'fullscreen'}
|
||||||
|
onClick={props.onClick}
|
||||||
|
iconPadding={'var(--margins-xs)'}
|
||||||
|
/>
|
||||||
|
</StyledFullscreenButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ImageContainer = styled.div`
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
${StyledSubtitleDotMenu} {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 12px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
${StyledFullscreenButton} {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 8px;
|
||||||
|
right: 8px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
messageId: string;
|
||||||
|
attachments: Array<PropsForAttachment>;
|
||||||
|
visibleIndex: number;
|
||||||
|
nextAction: () => void;
|
||||||
|
previousAction: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AttachmentCarousel = (props: Props) => {
|
||||||
|
const { messageId, attachments, visibleIndex, nextAction, previousAction } = props;
|
||||||
|
|
||||||
|
const [imageBroken, setImageBroken] = useState(false);
|
||||||
|
|
||||||
|
const handleImageError = useCallback(() => {
|
||||||
|
setImageBroken(true);
|
||||||
|
}, [setImageBroken]);
|
||||||
|
|
||||||
|
if (isEmpty(attachments)) {
|
||||||
|
window.log.debug('No attachments to render in carousel');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isVideo = isVideoAttachment(attachments[visibleIndex]);
|
||||||
|
|
||||||
|
const showLightbox = () => {
|
||||||
|
void showLightboxFromAttachmentProps(messageId, attachments[visibleIndex]);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (imageBroken) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex container={true} flexDirection={'row'} justifyContent={'center'} alignItems={'center'}>
|
||||||
|
<CarouselButton visible={visibleIndex > 0} onClick={previousAction} rotation={90} />
|
||||||
|
<ImageContainer>
|
||||||
|
<Image
|
||||||
|
alt={getAlt(attachments[visibleIndex])}
|
||||||
|
attachment={attachments[visibleIndex]}
|
||||||
|
playIconOverlay={isVideo}
|
||||||
|
height={'var(--right-panel-attachment-height)'}
|
||||||
|
width={'var(--right-panel-attachment-width)'}
|
||||||
|
url={getThumbnailUrl(attachments[visibleIndex])}
|
||||||
|
attachmentIndex={visibleIndex}
|
||||||
|
softCorners={true}
|
||||||
|
onClick={isVideo ? showLightbox : undefined}
|
||||||
|
onError={handleImageError}
|
||||||
|
/>
|
||||||
|
<SubtitleDotMenu
|
||||||
|
id={'attachment-carousel-subtitle-dots'}
|
||||||
|
selectedOptionIndex={visibleIndex}
|
||||||
|
options={attachments.length}
|
||||||
|
style={{
|
||||||
|
display: attachments.length < 2 ? 'none' : undefined,
|
||||||
|
backgroundColor: 'var(--modal-background-color)',
|
||||||
|
borderRadius: '50px',
|
||||||
|
width: 'fit-content',
|
||||||
|
padding: 'var(--margins-xs)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FullscreenButton
|
||||||
|
onClick={showLightbox}
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--modal-background-color)',
|
||||||
|
borderRadius: '50px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ImageContainer>
|
||||||
|
<CarouselButton
|
||||||
|
visible={visibleIndex < attachments.length - 1}
|
||||||
|
onClick={nextAction}
|
||||||
|
rotation={270}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { LabelWithInfo } from '.';
|
||||||
|
import { PropsForAttachment } from '../../../../../../state/ducks/conversations';
|
||||||
|
import { Flex } from '../../../../../basic/Flex';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
attachment: PropsForAttachment;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledLabelContainer = styled(Flex)`
|
||||||
|
div {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const AttachmentInfo = (props: Props) => {
|
||||||
|
const { attachment } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex container={true} flexDirection="column">
|
||||||
|
<LabelWithInfo
|
||||||
|
label={`${window.i18n('fileId')}:`}
|
||||||
|
info={attachment?.id ? String(attachment.id) : window.i18n('notApplicable')}
|
||||||
|
/>
|
||||||
|
<StyledLabelContainer container={true} flexDirection="row" justifyContent="space-between">
|
||||||
|
<LabelWithInfo
|
||||||
|
label={`${window.i18n('fileType')}:`}
|
||||||
|
info={
|
||||||
|
attachment?.contentType ? String(attachment.contentType) : window.i18n('notApplicable')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<LabelWithInfo
|
||||||
|
label={`${window.i18n('fileSize')}:`}
|
||||||
|
info={attachment?.fileSize ? String(attachment.fileSize) : window.i18n('notApplicable')}
|
||||||
|
/>
|
||||||
|
</StyledLabelContainer>
|
||||||
|
<StyledLabelContainer container={true} flexDirection="row" justifyContent="space-between">
|
||||||
|
<LabelWithInfo
|
||||||
|
label={`${window.i18n('resolution')}:`}
|
||||||
|
info={
|
||||||
|
attachment?.width && attachment.height
|
||||||
|
? `${attachment.width}x${attachment.height}`
|
||||||
|
: window.i18n('notApplicable')
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<LabelWithInfo
|
||||||
|
label={`${window.i18n('duration')}:`}
|
||||||
|
info={attachment?.duration ? attachment?.duration : window.i18n('notApplicable')}
|
||||||
|
/>
|
||||||
|
</StyledLabelContainer>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { MessageInfoLabel } from '.';
|
||||||
|
import { useConversationUsername } from '../../../../../../hooks/useParamSelector';
|
||||||
|
import { Avatar, AvatarSize } from '../../../../../avatar/Avatar';
|
||||||
|
|
||||||
|
const StyledFromContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: var(--margins-lg);
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--margins-xs);
|
||||||
|
`;
|
||||||
|
const StyledAuthorNamesContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Name = styled.span`
|
||||||
|
font-weight: bold;
|
||||||
|
`;
|
||||||
|
const Pubkey = styled.span`
|
||||||
|
font-family: var(--font-font-mono);
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
user-select: text;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledMessageInfoAuthor = styled.div`
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const MessageFrom = (props: { sender: string }) => {
|
||||||
|
const { sender } = props;
|
||||||
|
const profileName = useConversationUsername(sender);
|
||||||
|
const from = window.i18n('from');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledMessageInfoAuthor>
|
||||||
|
<MessageInfoLabel>{from}</MessageInfoLabel>
|
||||||
|
<StyledFromContainer>
|
||||||
|
<Avatar size={AvatarSize.M} pubkey={sender} onAvatarClick={undefined} />
|
||||||
|
<StyledAuthorNamesContainer>
|
||||||
|
{!!profileName && <Name>{profileName}</Name>}
|
||||||
|
<Pubkey>{sender}</Pubkey>
|
||||||
|
</StyledAuthorNamesContainer>
|
||||||
|
</StyledFromContainer>
|
||||||
|
</StyledMessageInfoAuthor>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
import { AttachmentInfo } from './AttachmentInfo';
|
||||||
|
import { MessageFrom } from './MessageFrom';
|
||||||
|
import { LabelWithInfo, MessageInfo, MessageInfoLabel } from './MessageInfo';
|
||||||
|
|
||||||
|
export { AttachmentInfo, LabelWithInfo, MessageFrom, MessageInfo, MessageInfoLabel };
|
||||||
Loading…
Reference in New Issue