diff --git a/stylesheets/_quote.scss b/stylesheets/_quote.scss index fee3d1048..bd10407bf 100644 --- a/stylesheets/_quote.scss +++ b/stylesheets/_quote.scss @@ -159,34 +159,6 @@ } } - /* Reference Warning */ - &__reference { - &-warning { - height: 26px; - display: flex; - flex-direction: row; - align-items: center; - - background-color: var(--message-link-preview-background-color); - padding-inline-start: 8px; - padding-inline-end: 8px; - margin-inline-end: 8px; - } - - &-warning__icon { - height: 16px; - width: 16px; - @include color-svg('../images/broken-link.svg', var(--message-bubbles-received-text-color)); - } - - &-warning__text { - margin-inline-start: 6px; - color: var(--message-bubbles-received-text-color); - font-size: var(--font-size-sm); - line-height: 18px; - } - } - /* Misc */ &--no-click { cursor: auto; @@ -225,14 +197,4 @@ margin-top: var(--margins-xs); min-width: 300px; // if the quoted content is small it doesn't look very good so we set a minimum padding-right: var(--margins-xs); - - /* This is not within the module-quote class so we handle it separately */ - .module-quote__reference-warning--outgoing { - .module-quote__reference-warning__text { - color: var(--message-bubbles-sent-text-color); - } - .module-quote__reference-warning__icon { - @include color-svg('../images/broken-link.svg', var(--message-bubbles-sent-text-color)); - } - } } diff --git a/ts/components/conversation/message/message-content/MessageQuote.tsx b/ts/components/conversation/message/message-content/MessageQuote.tsx index 82c1d73d7..c99991259 100644 --- a/ts/components/conversation/message/message-content/MessageQuote.tsx +++ b/ts/components/conversation/message/message-content/MessageQuote.tsx @@ -9,7 +9,7 @@ import { isMessageDetailView, isMessageSelectionMode, } from '../../../../state/selectors/conversations'; -import { Quote } from './Quote'; +import { Quote } from './quote/Quote'; import { ToastUtils } from '../../../../session/utils'; import { Data } from '../../../../data/data'; import { MessageModel } from '../../../../models/message'; @@ -23,12 +23,36 @@ type Props = { export type MessageQuoteSelectorProps = Pick; export const MessageQuote = (props: Props) => { - const selected = useSelector(state => getMessageQuoteProps(state as any, props.messageId)); const multiSelectMode = useSelector(isMessageSelectionMode); const isMessageDetailViewMode = useSelector(isMessageDetailView); - const quote = selected ? selected.quote : undefined; - const direction = selected ? selected.direction : undefined; + const selected = useSelector(state => getMessageQuoteProps(state as any, props.messageId)); + if (!selected) { + return null; + } + + const { quote, direction } = selected; + if (!quote || !quote.sender || !quote.messageId) { + return null; + } + + const { + text, + attachment, + // TODO + // isFromMe, + sender: quoteAuthor, + authorProfileName, + authorName, + messageId: quotedMessageSentAt, + referencedMessageNotFound, + } = quote; + + const quoteText = text || window.i18n('originalMessageNotFound'); + const quoteNotFound = referencedMessageNotFound || false; + + const shortenedPubkey = PubKey.shorten(quoteAuthor); + const displayedPubkey = authorProfileName ? shortenedPubkey : quoteAuthor; const onQuoteClick = useCallback( async (event: React.MouseEvent) => { @@ -45,18 +69,15 @@ export const MessageQuote = (props: Props) => { return; } - const { - referencedMessageNotFound, - messageId: quotedMessageSentAt, - sender: quoteAuthor, - } = quote; // For simplicity's sake, we show the 'not found' toast no matter what if we were - // not able to find the referenced message when the quote was received. - if (referencedMessageNotFound || !quotedMessageSentAt || !quoteAuthor) { + // not able to find the referenced message when the quote was received. + if (quoteNotFound || !quotedMessageSentAt || !quoteAuthor) { ToastUtils.pushOriginalNotFound(); return; } + // TODO Should no longer have to do this lookup? + // Can just use referencedMessageNotFound? const collection = await Data.getMessagesBySentAt(_.toNumber(quotedMessageSentAt)); const foundInDb = collection.find((item: MessageModel) => { const messageAuthor = item.get('source'); @@ -68,6 +89,7 @@ export const MessageQuote = (props: Props) => { ToastUtils.pushOriginalNotFound(); return; } + void openConversationToSpecificMessage({ conversationKey: foundInDb.get('conversationId'), messageIdToNavigateTo: foundInDb.get('id'), @@ -76,22 +98,12 @@ export const MessageQuote = (props: Props) => { }, [quote, multiSelectMode, props.messageId] ); - if (!selected) { - return null; - } - - if (!quote || !quote.sender || !quote.messageId) { - return null; - } - const shortenedPubkey = PubKey.shorten(quote.sender); - - const displayedPubkey = quote.authorProfileName ? shortenedPubkey : quote.sender; return ( ) => void; -}; - -export interface QuotedAttachmentType { - contentType: MIME.MIMEType; - fileName: string; - /** Not included in protobuf */ - isVoiceMessage: boolean; - thumbnail?: Attachment; -} - -interface Attachment { - contentType: MIME.MIMEType; - /** Not included in protobuf, and is loaded asynchronously */ - objectUrl?: string; -} - -function validateQuote(quote: QuotePropsWithoutListener): boolean { - if (quote.text) { - return true; - } - - if (quote.attachment) { - return true; - } - - return false; -} - -function getObjectUrl(thumbnail: Attachment | undefined): string | undefined { - if (thumbnail && thumbnail.objectUrl) { - return thumbnail.objectUrl; - } - - return; -} - -function getTypeLabel({ - contentType, - isVoiceMessage, -}: { - contentType: MIME.MIMEType; - isVoiceMessage: boolean; -}): string | undefined { - if (GoogleChrome.isVideoTypeSupported(contentType)) { - return window.i18n('video'); - } - if (GoogleChrome.isImageTypeSupported(contentType)) { - return window.i18n('photo'); - } - if (MIME.isAudio(contentType) && isVoiceMessage) { - return window.i18n('voiceMessage'); - } - if (MIME.isAudio(contentType)) { - return window.i18n('audio'); - } - - return; -} -export const QuoteIcon = (props: any) => { - const { icon } = props; - - return ( -
-
-
-
-
-
-
- ); -}; - -export const QuoteImage = (props: { - handleImageErrorBound: () => void; - url: string; - contentType: string; - icon?: string; -}) => { - const { url, icon, contentType, handleImageErrorBound } = props; - const disableDrag = useDisableDrag(); - - const { loading, urlToLoad } = useEncryptedFileFetch(url, contentType, false); - const srcData = !loading ? urlToLoad : ''; - - const iconElement = icon ? ( -
-
-
-
-
- ) : null; - - return ( -
- {window.i18n('quoteThumbnailAlt')} - {iconElement} -
- ); -}; - -export const QuoteGenericFile = ( - props: Pick -) => { - const { attachment, isIncoming } = props; - - if (!attachment) { - return null; - } - - const { fileName, contentType } = attachment; - const isGenericFile = - !GoogleChrome.isVideoTypeSupported(contentType) && - !GoogleChrome.isImageTypeSupported(contentType) && - !MIME.isAudio(contentType); - - if (!isGenericFile) { - return null; - } - - return ( -
-
-
- {fileName} -
-
- ); -}; - -export const QuoteIconContainer = ( - props: Pick & { - handleImageErrorBound: () => void; - imageBroken: boolean; - } -) => { - const { attachment, imageBroken, handleImageErrorBound } = props; - - if (!attachment) { - return null; - } - - const { contentType, thumbnail } = attachment; - const objectUrl = getObjectUrl(thumbnail); - - if (GoogleChrome.isVideoTypeSupported(contentType)) { - return objectUrl && !imageBroken ? ( - - ) : ( - - ); - } - if (GoogleChrome.isImageTypeSupported(contentType)) { - return objectUrl && !imageBroken ? ( - - ) : ( - - ); - } - if (MIME.isAudio(contentType)) { - return ; - } - return null; -}; - -export const QuoteText = ( - props: Pick -) => { - const { text, attachment, isIncoming } = props; - - const convoId = useSelector(getSelectedConversationKey); - const isGroup = !useIsPrivate(convoId); - - if (text) { - return ( -
- -
- ); - } - - if (!attachment) { - return null; - } - - const { contentType, isVoiceMessage } = attachment; - - const typeLabel = getTypeLabel({ contentType, isVoiceMessage }); - if (typeLabel) { - return ( -
- {typeLabel} -
- ); - } - - return null; -}; - -type QuoteAuthorProps = { - author: string; - authorProfileName?: string; - authorName?: string; - isFromMe: boolean; - isIncoming: boolean; - showPubkeyForAuthor?: boolean; -}; - -const QuoteAuthor = (props: QuoteAuthorProps) => { - const { authorProfileName, author, authorName, isFromMe, isIncoming } = props; - - return ( -
- {isFromMe ? ( - window.i18n('you') - ) : ( - - )} -
- ); -}; - -export const QuoteReferenceWarning = ( - props: Pick -) => { - const { isIncoming, referencedMessageNotFound } = props; - - if (!referencedMessageNotFound) { - return null; - } - - return ( -
-
-
- {window.i18n('originalMessageNotFound')} -
-
- ); -}; - -export const Quote = (props: QuotePropsWithListener) => { - const [imageBroken, setImageBroken] = useState(false); - const handleImageErrorBound = () => { - setImageBroken(true); - }; - - const isPublic = useSelector(isPublicGroupConversation); - - if (!validateQuote(props)) { - return null; - } - - const { isIncoming, referencedMessageNotFound, attachment, text, onClick } = props; - - return ( -
-
-
- - - -
- -
- -
- ); -}; diff --git a/ts/components/conversation/message/message-content/quote/Quote.tsx b/ts/components/conversation/message/message-content/quote/Quote.tsx new file mode 100644 index 000000000..d8167d462 --- /dev/null +++ b/ts/components/conversation/message/message-content/quote/Quote.tsx @@ -0,0 +1,100 @@ +import React, { useState } from 'react'; +import classNames from 'classnames'; + +import * as MIME from '../../../../../types/MIME'; + +import { useSelector } from 'react-redux'; + +import { isPublicGroupConversation } from '../../../../../state/selectors/conversations'; +import { QuoteAuthor } from './QuoteAuthor'; +import { QuoteGenericFile } from './QuoteGenericFile'; +import { QuoteText } from './QuoteText'; +import { QuoteIconContainer } from './QuoteIconContainer'; + +export type QuotePropsWithoutListener = { + attachment?: QuotedAttachmentType; + sender: string; + authorProfileName?: string; + authorName?: string; + isFromMe: boolean; + isIncoming: boolean; + text: string | null; + referencedMessageNotFound: boolean; +}; + +export type QuotePropsWithListener = QuotePropsWithoutListener & { + onClick?: (e: React.MouseEvent) => void; +}; + +export interface Attachment { + contentType: MIME.MIMEType; + /** Not included in protobuf, and is loaded asynchronously */ + objectUrl?: string; +} + +export interface QuotedAttachmentType { + contentType: MIME.MIMEType; + fileName: string; + /** Not included in protobuf */ + isVoiceMessage: boolean; + thumbnail?: Attachment; +} + +function validateQuote(quote: QuotePropsWithoutListener): boolean { + if (quote.text) { + return true; + } + + if (quote.attachment) { + return true; + } + + return false; +} + +export const Quote = (props: QuotePropsWithListener) => { + const [imageBroken, setImageBroken] = useState(false); + const handleImageErrorBound = () => { + setImageBroken(true); + }; + + const isPublic = useSelector(isPublicGroupConversation); + + if (!validateQuote(props)) { + return null; + } + + const { isIncoming, attachment, text, onClick } = props; + + return ( +
+
+
+ + + +
+ +
+
+ ); +}; diff --git a/ts/components/conversation/message/message-content/quote/QuoteAuthor.tsx b/ts/components/conversation/message/message-content/quote/QuoteAuthor.tsx new file mode 100644 index 000000000..b09418813 --- /dev/null +++ b/ts/components/conversation/message/message-content/quote/QuoteAuthor.tsx @@ -0,0 +1,38 @@ +import classNames from 'classnames'; +import React = require('react'); +import { ContactName } from '../../../ContactName'; +import { PubKey } from '../../../../../session/types'; + +type QuoteAuthorProps = { + author: string; + authorProfileName?: string; + authorName?: string; + isFromMe: boolean; + isIncoming: boolean; + showPubkeyForAuthor?: boolean; +}; + +export const QuoteAuthor = (props: QuoteAuthorProps) => { + const { authorProfileName, author, authorName, isFromMe, isIncoming } = props; + + return ( +
+ {isFromMe ? ( + window.i18n('you') + ) : ( + + )} +
+ ); +}; diff --git a/ts/components/conversation/message/message-content/quote/QuoteGenericFile.tsx b/ts/components/conversation/message/message-content/quote/QuoteGenericFile.tsx new file mode 100644 index 000000000..ea4579fe8 --- /dev/null +++ b/ts/components/conversation/message/message-content/quote/QuoteGenericFile.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import classNames from 'classnames'; +import { MIME } from '../../../../../types'; +import { GoogleChrome } from '../../../../../util'; +import { QuotePropsWithoutListener } from './Quote'; + +export const QuoteGenericFile = ( + props: Pick +) => { + const { attachment, isIncoming } = props; + + if (!attachment) { + return null; + } + + const { fileName, contentType } = attachment; + const isGenericFile = + !GoogleChrome.isVideoTypeSupported(contentType) && + !GoogleChrome.isImageTypeSupported(contentType) && + !MIME.isAudio(contentType); + + if (!isGenericFile) { + return null; + } + + return ( +
+
+
+ {fileName} +
+
+ ); +}; diff --git a/ts/components/conversation/message/message-content/quote/QuoteIcon.tsx b/ts/components/conversation/message/message-content/quote/QuoteIcon.tsx new file mode 100644 index 000000000..0ecc33706 --- /dev/null +++ b/ts/components/conversation/message/message-content/quote/QuoteIcon.tsx @@ -0,0 +1,21 @@ +import classNames from 'classnames'; +import React from 'react'; + +export const QuoteIcon = (props: any) => { + const { icon } = props; + + return ( +
+
+
+
+
+
+
+ ); +}; diff --git a/ts/components/conversation/message/message-content/quote/QuoteIconContainer.tsx b/ts/components/conversation/message/message-content/quote/QuoteIconContainer.tsx new file mode 100644 index 000000000..e565d7753 --- /dev/null +++ b/ts/components/conversation/message/message-content/quote/QuoteIconContainer.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { Attachment, QuotePropsWithoutListener } from './Quote'; +import { GoogleChrome } from '../../../../../util'; +import { MIME } from '../../../../../types'; + +import { noop } from 'lodash'; +import { QuoteImage } from './QuoteImage'; +import { QuoteIcon } from './QuoteIcon'; + +function getObjectUrl(thumbnail: Attachment | undefined): string | undefined { + if (thumbnail && thumbnail.objectUrl) { + return thumbnail.objectUrl; + } + + return; +} + +export const QuoteIconContainer = ( + props: Pick & { + handleImageErrorBound: () => void; + imageBroken: boolean; + } +) => { + const { attachment, imageBroken, handleImageErrorBound } = props; + + if (!attachment) { + return null; + } + + const { contentType, thumbnail } = attachment; + const objectUrl = getObjectUrl(thumbnail); + + if (GoogleChrome.isVideoTypeSupported(contentType)) { + return objectUrl && !imageBroken ? ( + + ) : ( + + ); + } + if (GoogleChrome.isImageTypeSupported(contentType)) { + return objectUrl && !imageBroken ? ( + + ) : ( + + ); + } + if (MIME.isAudio(contentType)) { + return ; + } + return null; +}; diff --git a/ts/components/conversation/message/message-content/quote/QuoteImage.tsx b/ts/components/conversation/message/message-content/quote/QuoteImage.tsx new file mode 100644 index 000000000..aefd42a99 --- /dev/null +++ b/ts/components/conversation/message/message-content/quote/QuoteImage.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import classNames from 'classnames'; +import { useDisableDrag } from '../../../../../hooks/useDisableDrag'; +import { useEncryptedFileFetch } from '../../../../../hooks/useEncryptedFileFetch'; + +export const QuoteImage = (props: { + handleImageErrorBound: () => void; + url: string; + contentType: string; + icon?: string; +}) => { + const { url, icon, contentType, handleImageErrorBound } = props; + const disableDrag = useDisableDrag(); + + const { loading, urlToLoad } = useEncryptedFileFetch(url, contentType, false); + const srcData = !loading ? urlToLoad : ''; + + const iconElement = icon ? ( +
+
+
+
+
+ ) : null; + + return ( +
+ {window.i18n('quoteThumbnailAlt')} + {iconElement} +
+ ); +}; diff --git a/ts/components/conversation/message/message-content/quote/QuoteText.tsx b/ts/components/conversation/message/message-content/quote/QuoteText.tsx new file mode 100644 index 000000000..98a3f1001 --- /dev/null +++ b/ts/components/conversation/message/message-content/quote/QuoteText.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import { QuotePropsWithoutListener } from './Quote'; +import { useSelector } from 'react-redux'; +import { getSelectedConversationKey } from '../../../../../state/selectors/conversations'; +import { useIsPrivate } from '../../../../../hooks/useParamSelector'; +import classNames from 'classnames'; +import { MessageBody } from '../MessageBody'; +import { MIME } from '../../../../../types'; +import { GoogleChrome } from '../../../../../util'; + +function getTypeLabel({ + contentType, + isVoiceMessage, +}: { + contentType: MIME.MIMEType; + isVoiceMessage: boolean; +}): string | undefined { + if (GoogleChrome.isVideoTypeSupported(contentType)) { + return window.i18n('video'); + } + if (GoogleChrome.isImageTypeSupported(contentType)) { + return window.i18n('photo'); + } + if (MIME.isAudio(contentType) && isVoiceMessage) { + return window.i18n('voiceMessage'); + } + if (MIME.isAudio(contentType)) { + return window.i18n('audio'); + } + + return; +} + +export const QuoteText = ( + props: Pick +) => { + const { text, attachment, isIncoming } = props; + + const convoId = useSelector(getSelectedConversationKey); + const isGroup = !useIsPrivate(convoId); + + if (text) { + return ( +
+ +
+ ); + } + + if (!attachment) { + return null; + } + + const { contentType, isVoiceMessage } = attachment; + + const typeLabel = getTypeLabel({ contentType, isVoiceMessage }); + if (typeLabel) { + return ( +
+ {typeLabel} +
+ ); + } + + return null; +}; diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 9c60f02d2..ba9c3e5ef 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -9,7 +9,7 @@ import { } from '../../models/messageType'; import { omit } from 'lodash'; import { ReplyingToMessageProps } from '../../components/conversation/composition/CompositionBox'; -import { QuotedAttachmentType } from '../../components/conversation/message/message-content/Quote'; +import { QuotedAttachmentType } from '../../components/conversation/message/message-content/quote/Quote'; import { LightBoxOptions } from '../../components/conversation/SessionConversation'; import { ConversationNotificationSettingType,