diff --git a/_locales/en/messages.json b/_locales/en/messages.json index ae35a7e01f80..18fc63b37faa 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -919,6 +919,10 @@ "message": "Downloading", "description": "Shown in the message bubble while a long message attachment is being downloaded" }, + "downloadFullMessage": { + "message": "Download Full Message", + "description": "Shown in the message bubble while a long message attachment is not downloaded" + }, "downloadAttachment": { "message": "Download Attachment", "description": "Shown in a message's triple-dot menu if there isn't room for a dedicated download button" diff --git a/stylesheets/components/MessageBody.scss b/stylesheets/components/MessageBody.scss index e57f0ce89490..d6314b264577 100644 --- a/stylesheets/components/MessageBody.scss +++ b/stylesheets/components/MessageBody.scss @@ -6,6 +6,7 @@ font-weight: bold; } + &__download-body, &__read-more { @include button-reset; font-weight: bold; diff --git a/ts/components/conversation/Message.stories.tsx b/ts/components/conversation/Message.stories.tsx index 7d9bb6092273..9293e7394456 100644 --- a/ts/components/conversation/Message.stories.tsx +++ b/ts/components/conversation/Message.stories.tsx @@ -19,6 +19,7 @@ import { IMAGE_PNG, IMAGE_WEBP, VIDEO_MP4, + LONG_MESSAGE, stringToMIMEType, IMAGE_GIF, } from '../../types/MIME'; @@ -205,7 +206,11 @@ const createProps = (overrideProps: Partial = {}): Props => ({ status: overrideProps.status || 'sent', text: overrideProps.text || text('text', ''), textDirection: overrideProps.textDirection || TextDirection.Default, - textPending: boolean('textPending', overrideProps.textPending || false), + textAttachment: overrideProps.textAttachment || { + contentType: LONG_MESSAGE, + size: 123, + pending: boolean('textPending', false), + }, theme: ThemeType.light, timestamp: number('timestamp', overrideProps.timestamp || Date.now()), }); @@ -420,7 +425,27 @@ story.add('Will expire but still sending', () => { story.add('Pending', () => { const props = createProps({ text: 'Hello there from a pal! I am sending a long message so that it will wrap a bit, since I like that look.', - textPending: true, + textAttachment: { + contentType: LONG_MESSAGE, + size: 123, + pending: true, + }, + }); + + return renderBothDirections(props); +}); + +story.add('Long body can be downloaded', () => { + const props = createProps({ + text: 'Hello there from a pal! I am sending a long message so that it will wrap a bit, since I like that look.', + textAttachment: { + contentType: LONG_MESSAGE, + size: 123, + pending: false, + error: true, + digest: 'abc', + key: 'def', + }, }); return renderBothDirections(props); diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 30379c5dfa42..53b2eb7cfb84 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -201,7 +201,7 @@ export type PropsData = { displayLimit?: number; text?: string; textDirection: TextDirection; - textPending?: boolean; + textAttachment?: AttachmentType; isSticker?: boolean; isSelected?: boolean; isSelectedCounter?: number; @@ -818,7 +818,7 @@ export class Message extends React.PureComponent { status, i18n, text, - textPending, + textAttachment, timestamp, id, showMessageDetail, @@ -842,7 +842,7 @@ export class Message extends React.PureComponent { onWidthMeasured={isInline ? this.updateMetadataWidth : undefined} showMessageDetail={showMessageDetail} status={status} - textPending={textPending} + textPending={textAttachment?.pending} timestamp={timestamp} /> ); @@ -903,7 +903,7 @@ export class Message extends React.PureComponent { shouldCollapseBelow, status, text, - textPending, + textAttachment, theme, timestamp, } = this.props; @@ -1031,7 +1031,7 @@ export class Message extends React.PureComponent { played, showMessageDetail, status, - textPending, + textPending: textAttachment?.pending, timestamp, kickOffAttachmentDownload() { @@ -1206,6 +1206,7 @@ export class Message extends React.PureComponent { width={72} url={first.image.url} attachment={first.image} + blurHash={first.image.blurHash} onError={this.handleImageError} i18n={i18n} onClick={onPreviewImageClick} @@ -1699,10 +1700,11 @@ export class Message extends React.PureComponent { id, messageExpanded, openConversation, + kickOffAttachmentDownload, status, text, textDirection, - textPending, + textAttachment, } = this.props; const { metadataWidth } = this.state; const isRTL = textDirection === TextDirection.RightToLeft; @@ -1741,8 +1743,17 @@ export class Message extends React.PureComponent { id={id} messageExpanded={messageExpanded} openConversation={openConversation} + kickOffBodyDownload={() => { + if (!textAttachment) { + return; + } + kickOffAttachmentDownload({ + attachment: textAttachment, + messageId: id, + }); + }} text={contents || ''} - textPending={textPending} + textAttachment={textAttachment} /> {!isRTL && this.getMetadataPlacement() === MetadataPlacement.InlineWithText && ( diff --git a/ts/components/conversation/MessageBody.stories.tsx b/ts/components/conversation/MessageBody.stories.tsx index 866645323a8d..68e525be4de2 100644 --- a/ts/components/conversation/MessageBody.stories.tsx +++ b/ts/components/conversation/MessageBody.stories.tsx @@ -25,7 +25,9 @@ const createProps = (overrideProps: Partial = {}): Props => ({ direction: 'incoming', i18n, text: text('text', overrideProps.text || ''), - textPending: boolean('textPending', overrideProps.textPending || false), + textAttachment: overrideProps.textAttachment || { + pending: boolean('textPending', false), + }, }); story.add('Links Enabled', () => { @@ -91,7 +93,9 @@ story.add('Jumbomoji Disabled by Text', () => { story.add('Text Pending', () => { const props = createProps({ text: 'Check out https://www.signal.org', - textPending: true, + textAttachment: { + pending: true, + }, }); return ; diff --git a/ts/components/conversation/MessageBody.tsx b/ts/components/conversation/MessageBody.tsx index f6829f2dfd3c..960245ce5917 100644 --- a/ts/components/conversation/MessageBody.tsx +++ b/ts/components/conversation/MessageBody.tsx @@ -4,6 +4,8 @@ import type { KeyboardEvent } from 'react'; import React from 'react'; +import type { AttachmentType } from '../../types/Attachment'; +import { canBeDownloaded } from '../../types/Attachment'; import type { SizeClassType } from '../emoji/lib'; import { getSizeClass } from '../emoji/lib'; import { AtMentionify } from './AtMentionify'; @@ -25,7 +27,7 @@ type OpenConversationActionType = ( export type Props = { direction?: 'incoming' | 'outgoing'; text: string; - textPending?: boolean; + textAttachment?: Pick; /** If set, all emoji will be the same size. Otherwise, just one emoji will be large. */ disableJumbomoji?: boolean; /** If set, links will be left alone instead of turned into clickable `` tags. */ @@ -34,6 +36,7 @@ export type Props = { bodyRanges?: BodyRangesType; onIncreaseTextLength?: () => unknown; openConversation?: OpenConversationActionType; + kickOffBodyDownload?: () => void; }; const renderEmoji = ({ @@ -71,10 +74,12 @@ export function MessageBody({ onIncreaseTextLength, openConversation, text, - textPending, + textAttachment, + kickOffBodyDownload, }: Props): JSX.Element { const hasReadMore = Boolean(onIncreaseTextLength); - const textWithSuffix = textPending || hasReadMore ? `${text}...` : text; + const textWithSuffix = + textAttachment?.pending || hasReadMore ? `${text}...` : text; const sizeClass = disableJumbomoji ? undefined : getSizeClass(text); const processedText = AtMentionify.preprocessMentions( @@ -103,6 +108,40 @@ export function MessageBody({ ); }; + let pendingContent: React.ReactNode; + if (hasReadMore) { + pendingContent = null; + } else if (textAttachment?.pending) { + pendingContent = ( + {i18n('downloading')} + ); + } else if ( + textAttachment && + canBeDownloaded(textAttachment) && + kickOffBodyDownload + ) { + pendingContent = ( + + {' '} + + + ); + } + return ( {disableLinks ? ( @@ -127,9 +166,7 @@ export function MessageBody({ }} /> )} - {textPending ? ( - {i18n('downloading')} - ) : null} + {pendingContent} {onIncreaseTextLength ? (