From 1f52b9620bfe5a536fba2591a6f815cbf5e8ae27 Mon Sep 17 00:00:00 2001 From: William Grant Date: Thu, 19 Oct 2023 17:32:23 +1100 Subject: [PATCH] feat: wip work --- .../header/ConversationHeaderSubtitle.tsx | 8 +- .../message-info/OverlayMessageInfo.tsx | 265 ++++++++++++++++++ .../components/AttachmentCarousel.tsx | 140 +++++++++ .../components/AttachmentInfo.tsx | 54 ++++ .../message-info/components/MessageFrom.tsx | 48 ++++ .../overlay/message-info/components/index.tsx | 5 + ts/models/message.ts | 4 + 7 files changed, 522 insertions(+), 2 deletions(-) create mode 100644 ts/components/conversation/right-panel/overlay/message-info/OverlayMessageInfo.tsx create mode 100644 ts/components/conversation/right-panel/overlay/message-info/components/AttachmentCarousel.tsx create mode 100644 ts/components/conversation/right-panel/overlay/message-info/components/AttachmentInfo.tsx create mode 100644 ts/components/conversation/right-panel/overlay/message-info/components/MessageFrom.tsx create mode 100644 ts/components/conversation/right-panel/overlay/message-info/components/index.tsx diff --git a/ts/components/conversation/header/ConversationHeaderSubtitle.tsx b/ts/components/conversation/header/ConversationHeaderSubtitle.tsx index 1e61ae95e..6552eafc8 100644 --- a/ts/components/conversation/header/ConversationHeaderSubtitle.tsx +++ b/ts/components/conversation/header/ConversationHeaderSubtitle.tsx @@ -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; selectedOptionIndex: number; style: CSSProperties; }) => ( - + {options.map((option, index) => { if (!option) { return null; diff --git a/ts/components/conversation/right-panel/overlay/message-info/OverlayMessageInfo.tsx b/ts/components/conversation/right-panel/overlay/message-info/OverlayMessageInfo.tsx new file mode 100644 index 000000000..385ba90b3 --- /dev/null +++ b/ts/components/conversation/right-panel/overlay/message-info/OverlayMessageInfo.tsx @@ -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 ( + + + + ); +}; + +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 ( + + +
{ + dispatch(closeRightPanel()); + dispatch(resetRightOverlayMode()); + dispatch(closeMessageDetailsView()); + }} + > + {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[0], + }); + } + }} + /> + )} + {isDeletable && ( + { + void deleteMessagesById([messageId], convoId); + dispatch(closeRightPanel()); + dispatch(resetRightOverlayMode()); + }} + /> + )} + {isDeletableForEveryone && ( + { + void deleteMessagesByIdForEveryone([messageId], convoId); + dispatch(closeRightPanel()); + dispatch(resetRightOverlayMode()); + }} + /> + )} + + + + +
+
+ ); +}; diff --git a/ts/components/conversation/right-panel/overlay/message-info/components/AttachmentCarousel.tsx b/ts/components/conversation/right-panel/overlay/message-info/components/AttachmentCarousel.tsx new file mode 100644 index 000000000..49a50c491 --- /dev/null +++ b/ts/components/conversation/right-panel/overlay/message-info/components/AttachmentCarousel.tsx @@ -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 ( + + ); +}; + +const StyledFullscreenButton = styled.div``; + +const FullscreenButton = (props: { onClick: () => void; style?: CSSProperties }) => { + return ( + + + + ); +}; + +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; + 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 ( + + 0} onClick={previousAction} rotation={90} /> + + {getAlt(attachments[visibleIndex])} + + + + + + ); +}; diff --git a/ts/components/conversation/right-panel/overlay/message-info/components/AttachmentInfo.tsx b/ts/components/conversation/right-panel/overlay/message-info/components/AttachmentInfo.tsx new file mode 100644 index 000000000..555416c89 --- /dev/null +++ b/ts/components/conversation/right-panel/overlay/message-info/components/AttachmentInfo.tsx @@ -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 ( + + + + + + + + + + + + ); +}; diff --git a/ts/components/conversation/right-panel/overlay/message-info/components/MessageFrom.tsx b/ts/components/conversation/right-panel/overlay/message-info/components/MessageFrom.tsx new file mode 100644 index 000000000..4268687ca --- /dev/null +++ b/ts/components/conversation/right-panel/overlay/message-info/components/MessageFrom.tsx @@ -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 ( + + {from} + + + + {!!profileName && {profileName}} + {sender} + + + + ); +}; diff --git a/ts/components/conversation/right-panel/overlay/message-info/components/index.tsx b/ts/components/conversation/right-panel/overlay/message-info/components/index.tsx new file mode 100644 index 000000000..b9241f9c8 --- /dev/null +++ b/ts/components/conversation/right-panel/overlay/message-info/components/index.tsx @@ -0,0 +1,5 @@ +import { AttachmentInfo } from './AttachmentInfo'; +import { MessageFrom } from './MessageFrom'; +import { LabelWithInfo, MessageInfo, MessageInfoLabel } from './MessageInfo'; + +export { AttachmentInfo, LabelWithInfo, MessageFrom, MessageInfo, MessageInfoLabel }; diff --git a/ts/models/message.ts b/ts/models/message.ts index e0cc2e85f..fd0bde8f1 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -720,6 +720,10 @@ export class MessageModel extends Backbone.Model { messageId: this.get('id'), errors, direction: this.get('direction'), + sender: this.get('source'), + attachments, + timestamp: this.get('timestamp'), + serverTimestamp: this.get('serverTimestamp'), contacts: sortedContacts || [], };