feat: wip work

pull/3017/head
William Grant 1 year ago
parent c9a8ea2b81
commit 1f52b9620b

@ -27,6 +27,8 @@ export const StyledSubtitleContainer = styled.div`
}
`;
export const StyledSubtitleDotMenu = styled(Flex)``;
const StyledSubtitleDot = styled.span<{ active: boolean }>`
border-radius: 50%;
background-color: ${props =>
@ -37,16 +39,18 @@ const StyledSubtitleDot = styled.span<{ active: boolean }>`
margin: 0 2px;
`;
const SubtitleDotMenu = ({
export const SubtitleDotMenu = ({
id,
options,
selectedOptionIndex,
style,
}: {
id?: string;
options: Array<string | null>;
selectedOptionIndex: number;
style: CSSProperties;
}) => (
<Flex container={true} alignItems={'center'} style={style}>
<Flex id={} container={true} alignItems={'center'} style={style}>
{options.map((option, index) => {
if (!option) {
return null;

@ -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 };

@ -720,6 +720,10 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
messageId: this.get('id'),
errors,
direction: this.get('direction'),
sender: this.get('source'),
attachments,
timestamp: this.get('timestamp'),
serverTimestamp: this.get('serverTimestamp'),
contacts: sortedContacts || [],
};

Loading…
Cancel
Save