diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 284e14cb3d..717ffc2a5a 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -2226,15 +2226,15 @@ }, "message--getDescription--disappearing-media": { "message": "View-once Media", - "description": "Shown in notifications and in the left pane after view-once message is deleted." + "description": "Shown in notifications and in the left pane after view-once message is deleted. Also shown when quoting a view once media." }, "message--getDescription--disappearing-photo": { "message": "View-once Photo", - "description": "Shown in notifications and in the left pane when a message is a view once photo." + "description": "Shown in notifications and in the left pane when a message is a view once photo. Also shown when quoting a view once photo." }, "message--getDescription--disappearing-video": { "message": "View-once Video", - "description": "Shown in notifications and in the left pane when a message is a view once video." + "description": "Shown in notifications and in the left pane when a message is a view once video. Also shown when quoting a view once video." }, "message--deletedForEveryone": { "message": "This message was deleted.", diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 5c65ae435c..e984aaa15b 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -1789,6 +1789,9 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05', .module-quote__icon-container__icon--movie { @include color-svg('../images/movie.svg', $color-ultramarine); } +.module-quote__icon-container__icon--view-once { + @include color-svg('../images/icons/v2/view-once-24.svg', $color-ultramarine); +} .module-quote__generic-file { display: flex; diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index bed996b9b0..a9157dfe55 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -149,6 +149,7 @@ export type PropsData = { authorName?: string; bodyRanges?: BodyRangesType; referencedMessageNotFound: boolean; + isViewOnce: boolean; }; previews: Array; isExpired?: boolean; @@ -1062,7 +1063,7 @@ export class Message extends React.Component { const withContentAbove = conversationType === 'group' && direction === 'incoming'; - const { referencedMessageNotFound } = quote; + const { isViewOnce, referencedMessageNotFound } = quote; const clickHandler = disableScroll ? undefined @@ -1087,6 +1088,7 @@ export class Message extends React.Component { bodyRanges={quote.bodyRanges} conversationColor={conversationColor} customColor={customColor} + isViewOnce={isViewOnce} referencedMessageNotFound={referencedMessageNotFound} isFromMe={quote.isFromMe} withContentAbove={withContentAbove} diff --git a/ts/components/conversation/Quote.stories.tsx b/ts/components/conversation/Quote.stories.tsx index 94f3f08ac1..506ead7cc4 100644 --- a/ts/components/conversation/Quote.stories.tsx +++ b/ts/components/conversation/Quote.stories.tsx @@ -81,6 +81,7 @@ const renderInMessage = ({ conversationColor, isFromMe, rawAttachment, + isViewOnce, referencedMessageNotFound, text: quoteText, }: Props) => { @@ -96,6 +97,7 @@ const renderInMessage = ({ conversationColor, isFromMe, rawAttachment, + isViewOnce, referencedMessageNotFound, sentAt: Date.now() - 30 * 1000, text: quoteText, @@ -133,6 +135,7 @@ const createProps = (overrideProps: Partial = {}): Props => ({ 'referencedMessageNotFound', overrideProps.referencedMessageNotFound || false ), + isViewOnce: boolean('isViewOnce', overrideProps.isViewOnce || false), text: text( 'text', isString(overrideProps.text) @@ -247,6 +250,20 @@ story.add('Image Attachment w/o Thumbnail', () => { return ; }); +story.add('Image Tap-to-View', () => { + const props = createProps({ + text: '', + isViewOnce: true, + rawAttachment: { + contentType: IMAGE_PNG, + fileName: 'sax.png', + isVoiceMessage: false, + }, + }); + + return ; +}); + story.add('Video Only', () => { const props = createProps({ rawAttachment: { @@ -293,6 +310,20 @@ story.add('Video Attachment w/o Thumbnail', () => { return ; }); +story.add('Video Tap-to-View', () => { + const props = createProps({ + text: '', + isViewOnce: true, + rawAttachment: { + contentType: VIDEO_MP4, + fileName: 'great-video.mp4', + isVoiceMessage: false, + }, + }); + + return ; +}); + story.add('Audio Only', () => { const props = createProps({ rawAttachment: { @@ -359,6 +390,20 @@ story.add('Other File Only', () => { return ; }); +story.add('Media Tap-to-View', () => { + const props = createProps({ + text: '', + isViewOnce: true, + rawAttachment: { + contentType: AUDIO_MP3, + fileName: 'great-video.mp3', + isVoiceMessage: false, + }, + }); + + return ; +}); + story.add('Other File Attachment', () => { const props = createProps({ rawAttachment: { diff --git a/ts/components/conversation/Quote.tsx b/ts/components/conversation/Quote.tsx index e5e075cf36..b1132ce9d2 100644 --- a/ts/components/conversation/Quote.tsx +++ b/ts/components/conversation/Quote.tsx @@ -31,6 +31,7 @@ export type Props = { onClose?: () => void; text: string; rawAttachment?: QuotedAttachmentType; + isViewOnce: boolean; referencedMessageNotFound: boolean; }; @@ -83,19 +84,32 @@ function getObjectUrl(thumbnail: Attachment | undefined): string | undefined { function getTypeLabel({ i18n, + isViewOnce = false, contentType, isVoiceMessage, }: { i18n: LocalizerType; + isViewOnce?: boolean; contentType: MIME.MIMEType; isVoiceMessage: boolean; }): string | undefined { if (GoogleChrome.isVideoTypeSupported(contentType)) { + if (isViewOnce) { + return i18n('message--getDescription--disappearing-video'); + } return i18n('video'); } if (GoogleChrome.isImageTypeSupported(contentType)) { + if (isViewOnce) { + return i18n('message--getDescription--disappearing-photo'); + } return i18n('photo'); } + + if (isViewOnce) { + return i18n('message--getDescription--disappearing-media'); + } + if (MIME.isAudio(contentType) && isVoiceMessage) { return i18n('voiceMessage'); } @@ -217,7 +231,7 @@ export class Quote extends React.Component { } public renderIconContainer(): JSX.Element | null { - const { rawAttachment } = this.props; + const { rawAttachment, isViewOnce } = this.props; const { imageBroken } = this.state; const attachment = getAttachment(rawAttachment); @@ -228,6 +242,10 @@ export class Quote extends React.Component { const { contentType, thumbnail } = attachment; const objectUrl = getObjectUrl(thumbnail); + if (isViewOnce) { + return this.renderIcon('view-once'); + } + if (GoogleChrome.isVideoTypeSupported(contentType)) { return objectUrl && !imageBroken ? this.renderImage(objectUrl, 'play') @@ -246,7 +264,14 @@ export class Quote extends React.Component { } public renderText(): JSX.Element | null { - const { bodyRanges, i18n, text, rawAttachment, isIncoming } = this.props; + const { + bodyRanges, + i18n, + text, + rawAttachment, + isIncoming, + isViewOnce, + } = this.props; if (text) { const quoteText = bodyRanges @@ -274,7 +299,12 @@ export class Quote extends React.Component { const { contentType, isVoiceMessage } = attachment; - const typeLabel = getTypeLabel({ i18n, contentType, isVoiceMessage }); + const typeLabel = getTypeLabel({ + i18n, + isViewOnce, + contentType, + isVoiceMessage, + }); if (typeLabel) { return (
{ + ): Promise { const { getName } = Contact; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const contact = quotedMessage.getContact()!; @@ -3150,6 +3150,7 @@ export class ConversationModel extends window.Backbone bodyRanges: quotedMessage.get('bodyRanges'), id: quotedMessage.get('sent_at'), text: body || embeddedContactName, + isViewOnce: quotedMessage.isTapToView(), attachments: quotedMessage.isTapToView() ? [{ contentType: 'image/jpeg', fileName: null }] : await this.getQuoteAttachment(attachments, preview, sticker), diff --git a/ts/models/messages.ts b/ts/models/messages.ts index 722062a145..3201e4c608 100644 --- a/ts/models/messages.ts +++ b/ts/models/messages.ts @@ -1243,6 +1243,7 @@ export class MessageModel extends window.Backbone.Model { authorUuid, bodyRanges, id: sentAt, + isViewOnce, referencedMessageNotFound, text, } = quote; @@ -1342,6 +1343,7 @@ export class MessageModel extends window.Backbone.Model { rawAttachment: firstAttachment ? this.processQuoteAttachment(firstAttachment) : undefined, + isViewOnce, referencedMessageNotFound: !foundReference, sentAt: Number(sentAt), text: this.createNonBreakingLastSeparator(text), @@ -3363,10 +3365,15 @@ export class MessageModel extends window.Backbone.Model { contentType: 'image/jpeg', }, ]; + // eslint-disable-next-line no-param-reassign + quote.isViewOnce = true; return; } + // eslint-disable-next-line no-param-reassign + quote.isViewOnce = false; + // eslint-disable-next-line no-param-reassign quote.text = originalMessage.get('body'); if (firstAttachment) { diff --git a/ts/textsecure.d.ts b/ts/textsecure.d.ts index 4fb943eb47..0bc8c66577 100644 --- a/ts/textsecure.d.ts +++ b/ts/textsecure.d.ts @@ -653,14 +653,15 @@ export declare namespace DataMessageClass { // Note: deep nesting class Quote { - id: ProtoBigNumberType | null; - authorUuid: string | null; - text: string | null; + id?: ProtoBigNumberType | null; + authorUuid?: string | null; + text?: string | null; attachments?: Array; bodyRanges?: Array; // Added later during processing referencedMessageNotFound?: boolean; + isViewOnce?: boolean; } class BodyRange {