From 5c00b89600df90c0e621eae0da45fe2627965b87 Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Thu, 3 Oct 2019 12:03:46 -0700 Subject: [PATCH] Support for receiving View Once Video --- _locales/en/messages.json | 20 +++-- js/models/messages.js | 34 +++++--- js/views/conversation_view.js | 2 + protos/SignalService.proto | 9 ++- ts/components/Lightbox.tsx | 104 ++++++++++++++++++++----- ts/components/LightboxGallery.tsx | 11 +-- ts/components/conversation/Message.tsx | 7 +- ts/util/formatDuration.ts | 13 ++++ ts/util/lint/exceptions.json | 4 +- 9 files changed, 156 insertions(+), 48 deletions(-) create mode 100644 ts/util/formatDuration.ts diff --git a/_locales/en/messages.json b/_locales/en/messages.json index fa3e7d0dd2..75b04542b7 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1285,7 +1285,7 @@ "Label text for button to upgrade the app to the latest version" }, "mediaMessage": { - "message": "Media message", + "message": "Media Message", "description": "Description of a message that has an attachment and no text, displayed in the conversation list as a preview." }, @@ -1762,7 +1762,12 @@ "Shown in notifications and in the left pane when a message has features too new for this signal install." }, "message--getDescription--disappearing-photo": { - "message": "Disappearing photo", + "message": "Disappearing Photo", + "description": + "Shown in notifications and in the left pane when a message is a disappearing photo." + }, + "message--getDescription--disappearing-video": { + "message": "Disappering Video", "description": "Shown in notifications and in the left pane when a message is a disappearing photo." }, @@ -1929,14 +1934,19 @@ "Text shown on messages with with individual timers, after user has viewed it" }, "Message--tap-to-view--outgoing": { - "message": "Photo", + "message": "Media", "description": "Text shown on outgoing messages with with individual timers (inaccessble)" }, "Message--tap-to-view--incoming": { - "message": "View Photo", + "message": "Photo", "description": - "Text shown on messages with with individual timers, before user has viewed it" + "Text shown on photo messages with with individual timers, before user has viewed it" + }, + "Message--tap-to-view--incoming-video": { + "message": "Video", + "description": + "Text shown on video messages with with individual timers, before user has viewed it" }, "Conversation--getDraftPreview--attachment": { "message": "(attachment)", diff --git a/js/models/messages.js b/js/models/messages.js index b01f00c68a..a47d3daed1 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -36,6 +36,8 @@ savePackMetadata, getStickerPackStatus, } = window.Signal.Stickers; + const { GoogleChrome } = window.Signal.Util; + const { addStickerPackReference } = window.Signal.Data; const { bytesFromString } = window.Signal.Crypto; @@ -726,7 +728,23 @@ return i18n('message--getDescription--unsupported-message'); } if (this.isTapToView()) { - return i18n('message--getDescription--disappearing-photo'); + if (this.isErased()) { + return i18n('mediaMessage'); + } + + const attachments = this.get('attachments'); + if (!attachments || !attachments[0]) { + return i18n('mediaMessage'); + } + + const { contentType } = attachments[0]; + if (GoogleChrome.isImageTypeSupported(contentType)) { + return i18n('message--getDescription--disappearing-photo'); + } else if (GoogleChrome.isVideoTypeSupported(contentType)) { + return i18n('message--getDescription--disappearing-video'); + } + + return i18n('mediaMessage'); } if (this.isGroupUpdate()) { const groupUpdate = this.get('group_update'); @@ -874,11 +892,7 @@ } const firstAttachment = attachments[0]; - if ( - !window.Signal.Util.GoogleChrome.isImageTypeSupported( - firstAttachment.contentType - ) - ) { + if (!GoogleChrome.isImageTypeSupported(firstAttachment.contentType)) { return false; } @@ -1722,12 +1736,8 @@ if ( !firstAttachment || - (!window.Signal.Util.GoogleChrome.isImageTypeSupported( - firstAttachment.contentType - ) && - !window.Signal.Util.GoogleChrome.isVideoTypeSupported( - firstAttachment.contentType - )) + (!GoogleChrome.isImageTypeSupported(firstAttachment.contentType) && + !GoogleChrome.isVideoTypeSupported(firstAttachment.contentType)) ) { return message; } diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index 7d37049d99..42dcafec3b 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -1893,6 +1893,8 @@ return { objectURL: getAbsoluteTempPath(path), contentType, + onSave: null, // important so download button is omitted + isViewOnce: true, }; }; this.lightboxView = new Whisper.ReactWrapperView({ diff --git a/protos/SignalService.proto b/protos/SignalService.proto index 582f2da4da..8dee65e71f 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -172,10 +172,11 @@ message DataMessage { enum ProtocolVersion { option allow_alias = true; - INITIAL = 0; - MESSAGE_TIMERS = 1; - VIEW_ONCE = 2; - CURRENT = 2; + INITIAL = 0; + MESSAGE_TIMERS = 1; + VIEW_ONCE = 2; + VIEW_ONCE_VIDEO = 3; + CURRENT = 3; } optional string body = 1; diff --git a/ts/components/Lightbox.tsx b/ts/components/Lightbox.tsx index 3a695e753f..8d6fe419bf 100644 --- a/ts/components/Lightbox.tsx +++ b/ts/components/Lightbox.tsx @@ -8,6 +8,7 @@ import is from '@sindresorhus/is'; import * as GoogleChrome from '../util/GoogleChrome'; import * as MIME from '../types/MIME'; +import { formatDuration } from '../util/formatDuration'; import { LocalizerType } from '../types/Util'; const Colors = { @@ -29,10 +30,14 @@ interface Props { i18n: LocalizerType; objectURL: string; caption?: string; + isViewOnce: boolean; onNext?: () => void; onPrevious?: () => void; onSave?: () => void; } +interface State { + videoTime?: number; +} const CONTROLS_WIDTH = 50; const CONTROLS_SPACING = 10; @@ -116,6 +121,19 @@ const styles = { width: 50, height: 50, }, + timestampPill: { + borderRadius: '15px', + backgroundColor: '#000000', + color: '#eeefef', + fontSize: '16px', + letterSpacing: '0px', + lineHeight: '18px', + // This cast is necessary or typescript chokes + textAlign: 'center' as 'center', + padding: '6px', + paddingLeft: '18px', + paddingRight: '18px', + }, }; interface IconButtonProps { @@ -169,7 +187,7 @@ const Icon = ({ /> ); -export class Lightbox extends React.Component { +export class Lightbox extends React.Component { private readonly containerRef: React.RefObject; private readonly videoRef: React.RefObject; @@ -178,21 +196,39 @@ export class Lightbox extends React.Component { this.videoRef = React.createRef(); this.containerRef = React.createRef(); + + this.state = { + videoTime: undefined, + }; } public componentDidMount() { + const { isViewOnce } = this.props; + const useCapture = true; document.addEventListener('keyup', this.onKeyUp, useCapture); + const video = this.getVideo(); + if (video && isViewOnce) { + video.addEventListener('timeupdate', this.onTimeUpdate); + } + this.playVideo(); } public componentWillUnmount() { + const { isViewOnce } = this.props; + const useCapture = true; document.removeEventListener('keyup', this.onKeyUp, useCapture); + + const video = this.getVideo(); + if (video && isViewOnce) { + video.removeEventListener('timeupdate', this.onTimeUpdate); + } } - public playVideo() { + public getVideo() { if (!this.videoRef) { return; } @@ -202,11 +238,20 @@ export class Lightbox extends React.Component { return; } - if (current.paused) { + return current; + } + + public playVideo() { + const video = this.getVideo(); + if (!video) { + return; + } + + if (video.paused) { // tslint:disable-next-line no-floating-promises - current.play(); + video.play(); } else { - current.pause(); + video.pause(); } } @@ -215,11 +260,13 @@ export class Lightbox extends React.Component { caption, contentType, i18n, + isViewOnce, objectURL, onNext, onPrevious, onSave, } = this.props; + const { videoTime } = this.state; return (
{
{!is.undefined(contentType) - ? this.renderObject({ objectURL, contentType, i18n }) + ? this.renderObject({ objectURL, contentType, i18n, isViewOnce }) : null} {caption ?
{caption}
: null}
@@ -247,18 +294,24 @@ export class Lightbox extends React.Component { ) : null}
-
- {onPrevious ? ( - - ) : ( - - )} - {onNext ? ( - - ) : ( - - )} -
+ {isViewOnce && is.number(videoTime) ? ( +
+
{formatDuration(videoTime)}
+
+ ) : ( +
+ {onPrevious ? ( + + ) : ( + + )} + {onNext ? ( + + ) : ( + + )} +
+ )} ); } @@ -267,10 +320,12 @@ export class Lightbox extends React.Component { objectURL, contentType, i18n, + isViewOnce, }: { objectURL: string; contentType: MIME.MIMEType; i18n: LocalizerType; + isViewOnce: boolean; }) => { const isImageTypeSupported = GoogleChrome.isImageTypeSupported(contentType); if (isImageTypeSupported) { @@ -290,7 +345,8 @@ export class Lightbox extends React.Component {