From ee4ed2e075546b4537561eea19c323975634ae63 Mon Sep 17 00:00:00 2001 From: audric <audric@loki.network> Date: Tue, 24 Aug 2021 13:23:23 +1000 Subject: [PATCH] fix attachment logic with ui redesign --- stylesheets/_modules.scss | 205 +----------- stylesheets/_quote.scss | 10 +- stylesheets/_session.scss | 19 +- stylesheets/_theme_dark.scss | 44 --- .../conversation/ConversationHeader.tsx | 16 +- ts/components/conversation/Image.tsx | 31 +- ts/components/conversation/ImageGrid.tsx | 40 +-- ts/components/conversation/Message.tsx | 313 ++++++++---------- ts/components/conversation/Quote.tsx | 84 +++-- .../message/MessageAuthorText.tsx | 37 ++- .../SessionQuotedMessageComposition.tsx | 4 - .../session/settings/SessionSettings.tsx | 1 + ts/state/selectors/conversations.ts | 31 ++ 13 files changed, 275 insertions(+), 560 deletions(-) diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index f9b0e8394..a46734f82 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -85,80 +85,16 @@ right: 8px; } -.module-message__attachment-container { - // Entirely to ensure that images are centered if they aren't full width of bubble - text-align: center; - position: relative; - - margin-inline-start: -12px; - margin-inline-end: -12px; - margin-top: -10px; - margin-bottom: -10px; - - border-radius: $session_message-container-border-radius; - overflow: hidden; - // no background by default for the attachment container -} - -.module-message--outgoing { - .module-message__attachment-container--with-content-below, - .module-message__attachment-container--with-content-above { - background: none; - } -} - -.module-message--incoming { - .module-message__attachment-container--with-content-below, - .module-message__attachment-container--with-content-above { - background: none; - } -} - -.module-message__attachment-container--with-content-below { - margin-bottom: 7px; - border-bottom-left-radius: 0px; - border-bottom-right-radius: 0px; -} - -.module-message__attachment-container--with-content-above { - margin-top: 4px; - border-top-left-radius: 0px; - border-top-right-radius: 0px; -} - .module-message__img-attachment { - margin-bottom: -3px; - - // redundant with attachment-container, but we get cursor flashing on move otherwise cursor: pointer; } -.module-message__audio-attachment { - margin-top: 2px; -} - -.module-message__audio-attachment--with-content-below { - margin-bottom: 5px; -} - -.module-message__audio-attachment--with-content-above { - margin-top: 6px; -} - .module-message__generic-attachment { display: flex; flex-direction: row; align-items: center; } -.module-message__generic-attachment--with-content-below { - padding-bottom: 6px; -} - -.module-message__generic-attachment--with-content-above { - padding-top: 4px; -} - .module-message__generic-attachment__icon-container { position: relative; cursor: pointer; @@ -269,12 +205,6 @@ border-top-right-radius: $session_message-container-border-radius; } -.module-message__link-preview--with-content-above { - margin-top: 4px; - border-top-left-radius: 0px; - border-top-right-radius: 0px; -} - .module-message__link-preview__content { padding: 8px; border-top-left-radius: $session_message-container-border-radius; @@ -286,13 +216,6 @@ border: 1px solid $color-black-015; } -.module-message__link-preview__content--with-content-above { - border-top: none; - border-bottom: none; - border-top-left-radius: 0px; - border-top-right-radius: 0px; -} - .module-message__link-preview__image_container { margin: -2px; margin-inline-end: 8px; @@ -389,70 +312,6 @@ align-items: center; } -// Module: Embedded Contact - -.module-embedded-contact { - // Cursor is always a pointer because this component is always wired up to the contact detail screen - cursor: pointer; - display: flex; - flex-direction: row; - align-items: center; -} - -.module-embedded-contact--with-content-above { - padding-top: 4px; -} - -.module-embedded-contact--with-content-below { - padding-bottom: 4px; -} - -.module-embedded-contact__spinner-container { - padding-inline-start: 5px; - padding-inline-end: 5px; -} - -.module-embedded-contact__text-container { - flex-grow: 1; - margin-inline-start: 8px; - - max-width: calc(100% - 58px); -} - -.module-embedded-contact__contact-name { - font-size: 14px; - line-height: 18px; - font-weight: 300; - margin-top: 6px; - color: $color-gray-90; - - max-width: 100%; - white-space: nowrap; - overflow-x: hidden; - text-overflow: ellipsis; -} - -.module-embedded-contact__contact-name--incoming { - color: $color-white; -} - -.module-embedded-contact__contact-method { - font-size: 11px; - line-height: 16px; - letter-spacing: 0.3px; - margin-top: 3px; - color: $color-gray-60; - - max-width: 100%; - white-space: nowrap; - overflow-x: hidden; - text-overflow: ellipsis; -} - -.module-embedded-contact__contact-method--incoming { - color: $color-white-07; -} - // Module: Contact Detail .module-contact-detail { @@ -543,45 +402,6 @@ font-weight: bold; } -// Module: Reset Session Notification - -.module-reset-session-notification { - margin-top: 14px; - font-size: 14px; - line-height: 20px; - letter-spacing: 0.3px; - color: $color-gray-60; - text-align: center; -} - -.module-verification-notification__button { - margin-top: 5px; - display: inline-block; - cursor: pointer; - font-size: 13px; - font-weight: 300; - line-height: 18px; - padding: 12px; - color: $color-loki-green; - background-color: $color-light-02; - border-radius: 4px; -} - -// Module: Verification Notification - -.module-verification-notification { - margin-top: 14px; - font-size: 14px; - line-height: 20px; - letter-spacing: 0.3px; - color: $color-gray-60; - text-align: center; -} - -.module-verification-notification__contact { - font-weight: 300; -} - // Module: Timer Notification .module-timer-notification { @@ -751,6 +571,7 @@ align-items: center; -webkit-user-select: text; + cursor: pointer; .module-contact-name__profile-name { width: 100%; @@ -1226,22 +1047,6 @@ border-radius: 4px; } -.module-image--curved-top-left { - border-top-left-radius: $session_message-container-border-radius; -} -.module-image--curved-top-right { - border-top-right-radius: $session_message-container-border-radius; -} -.module-image--curved-bottom-left { - border-bottom-left-radius: $session_message-container-border-radius; -} -.module-image--curved-bottom-right { - border-bottom-right-radius: $session_message-container-border-radius; -} -.module-image--small-curved-top-left { - border-top-left-radius: 10px; -} - .module-image__border-overlay { position: absolute; top: 0; @@ -1249,7 +1054,6 @@ z-index: 1; left: 0; right: 0; - box-shadow: inset 0px 0px 0px 1px $color-black-015; } .module-image__border-overlay--dark { @@ -1265,10 +1069,7 @@ .module-image__image { object-fit: cover; - // redundant with attachment-container, but we get cursor flashing on move otherwise cursor: pointer; - - margin-bottom: -3px; } .module-image__bottom-overlay { @@ -1347,10 +1148,6 @@ margin: -1px; } -.module-image-grid--one-image { - margin-bottom: -5px; -} - .module-image-grid__column { display: inline-flex; flex-direction: column; diff --git a/stylesheets/_quote.scss b/stylesheets/_quote.scss index 6e4a1db1d..deb769f76 100644 --- a/stylesheets/_quote.scss +++ b/stylesheets/_quote.scss @@ -131,15 +131,9 @@ } .module-quote-container { - margin-inline-start: -6px; - margin-inline-end: -6px; - margin-top: -4px; margin-bottom: 5px; - padding-left: 5px; -} - -.module-quote-container--with-content-above { - margin-top: 3px; + margin-top: 10px; + padding-left: 10px; } .module-quote--no-click { diff --git a/stylesheets/_session.scss b/stylesheets/_session.scss index 3c326ca54..b68506614 100644 --- a/stylesheets/_session.scss +++ b/stylesheets/_session.scss @@ -342,10 +342,6 @@ textarea { .module-message__container { position: relative; display: inline-block; - padding-inline-end: 10px; - padding-inline-start: 10px; - padding-top: 10px; - padding-bottom: 10px; overflow: hidden; min-width: 30px; // To limit messages with things forcing them wider, like long attachment names @@ -355,12 +351,17 @@ textarea { label { user-select: none; } +.module-message__attachment-container { + // Entirely to ensure that images are centered if they aren't full width of bubble + text-align: center; + position: relative; + + border-radius: $session_message-container-border-radius; + overflow: hidden; + // no background by default for the attachment container +} -.module-message__attachment-container, -.module-image--curved-bottom-right, -.module-image--curved-top-left, -.module-image--curved-top-right, -.module-image--curved-bottom-left { +.module-message__attachment-container { border-top-left-radius: $session_message-container-border-radius; border-top-right-radius: $session_message-container-border-radius; border-bottom-left-radius: $session_message-container-border-radius; diff --git a/stylesheets/_theme_dark.scss b/stylesheets/_theme_dark.scss index 63f0d71e7..cb9341bb9 100644 --- a/stylesheets/_theme_dark.scss +++ b/stylesheets/_theme_dark.scss @@ -207,11 +207,6 @@ border: 1px solid $color-gray-60; } - .module-message__link-preview__content--with-content-above { - border-top: none; - border-bottom: none; - } - .module-message__link-preview__title { color: $color-gray-05; } @@ -220,24 +215,6 @@ color: $color-gray-25; } - // Module: Embedded Contact - - .module-embedded-contact__contact-name { - color: $color-dark-05; - } - - .module-embedded-contact__contact-name--incoming { - color: $color-white; - } - - .module-embedded-contact__contact-method { - color: $color-white-07; - } - - .module-embedded-contact__contact-method--incoming { - color: $color-white-07; - } - // Module: Contact Detail .module-contact-detail__send-message { @@ -259,23 +236,6 @@ color: $color-dark-30; } - // Module: Reset Session Notification - - .module-reset-session-notification { - color: $color-dark-30; - } - - .module-verification-notification__button { - color: $color-loki-green; - background-color: $color-gray-75; - } - - // Module: Verification Notification - - .module-verification-notification { - color: $color-dark-30; - } - // Module: Timer Notification .module-timer-notification { @@ -404,10 +364,6 @@ background: none; } - .module-image__border-overlay { - box-shadow: inset 0px 0px 0px 1px $color-white-015; - } - .module-image__loading-placeholder { background-color: $color-white-015; } diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx index 09b207052..ff41d5922 100644 --- a/ts/components/conversation/ConversationHeader.tsx +++ b/ts/components/conversation/ConversationHeader.tsx @@ -18,6 +18,7 @@ import { getSelectedMessageIds, isMessageDetailView, isMessageSelectionMode, + isRightPanelShowing, } from '../../state/selectors/conversations'; import { useDispatch, useSelector } from 'react-redux'; import { useMembersAvatars } from '../../hooks/useMembersAvatar'; @@ -25,6 +26,7 @@ import { useMembersAvatars } from '../../hooks/useMembersAvatar'; import { deleteMessagesById } from '../../interactions/conversationInteractions'; import { closeMessageDetailsView, + closeRightPanel, NotificationForConvoOption, openRightPanel, resetSelectedMessageIds, @@ -210,6 +212,8 @@ export type ConversationHeaderTitleProps = { const ConversationHeaderTitle = () => { const headerTitleProps = useSelector(getConversationHeaderTitleProps); const notificationSetting = useSelector(getCurrentNotificationSettingText); + const isRightPanelOn = useSelector(isRightPanelShowing); + const dispatch = useDispatch(); if (!headerTitleProps) { return null; } @@ -257,7 +261,17 @@ const ConversationHeaderTitle = () => { const title = profileName || name || phoneNumber; return ( - <div className="module-conversation-header__title"> + <div + className="module-conversation-header__title" + onClick={() => { + if (isRightPanelOn) { + dispatch(closeRightPanel()); + } else { + dispatch(openRightPanel()); + } + }} + role="button" + > <span className="module-contact-name__profile-name">{title}</span> <StyledSubtitleContainer> <ConversationHeaderSubtitle text={fullTextSubtitle} /> diff --git a/ts/components/conversation/Image.tsx b/ts/components/conversation/Image.tsx index e1c59b736..cdba596bf 100644 --- a/ts/components/conversation/Image.tsx +++ b/ts/components/conversation/Image.tsx @@ -17,12 +17,6 @@ type Props = { bottomOverlay?: boolean; closeButton?: boolean; - curveBottomLeft?: boolean; - curveBottomRight?: boolean; - curveTopLeft?: boolean; - curveTopRight?: boolean; - - smallCurveTopLeft?: boolean; darkOverlay?: boolean; playIconOverlay?: boolean; @@ -40,10 +34,6 @@ export const Image = (props: Props) => { attachment, bottomOverlay, closeButton, - curveBottomLeft, - curveBottomRight, - curveTopLeft, - curveTopRight, darkOverlay, height, onClick, @@ -51,7 +41,6 @@ export const Image = (props: Props) => { onError, overlayText, playIconOverlay, - smallCurveTopLeft, softCorners, url, width, @@ -83,11 +72,6 @@ export const Image = (props: Props) => { className={classNames( 'module-image', canClick ? 'module-image__with-click-handler' : null, - curveBottomLeft ? 'module-image--curved-bottom-left' : null, - curveBottomRight ? 'module-image--curved-bottom-right' : null, - curveTopLeft ? 'module-image--curved-top-left' : null, - curveTopRight ? 'module-image--curved-top-right' : null, - smallCurveTopLeft ? 'module-image--small-curved-top-left' : null, softCorners ? 'module-image--soft-corners' : null )} > @@ -125,11 +109,6 @@ export const Image = (props: Props) => { <div className={classNames( 'module-image__border-overlay', - curveTopLeft ? 'module-image--curved-top-left' : null, - curveTopRight ? 'module-image--curved-top-right' : null, - curveBottomLeft ? 'module-image--curved-bottom-left' : null, - curveBottomRight ? 'module-image--curved-bottom-right' : null, - smallCurveTopLeft ? 'module-image--small-curved-top-left' : null, softCorners ? 'module-image--soft-corners' : null, darkOverlay ? 'module-image__border-overlay--dark' : null )} @@ -146,15 +125,7 @@ export const Image = (props: Props) => { className="module-image__close-button" /> ) : null} - {bottomOverlay ? ( - <div - className={classNames( - 'module-image__bottom-overlay', - curveBottomLeft ? 'module-image--curved-bottom-left' : null, - curveBottomRight ? 'module-image--curved-bottom-right' : null - )} - /> - ) : null} + {bottomOverlay ? <div className={classNames('module-image__bottom-overlay')} /> : null} {!(pending || loading) && playIconOverlay ? ( <div className="module-image__play-overlay__circle"> <div className="module-image__play-overlay__icon" /> diff --git a/ts/components/conversation/ImageGrid.tsx b/ts/components/conversation/ImageGrid.tsx index 3731506ee..51017533e 100644 --- a/ts/components/conversation/ImageGrid.tsx +++ b/ts/components/conversation/ImageGrid.tsx @@ -16,8 +16,6 @@ import { Image } from './Image'; type Props = { attachments: Array<AttachmentTypeWithPath>; - withContentAbove?: boolean; - withContentBelow?: boolean; bottomOverlay?: boolean; onError: () => void; @@ -26,23 +24,9 @@ type Props = { export const ImageGrid = (props: Props) => { // tslint:disable-next-line max-func-body-length */ - const { - attachments, - bottomOverlay, - onError, - onClickAttachment, - withContentAbove, - withContentBelow, - } = props; + const { attachments, bottomOverlay, onError, onClickAttachment } = props; - const curveTopLeft = !Boolean(withContentAbove); - const curveTopRight = curveTopLeft; - - const curveBottom = !Boolean(withContentBelow); - const curveBottomLeft = curveBottom; - const curveBottomRight = curveBottom; - - const withBottomOverlay = Boolean(bottomOverlay && curveBottom); + const withBottomOverlay = Boolean(bottomOverlay); if (!attachments || !attachments.length) { return null; @@ -56,10 +40,6 @@ export const ImageGrid = (props: Props) => { <Image alt={getAlt(attachments[0])} bottomOverlay={withBottomOverlay} - curveTopLeft={curveTopLeft} - curveTopRight={curveTopRight} - curveBottomLeft={curveBottomLeft} - curveBottomRight={curveBottomRight} attachment={attachments[0]} playIconOverlay={isVideoAttachment(attachments[0])} height={height} @@ -79,8 +59,6 @@ export const ImageGrid = (props: Props) => { alt={getAlt(attachments[0])} attachment={attachments[0]} bottomOverlay={withBottomOverlay} - curveTopLeft={curveTopLeft} - curveBottomLeft={curveBottomLeft} playIconOverlay={isVideoAttachment(attachments[0])} height={149} width={149} @@ -91,8 +69,6 @@ export const ImageGrid = (props: Props) => { <Image alt={getAlt(attachments[1])} bottomOverlay={withBottomOverlay} - curveTopRight={curveTopRight} - curveBottomRight={curveBottomRight} playIconOverlay={isVideoAttachment(attachments[1])} height={149} width={149} @@ -111,8 +87,6 @@ export const ImageGrid = (props: Props) => { <Image alt={getAlt(attachments[0])} bottomOverlay={withBottomOverlay} - curveTopLeft={curveTopLeft} - curveBottomLeft={curveBottomLeft} attachment={attachments[0]} playIconOverlay={isVideoAttachment(attachments[0])} height={200} @@ -124,7 +98,6 @@ export const ImageGrid = (props: Props) => { <div className="module-image-grid__column"> <Image alt={getAlt(attachments[1])} - curveTopRight={curveTopRight} height={99} width={99} attachment={attachments[1]} @@ -136,7 +109,6 @@ export const ImageGrid = (props: Props) => { <Image alt={getAlt(attachments[2])} bottomOverlay={withBottomOverlay} - curveBottomRight={curveBottomRight} height={99} width={99} attachment={attachments[2]} @@ -157,7 +129,6 @@ export const ImageGrid = (props: Props) => { <div className="module-image-grid__row"> <Image alt={getAlt(attachments[0])} - curveTopLeft={curveTopLeft} attachment={attachments[0]} playIconOverlay={isVideoAttachment(attachments[0])} height={149} @@ -168,7 +139,6 @@ export const ImageGrid = (props: Props) => { /> <Image alt={getAlt(attachments[1])} - curveTopRight={curveTopRight} playIconOverlay={isVideoAttachment(attachments[1])} height={149} width={149} @@ -182,7 +152,6 @@ export const ImageGrid = (props: Props) => { <Image alt={getAlt(attachments[2])} bottomOverlay={withBottomOverlay} - curveBottomLeft={curveBottomLeft} playIconOverlay={isVideoAttachment(attachments[2])} height={149} width={149} @@ -194,7 +163,6 @@ export const ImageGrid = (props: Props) => { <Image alt={getAlt(attachments[3])} bottomOverlay={withBottomOverlay} - curveBottomRight={curveBottomRight} playIconOverlay={isVideoAttachment(attachments[3])} height={149} width={149} @@ -218,7 +186,6 @@ export const ImageGrid = (props: Props) => { <div className="module-image-grid__row"> <Image alt={getAlt(attachments[0])} - curveTopLeft={curveTopLeft} attachment={attachments[0]} playIconOverlay={isVideoAttachment(attachments[0])} height={149} @@ -229,7 +196,6 @@ export const ImageGrid = (props: Props) => { /> <Image alt={getAlt(attachments[1])} - curveTopRight={curveTopRight} playIconOverlay={isVideoAttachment(attachments[1])} height={149} width={149} @@ -243,7 +209,6 @@ export const ImageGrid = (props: Props) => { <Image alt={getAlt(attachments[2])} bottomOverlay={withBottomOverlay} - curveBottomLeft={curveBottomLeft} playIconOverlay={isVideoAttachment(attachments[2])} height={99} width={99} @@ -266,7 +231,6 @@ export const ImageGrid = (props: Props) => { <Image alt={getAlt(attachments[4])} bottomOverlay={withBottomOverlay} - curveBottomRight={curveBottomRight} playIconOverlay={isVideoAttachment(attachments[4])} height={99} width={99} diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 38cf8ff0f..44478dc05 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -211,26 +211,13 @@ class MessageInner extends React.PureComponent<Props, State> { // tslint:disable-next-line max-func-body-length cyclomatic-complexity public renderAttachment() { - const { - id, - attachments, - text, - conversationType, - direction, - quote, - isTrustedForAttachmentDownload, - } = this.props; + const { id, attachments, direction, isTrustedForAttachmentDownload } = this.props; const { imageBroken } = this.state; if (!attachments || !attachments[0]) { return null; } const firstAttachment = attachments[0]; - - // For attachments which aren't full-frame - const withContentBelow = Boolean(text); - const withContentAbove = - Boolean(quote) || (conversationType === 'group' && direction === 'incoming'); const displayImage = canDisplayImage(attachments); if (!isTrustedForAttachmentDownload) { @@ -244,17 +231,9 @@ class MessageInner extends React.PureComponent<Props, State> { (isVideo(attachments) && hasVideoScreenshot(attachments))) ) { return ( - <div - className={classNames( - 'module-message__attachment-container', - withContentAbove ? 'module-message__attachment-container--with-content-above' : null, - withContentBelow ? 'module-message__attachment-container--with-content-below' : null - )} - > + <div className={classNames('module-message__attachment-container')}> <ImageGrid attachments={attachments} - withContentAbove={withContentAbove} - withContentBelow={withContentBelow} onError={this.handleImageError} onClickAttachment={this.onClickOnImageGrid} /> @@ -281,13 +260,7 @@ class MessageInner extends React.PureComponent<Props, State> { const isDangerous = isFileDangerous(fileName || ''); return ( - <div - className={classNames( - 'module-message__generic-attachment', - withContentBelow ? 'module-message__generic-attachment--with-content-below' : null, - withContentAbove ? 'module-message__generic-attachment--with-content-above' : null - )} - > + <div className={classNames('module-message__generic-attachment')}> {pending ? ( <div className="module-message__generic-attachment__spinner-container"> <Spinner size="small" direction={direction} /> @@ -337,7 +310,7 @@ class MessageInner extends React.PureComponent<Props, State> { // tslint:disable-next-line cyclomatic-complexity public renderPreview() { - const { attachments, conversationType, direction, previews, quote } = this.props; + const { attachments, previews } = this.props; // Attachments take precedence over Link Previews if (attachments && attachments.length) { @@ -353,41 +326,19 @@ class MessageInner extends React.PureComponent<Props, State> { return null; } - const withContentAbove = - Boolean(quote) || (conversationType === 'group' && direction === 'incoming'); - const previewHasImage = first.image && isImageAttachment(first.image); const width = first.image && first.image.width; const isFullSizeImage = width && width >= MINIMUM_LINK_PREVIEW_IMAGE_WIDTH; return ( - <div - role="button" - className={classNames( - 'module-message__link-preview', - withContentAbove ? 'module-message__link-preview--with-content-above' : null - )} - > + <div role="button" className={classNames('module-message__link-preview')}> {first.image && previewHasImage && isFullSizeImage ? ( - <ImageGrid - attachments={[first.image]} - withContentAbove={withContentAbove} - withContentBelow={true} - onError={this.handleImageError} - /> + <ImageGrid attachments={[first.image]} onError={this.handleImageError} /> ) : null} - <div - className={classNames( - 'module-message__link-preview__content', - withContentAbove || isFullSizeImage - ? 'module-message__link-preview__content--with-content-above' - : null - )} - > + <div className={classNames('module-message__link-preview__content')}> {first.image && previewHasImage && !isFullSizeImage ? ( <div className="module-message__link-preview__image_container"> <Image - smallCurveTopLeft={!withContentAbove} softCorners={true} alt={window.i18n('previewThumbnail', [first.domain])} height={72} @@ -423,13 +374,11 @@ class MessageInner extends React.PureComponent<Props, State> { } public renderQuote() { - const { conversationType, direction, quote, isPublic, convoId } = this.props; + const { direction, quote } = this.props; if (!quote || !quote.authorPhoneNumber || !quote.messageId) { return null; } - const withContentAbove = conversationType === 'group' && direction === 'incoming'; - const shortenedPubkey = PubKey.shorten(quote.authorPhoneNumber); const displayedPubkey = quote.authorProfileName ? shortenedPubkey : quote.authorPhoneNumber; @@ -440,15 +389,11 @@ class MessageInner extends React.PureComponent<Props, State> { text={quote.text} attachment={quote.attachment} isIncoming={direction === 'incoming'} - conversationType={conversationType} - convoId={convoId} - isPublic={isPublic} authorPhoneNumber={displayedPubkey} authorProfileName={quote.authorProfileName} authorName={quote.authorName} referencedMessageNotFound={quote.referencedMessageNotFound} isFromMe={quote.isFromMe} - withContentAbove={withContentAbove} /> ); } @@ -538,6 +483,18 @@ class MessageInner extends React.PureComponent<Props, State> { return <OutgoingMessageStatus status={status} />; } + public renderExpireTimer(isCorrectSide: boolean) { + const { expirationLength, expirationTimestamp } = this.props; + + if (!(isCorrectSide && expirationLength && expirationTimestamp)) { + return null; + } + + return ( + <ExpireTimer expirationLength={expirationLength} expirationTimestamp={expirationTimestamp} /> + ); + } + public getWidth(): number | undefined { const { attachments, previews } = this.props; @@ -600,8 +557,7 @@ class MessageInner extends React.PureComponent<Props, State> { return false; } - // tslint:disable-next-line: cyclomatic-complexity - // tslint:disable-next-line: max-func-body-length + // tslint:disable-next-line: cyclomatic-complexity cyclomatic-complexity public render() { const { direction, @@ -610,142 +566,122 @@ class MessageInner extends React.PureComponent<Props, State> { selectedMessages, receivedAt, isUnread, - text, - timestamp, - serverTimestamp, - expirationLength, - expirationTimestamp, - firstMessageOfSeries, - lastMessageOfSeries, } = this.props; - const { expired, expiring } = this.state; + const { expired } = this.state; if (expired) { return null; } const selected = selectedMessages.includes(messageId) || false; - - const width = this.getWidth(); - const isShowingImage = this.isShowingImage(); - - const divClasses = ['session-message-wrapper']; - - if (selected) { - divClasses.push('message-selected'); - } - - if (conversationType === 'group') { - divClasses.push('public-chat-message-wrapper'); - } - - if (this.props.quotedMessageToAnimate === messageId) { - divClasses.push('flash-green-once'); - } - + const isGroup = conversationType === 'group'; + const isQuotedMessageToAnimate = this.props.quotedMessageToAnimate === messageId; const isIncoming = direction === 'incoming'; - if (isIncoming) { - divClasses.push('session-message-wrapper-incoming'); - } else { - divClasses.push('session-message-wrapper-outgoing'); - } - - const hasText = Boolean(text); - - const bgShouldBeTransparent = isShowingImage && !hasText; - const toolTipTitle = moment(serverTimestamp || timestamp).format('llll'); - return ( <ReadableMessage messageId={messageId} - className={classNames(divClasses)} + className={classNames( + 'session-message-wrapper', + selected && 'message-selected', + isGroup && 'public-chat-message-wrapper', + isQuotedMessageToAnimate && 'flash-green-once', + isIncoming ? 'session-message-wrapper-incoming' : 'session-message-wrapper-outgoing' + )} onContextMenu={this.handleContextMenu} receivedAt={receivedAt} isUnread={isUnread} key={`readable-message-${messageId}`} > {this.renderAvatar()} - {!isIncoming && expirationLength && expirationTimestamp ? ( - <ExpireTimer - expirationLength={expirationLength} - expirationTimestamp={expirationTimestamp} - /> - ) : null} - <div - className={classNames( - 'module-message', - `module-message--${direction}`, - expiring ? 'module-message--expired' : null - )} - role="button" - onClick={this.onClickOnMessageOuterContainer} - > - {this.renderStatus(isIncoming)} - <Flex container={true} flexDirection="column"> - <MessageAuthorText - authorName={this.props.authorName} - authorPhoneNumber={this.props.authorPhoneNumber} - authorProfileName={this.props.authorProfileName} - conversationType={this.props.conversationType} - direction={this.props.direction} - firstMessageOfSeries={this.props.firstMessageOfSeries} - isPublic={this.props.isPublic} - /> + {this.renderExpireTimer(!isIncoming)} + {this.renderMessageContentWithStatuses()} + {this.renderExpireTimer(isIncoming)} + </ReadableMessage> + ); + } - <div - className={classNames( - 'module-message__container', - `module-message__container--${direction}`, - bgShouldBeTransparent - ? `module-message__container--${direction}--transparent` - : `module-message__container--${direction}--opaque`, - firstMessageOfSeries - ? `module-message__container--${direction}--first-of-series` - : '', - lastMessageOfSeries ? `module-message__container--${direction}--last-of-series` : '' - )} - style={{ - width: isShowingImage ? width : undefined, - }} - role="button" - onClick={this.onClickOnMessageInnerContainer} - title={toolTipTitle} - > - {this.renderQuote()} - {this.renderAttachment()} - {this.renderPreview()} - {this.renderText()} - </div> - </Flex> - {this.renderStatus(!isIncoming)} + private renderMessageContentWithStatuses() { + const { expiring } = this.state; + const { direction } = this.props; + const isIncoming = direction === 'incoming'; - <MessageContextMenu + return ( + <div + className={classNames( + 'module-message', + `module-message--${direction}`, + expiring ? 'module-message--expired' : null + )} + role="button" + onClick={this.onClickOnMessageOuterContainer} + > + {this.renderStatus(isIncoming)} + <Flex container={true} flexDirection="column"> + <MessageAuthorText + authorName={this.props.authorName} authorPhoneNumber={this.props.authorPhoneNumber} - convoId={this.props.convoId} - contextMenuId={this.ctxMenuID} + authorProfileName={this.props.authorProfileName} direction={this.props.direction} - isBlocked={this.props.isBlocked} - isDeletable={this.props.isDeletable} - messageId={this.props.id} - text={this.props.text} - timestamp={this.props.timestamp} - serverTimestamp={this.props.serverTimestamp} - attachments={this.props.attachments} - isAdmin={this.props.isSenderAdmin} - isOpenGroupV2={this.props.isOpenGroupV2} - isPublic={this.props.isPublic} - status={this.props.status} - weAreAdmin={this.props.weAreAdmin} - /> - </div> - {isIncoming && expirationLength && expirationTimestamp ? ( - <ExpireTimer - expirationLength={expirationLength} - expirationTimestamp={expirationTimestamp} + firstMessageOfSeries={this.props.firstMessageOfSeries} /> + + {this.renderMessageContent()} + </Flex> + {this.renderStatus(!isIncoming)} + + {this.renderContextMenu()} + </div> + ); + } + + private renderMessageContent() { + const { + direction, + text, + timestamp, + serverTimestamp, + firstMessageOfSeries, + lastMessageOfSeries, + } = this.props; + + const width = this.getWidth(); + const isShowingImage = this.isShowingImage(); + const hasText = Boolean(text); + const hasQuote = !_.isEmpty(this.props.quote); + const hasContentAfterAttachmentAndQuote = + !_.isEmpty(this.props.previews) || !_.isEmpty(this.props.text); + + const bgShouldBeTransparent = isShowingImage && !hasText && !hasQuote; + const toolTipTitle = moment(serverTimestamp || timestamp).format('llll'); + + return ( + <div + className={classNames( + 'module-message__container', + `module-message__container--${direction}`, + bgShouldBeTransparent + ? `module-message__container--${direction}--transparent` + : `module-message__container--${direction}--opaque`, + firstMessageOfSeries ? `module-message__container--${direction}--first-of-series` : '', + lastMessageOfSeries ? `module-message__container--${direction}--last-of-series` : '' + )} + style={{ + width: isShowingImage ? width : undefined, + }} + role="button" + onClick={this.onClickOnMessageInnerContainer} + title={toolTipTitle} + > + {this.renderQuote()} + {this.renderAttachment()} + {hasContentAfterAttachmentAndQuote ? ( + <Flex padding="5px 5px 10px 5px"> + {this.renderPreview()} + {this.renderText()} + </Flex> ) : null} - </ReadableMessage> + </div> ); } @@ -765,6 +701,29 @@ class MessageInner extends React.PureComponent<Props, State> { } } + private renderContextMenu() { + return ( + <MessageContextMenu + authorPhoneNumber={this.props.authorPhoneNumber} + convoId={this.props.convoId} + contextMenuId={this.ctxMenuID} + direction={this.props.direction} + isBlocked={this.props.isBlocked} + isDeletable={this.props.isDeletable} + messageId={this.props.id} + text={this.props.text} + timestamp={this.props.timestamp} + serverTimestamp={this.props.serverTimestamp} + attachments={this.props.attachments} + isAdmin={this.props.isSenderAdmin} + isOpenGroupV2={this.props.isOpenGroupV2} + isPublic={this.props.isPublic} + status={this.props.status} + weAreAdmin={this.props.weAreAdmin} + /> + ); + } + private onQuoteClick(e: any) { const { quote, multiSelectMode, id } = this.props; e.preventDefault(); diff --git a/ts/components/conversation/Quote.tsx b/ts/components/conversation/Quote.tsx index ca43f2dee..d6d3ce1c2 100644 --- a/ts/components/conversation/Quote.tsx +++ b/ts/components/conversation/Quote.tsx @@ -1,6 +1,6 @@ // tslint:disable:react-this-binding-issue -import React, { useCallback } from 'react'; +import React, { useCallback, useState } from 'react'; import classNames from 'classnames'; import * as MIME from '../../../ts/types/MIME'; @@ -9,9 +9,15 @@ import * as GoogleChrome from '../../../ts/util/GoogleChrome'; import { MessageBody } from './MessageBody'; import { ContactName } from './ContactName'; import { PubKey } from '../../session/types'; -import { ConversationTypeEnum } from '../../models/conversation'; import { useEncryptedFileFetch } from '../../hooks/useEncryptedFileFetch'; +import { useSelector } from 'react-redux'; +import { + getSelectedConversationKey, + isGroupConversation, + isPublicGroupConversation, +} from '../../state/selectors/conversations'; +import { noop } from 'underscore'; export type QuotePropsWithoutListener = { attachment?: QuotedAttachmentType; @@ -20,10 +26,6 @@ export type QuotePropsWithoutListener = { authorName?: string; isFromMe: boolean; isIncoming: boolean; - conversationType: ConversationTypeEnum; - convoId: string; - isPublic?: boolean; - withContentAbove: boolean; text: string | null; referencedMessageNotFound: boolean; }; @@ -107,7 +109,12 @@ export const QuoteIcon = (props: any) => { ); }; -export const QuoteImage = (props: any) => { +export const QuoteImage = (props: { + handleImageErrorBound: () => void; + url: string; + contentType: string; + icon?: string; +}) => { const { url, icon, contentType, handleImageErrorBound } = props; const { loading, urlToLoad } = useEncryptedFileFetch(url, contentType); @@ -144,7 +151,9 @@ export const QuoteImage = (props: any) => { ); }; -export const QuoteGenericFile = (props: any) => { +export const QuoteGenericFile = ( + props: Pick<QuotePropsWithoutListener, 'attachment' | 'isIncoming'> +) => { const { attachment, isIncoming } = props; if (!attachment) { @@ -176,7 +185,12 @@ export const QuoteGenericFile = (props: any) => { ); }; -export const QuoteIconContainer = (props: any) => { +export const QuoteIconContainer = ( + props: Pick<QuotePropsWithoutListener, 'attachment'> & { + handleImageErrorBound: () => void; + imageBroken: boolean; + } +) => { const { attachment, imageBroken, handleImageErrorBound } = props; if (!attachment) { @@ -188,7 +202,12 @@ export const QuoteIconContainer = (props: any) => { if (GoogleChrome.isVideoTypeSupported(contentType)) { return objectUrl && !imageBroken ? ( - <QuoteImage url={objectUrl} icon={'play'} /> + <QuoteImage + url={objectUrl} + contentType={MIME.IMAGE_JPEG} + icon="play" + handleImageErrorBound={noop} + /> ) : ( <QuoteIcon icon="movie" /> ); @@ -210,9 +229,12 @@ export const QuoteIconContainer = (props: any) => { return null; }; -export const QuoteText = (props: any) => { - const { text, attachment, isIncoming, conversationType, convoId } = props; - const isGroup = conversationType === ConversationTypeEnum.GROUP; +export const QuoteText = ( + props: Pick<QuotePropsWithoutListener, 'text' | 'attachment' | 'isIncoming'> +) => { + const { text, attachment, isIncoming } = props; + const isGroup = useSelector(isGroupConversation); + const convoId = useSelector(getSelectedConversationKey); if (text) { return ( @@ -285,7 +307,9 @@ const QuoteAuthor = (props: QuoteAuthorProps) => { ); }; -export const QuoteReferenceWarning = (props: any) => { +export const QuoteReferenceWarning = ( + props: Pick<QuotePropsWithoutListener, 'isIncoming' | 'referencedMessageNotFound'> +) => { const { isIncoming, referencedMessageNotFound } = props; if (!referencedMessageNotFound) { @@ -318,21 +342,21 @@ export const QuoteReferenceWarning = (props: any) => { }; export const Quote = (props: QuotePropsWithListener) => { - const handleImageErrorBound = null; + const [imageBroken, setImageBroken] = useState(false); + const handleImageErrorBound = () => { + setImageBroken(true); + }; - const { isIncoming, onClick, referencedMessageNotFound, withContentAbove } = props; + const isPublic = useSelector(isPublicGroupConversation); if (!validateQuote(props)) { return null; } + const { isIncoming, referencedMessageNotFound, attachment, text, onClick } = props; + return ( - <div - className={classNames( - 'module-quote-container', - withContentAbove ? 'module-quote-container--with-content-above' : null - )} - > + <div className={classNames('module-quote-container')}> <div onClick={onClick} role="button" @@ -340,7 +364,6 @@ export const Quote = (props: QuotePropsWithListener) => { 'module-quote', isIncoming ? 'module-quote--incoming' : 'module-quote--outgoing', !onClick ? 'module-quote--no-click' : null, - withContentAbove ? 'module-quote--with-content-above' : null, referencedMessageNotFound ? 'module-quote--with-reference-warning' : null )} > @@ -351,14 +374,21 @@ export const Quote = (props: QuotePropsWithListener) => { authorProfileName={props.authorProfileName} isFromMe={props.isFromMe} isIncoming={props.isIncoming} - showPubkeyForAuthor={props.isPublic} + showPubkeyForAuthor={isPublic} /> <QuoteGenericFile {...props} /> - <QuoteText {...props} /> + <QuoteText isIncoming={isIncoming} text={text} attachment={attachment} /> </div> - <QuoteIconContainer {...props} handleImageErrorBound={handleImageErrorBound} /> + <QuoteIconContainer + attachment={attachment} + handleImageErrorBound={handleImageErrorBound} + imageBroken={imageBroken} + /> </div> - <QuoteReferenceWarning {...props} /> + <QuoteReferenceWarning + isIncoming={isIncoming} + referencedMessageNotFound={referencedMessageNotFound} + /> </div> ); }; diff --git a/ts/components/conversation/message/MessageAuthorText.tsx b/ts/components/conversation/message/MessageAuthorText.tsx index 758c4a970..46b331ca0 100644 --- a/ts/components/conversation/message/MessageAuthorText.tsx +++ b/ts/components/conversation/message/MessageAuthorText.tsx @@ -1,7 +1,11 @@ import React from 'react'; -import { ConversationTypeEnum } from '../../../models/conversation'; +import { useSelector } from 'react-redux'; import { MessageModelType } from '../../../models/messageType'; import { PubKey } from '../../../session/types/PubKey'; +import { + isGroupConversation, + isPublicGroupConversation, +} from '../../../state/selectors/conversations'; import { Flex } from '../../basic/Flex'; import { ContactName } from '../ContactName'; @@ -9,9 +13,7 @@ export type MessageAuthorProps = { authorName: string | null; authorProfileName: string | null; authorPhoneNumber: string; - conversationType: ConversationTypeEnum; direction: MessageModelType; - isPublic: boolean; firstMessageOfSeries: boolean; }; @@ -20,15 +22,16 @@ export const MessageAuthorText = (props: MessageAuthorProps) => { authorName, authorPhoneNumber, authorProfileName, - conversationType, direction, - isPublic, firstMessageOfSeries, } = props; + const isPublic = useSelector(isPublicGroupConversation); + const isGroup = useSelector(isGroupConversation); + const title = authorName ? authorName : authorPhoneNumber; - if (direction !== 'incoming' || conversationType !== 'group' || !title || !firstMessageOfSeries) { + if (direction !== 'incoming' || !isGroup || !title || !firstMessageOfSeries) { return null; } @@ -37,17 +40,15 @@ export const MessageAuthorText = (props: MessageAuthorProps) => { const displayedPubkey = authorProfileName ? shortenedPubkey : authorPhoneNumber; return ( - <div className="module-message__author"> - <Flex container={true}> - <ContactName - phoneNumber={displayedPubkey} - name={authorName} - profileName={authorProfileName} - module="module-message__author" - boldProfileName={true} - shouldShowPubkey={Boolean(isPublic)} - /> - </Flex> - </div> + <Flex container={true}> + <ContactName + phoneNumber={displayedPubkey} + name={authorName} + profileName={authorProfileName} + module="module-message__author" + boldProfileName={true} + shouldShowPubkey={Boolean(isPublic)} + /> + </Flex> ); }; diff --git a/ts/components/session/conversation/SessionQuotedMessageComposition.tsx b/ts/components/session/conversation/SessionQuotedMessageComposition.tsx index eea2b1100..2b8b93354 100644 --- a/ts/components/session/conversation/SessionQuotedMessageComposition.tsx +++ b/ts/components/session/conversation/SessionQuotedMessageComposition.tsx @@ -93,10 +93,6 @@ export const SessionQuotedMessageComposition = () => { attachment={firstImageAttachment} height={100} width={100} - curveTopLeft={true} - curveTopRight={true} - curveBottomLeft={true} - curveBottomRight={true} url={firstImageAttachment.thumbnail.objectUrl} /> )} diff --git a/ts/components/session/settings/SessionSettings.tsx b/ts/components/session/settings/SessionSettings.tsx index 59459f019..4fde93db9 100644 --- a/ts/components/session/settings/SessionSettings.tsx +++ b/ts/components/session/settings/SessionSettings.tsx @@ -363,6 +363,7 @@ class SettingsViewInner extends React.Component<SettingsViewProps, State> { title: window.i18n('linkPreviewsTitle'), message: window.i18n('linkPreviewsConfirmMessage'), okTheme: SessionButtonColor.Danger, + // onClickOk: }) ); } diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 8d423b90a..cb3fe5ac2 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -48,6 +48,37 @@ export const getSelectedConversation = createSelector( } ); +/** + * Returns true if the current conversation selected is a group conversation. + * Returns false if the current conversation selected is not a group conversation, or none are selected + */ +export const isGroupConversation = createSelector( + getSelectedConversation, + (state: ReduxConversationType | undefined): boolean => { + return state?.type === 'group' || false; + } +); + +/** + * Returns true if the current conversation selected is a closed group and false otherwise. + */ +export const isClosedGroupConversation = createSelector( + getSelectedConversation, + (state: ReduxConversationType | undefined): boolean => { + return (state?.type === 'group' && !state.isPublic) || false; + } +); + +/** + * Returns true if the current conversation selected is a public group and false otherwise. + */ +export const isPublicGroupConversation = createSelector( + getSelectedConversation, + (state: ReduxConversationType | undefined): boolean => { + return (state?.type === 'group' && state.isPublic) || false; + } +); + export const getOurPrimaryConversation = createSelector( getConversations, (state: ConversationsStateType): ReduxConversationType =>