Support for receiving View Once Video

This commit is contained in:
Scott Nonnenberg 2019-10-03 12:03:46 -07:00
parent 9229824c24
commit 5c00b89600
9 changed files with 156 additions and 48 deletions

View file

@ -1285,7 +1285,7 @@
"Label text for button to upgrade the app to the latest version" "Label text for button to upgrade the app to the latest version"
}, },
"mediaMessage": { "mediaMessage": {
"message": "Media message", "message": "Media Message",
"description": "description":
"Description of a message that has an attachment and no text, displayed in the conversation list as a preview." "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." "Shown in notifications and in the left pane when a message has features too new for this signal install."
}, },
"message--getDescription--disappearing-photo": { "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": "description":
"Shown in notifications and in the left pane when a message is a disappearing photo." "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" "Text shown on messages with with individual timers, after user has viewed it"
}, },
"Message--tap-to-view--outgoing": { "Message--tap-to-view--outgoing": {
"message": "Photo", "message": "Media",
"description": "description":
"Text shown on outgoing messages with with individual timers (inaccessble)" "Text shown on outgoing messages with with individual timers (inaccessble)"
}, },
"Message--tap-to-view--incoming": { "Message--tap-to-view--incoming": {
"message": "View Photo", "message": "Photo",
"description": "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": { "Conversation--getDraftPreview--attachment": {
"message": "(attachment)", "message": "(attachment)",

View file

@ -36,6 +36,8 @@
savePackMetadata, savePackMetadata,
getStickerPackStatus, getStickerPackStatus,
} = window.Signal.Stickers; } = window.Signal.Stickers;
const { GoogleChrome } = window.Signal.Util;
const { addStickerPackReference } = window.Signal.Data; const { addStickerPackReference } = window.Signal.Data;
const { bytesFromString } = window.Signal.Crypto; const { bytesFromString } = window.Signal.Crypto;
@ -726,7 +728,23 @@
return i18n('message--getDescription--unsupported-message'); return i18n('message--getDescription--unsupported-message');
} }
if (this.isTapToView()) { 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()) { if (this.isGroupUpdate()) {
const groupUpdate = this.get('group_update'); const groupUpdate = this.get('group_update');
@ -874,11 +892,7 @@
} }
const firstAttachment = attachments[0]; const firstAttachment = attachments[0];
if ( if (!GoogleChrome.isImageTypeSupported(firstAttachment.contentType)) {
!window.Signal.Util.GoogleChrome.isImageTypeSupported(
firstAttachment.contentType
)
) {
return false; return false;
} }
@ -1722,12 +1736,8 @@
if ( if (
!firstAttachment || !firstAttachment ||
(!window.Signal.Util.GoogleChrome.isImageTypeSupported( (!GoogleChrome.isImageTypeSupported(firstAttachment.contentType) &&
firstAttachment.contentType !GoogleChrome.isVideoTypeSupported(firstAttachment.contentType))
) &&
!window.Signal.Util.GoogleChrome.isVideoTypeSupported(
firstAttachment.contentType
))
) { ) {
return message; return message;
} }

View file

@ -1893,6 +1893,8 @@
return { return {
objectURL: getAbsoluteTempPath(path), objectURL: getAbsoluteTempPath(path),
contentType, contentType,
onSave: null, // important so download button is omitted
isViewOnce: true,
}; };
}; };
this.lightboxView = new Whisper.ReactWrapperView({ this.lightboxView = new Whisper.ReactWrapperView({

View file

@ -172,10 +172,11 @@ message DataMessage {
enum ProtocolVersion { enum ProtocolVersion {
option allow_alias = true; option allow_alias = true;
INITIAL = 0; INITIAL = 0;
MESSAGE_TIMERS = 1; MESSAGE_TIMERS = 1;
VIEW_ONCE = 2; VIEW_ONCE = 2;
CURRENT = 2; VIEW_ONCE_VIDEO = 3;
CURRENT = 3;
} }
optional string body = 1; optional string body = 1;

View file

@ -8,6 +8,7 @@ import is from '@sindresorhus/is';
import * as GoogleChrome from '../util/GoogleChrome'; import * as GoogleChrome from '../util/GoogleChrome';
import * as MIME from '../types/MIME'; import * as MIME from '../types/MIME';
import { formatDuration } from '../util/formatDuration';
import { LocalizerType } from '../types/Util'; import { LocalizerType } from '../types/Util';
const Colors = { const Colors = {
@ -29,10 +30,14 @@ interface Props {
i18n: LocalizerType; i18n: LocalizerType;
objectURL: string; objectURL: string;
caption?: string; caption?: string;
isViewOnce: boolean;
onNext?: () => void; onNext?: () => void;
onPrevious?: () => void; onPrevious?: () => void;
onSave?: () => void; onSave?: () => void;
} }
interface State {
videoTime?: number;
}
const CONTROLS_WIDTH = 50; const CONTROLS_WIDTH = 50;
const CONTROLS_SPACING = 10; const CONTROLS_SPACING = 10;
@ -116,6 +121,19 @@ const styles = {
width: 50, width: 50,
height: 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 { interface IconButtonProps {
@ -169,7 +187,7 @@ const Icon = ({
/> />
); );
export class Lightbox extends React.Component<Props> { export class Lightbox extends React.Component<Props, State> {
private readonly containerRef: React.RefObject<HTMLDivElement>; private readonly containerRef: React.RefObject<HTMLDivElement>;
private readonly videoRef: React.RefObject<HTMLVideoElement>; private readonly videoRef: React.RefObject<HTMLVideoElement>;
@ -178,21 +196,39 @@ export class Lightbox extends React.Component<Props> {
this.videoRef = React.createRef(); this.videoRef = React.createRef();
this.containerRef = React.createRef(); this.containerRef = React.createRef();
this.state = {
videoTime: undefined,
};
} }
public componentDidMount() { public componentDidMount() {
const { isViewOnce } = this.props;
const useCapture = true; const useCapture = true;
document.addEventListener('keyup', this.onKeyUp, useCapture); document.addEventListener('keyup', this.onKeyUp, useCapture);
const video = this.getVideo();
if (video && isViewOnce) {
video.addEventListener('timeupdate', this.onTimeUpdate);
}
this.playVideo(); this.playVideo();
} }
public componentWillUnmount() { public componentWillUnmount() {
const { isViewOnce } = this.props;
const useCapture = true; const useCapture = true;
document.removeEventListener('keyup', this.onKeyUp, useCapture); 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) { if (!this.videoRef) {
return; return;
} }
@ -202,11 +238,20 @@ export class Lightbox extends React.Component<Props> {
return; 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 // tslint:disable-next-line no-floating-promises
current.play(); video.play();
} else { } else {
current.pause(); video.pause();
} }
} }
@ -215,11 +260,13 @@ export class Lightbox extends React.Component<Props> {
caption, caption,
contentType, contentType,
i18n, i18n,
isViewOnce,
objectURL, objectURL,
onNext, onNext,
onPrevious, onPrevious,
onSave, onSave,
} = this.props; } = this.props;
const { videoTime } = this.state;
return ( return (
<div <div
@ -232,7 +279,7 @@ export class Lightbox extends React.Component<Props> {
<div style={styles.controlsOffsetPlaceholder} /> <div style={styles.controlsOffsetPlaceholder} />
<div style={styles.objectContainer}> <div style={styles.objectContainer}>
{!is.undefined(contentType) {!is.undefined(contentType)
? this.renderObject({ objectURL, contentType, i18n }) ? this.renderObject({ objectURL, contentType, i18n, isViewOnce })
: null} : null}
{caption ? <div style={styles.caption}>{caption}</div> : null} {caption ? <div style={styles.caption}>{caption}</div> : null}
</div> </div>
@ -247,18 +294,24 @@ export class Lightbox extends React.Component<Props> {
) : null} ) : null}
</div> </div>
</div> </div>
<div style={styles.navigationContainer}> {isViewOnce && is.number(videoTime) ? (
{onPrevious ? ( <div style={styles.navigationContainer}>
<IconButton type="previous" onClick={onPrevious} /> <div style={styles.timestampPill}>{formatDuration(videoTime)}</div>
) : ( </div>
<IconButtonPlaceholder /> ) : (
)} <div style={styles.navigationContainer}>
{onNext ? ( {onPrevious ? (
<IconButton type="next" onClick={onNext} /> <IconButton type="previous" onClick={onPrevious} />
) : ( ) : (
<IconButtonPlaceholder /> <IconButtonPlaceholder />
)} )}
</div> {onNext ? (
<IconButton type="next" onClick={onNext} />
) : (
<IconButtonPlaceholder />
)}
</div>
)}
</div> </div>
); );
} }
@ -267,10 +320,12 @@ export class Lightbox extends React.Component<Props> {
objectURL, objectURL,
contentType, contentType,
i18n, i18n,
isViewOnce,
}: { }: {
objectURL: string; objectURL: string;
contentType: MIME.MIMEType; contentType: MIME.MIMEType;
i18n: LocalizerType; i18n: LocalizerType;
isViewOnce: boolean;
}) => { }) => {
const isImageTypeSupported = GoogleChrome.isImageTypeSupported(contentType); const isImageTypeSupported = GoogleChrome.isImageTypeSupported(contentType);
if (isImageTypeSupported) { if (isImageTypeSupported) {
@ -290,7 +345,8 @@ export class Lightbox extends React.Component<Props> {
<video <video
role="button" role="button"
ref={this.videoRef} ref={this.videoRef}
controls={true} loop={isViewOnce}
controls={!isViewOnce}
style={styles.object} style={styles.object}
key={objectURL} key={objectURL}
> >
@ -326,6 +382,16 @@ export class Lightbox extends React.Component<Props> {
close(); close();
}; };
private readonly onTimeUpdate = () => {
const video = this.getVideo();
if (!video) {
return;
}
this.setState({
videoTime: video.currentTime,
});
};
private readonly onKeyUp = (event: KeyboardEvent) => { private readonly onKeyUp = (event: KeyboardEvent) => {
const { onNext, onPrevious } = this.props; const { onNext, onPrevious } = this.props;
switch (event.key) { switch (event.key) {

View file

@ -66,14 +66,15 @@ export class LightboxGallery extends React.Component<Props, State> {
return ( return (
<Lightbox <Lightbox
close={close}
onPrevious={onPrevious}
onNext={onNext}
onSave={saveCallback}
objectURL={objectURL}
caption={captionCallback} caption={captionCallback}
close={close}
contentType={selectedMedia.contentType} contentType={selectedMedia.contentType}
i18n={i18n} i18n={i18n}
isViewOnce={false}
objectURL={objectURL}
onNext={onNext}
onPrevious={onPrevious}
onSave={saveCallback}
/> />
); );
} }

View file

@ -1149,6 +1149,7 @@ export class Message extends React.PureComponent<Props, State> {
public renderTapToViewText() { public renderTapToViewText() {
const { const {
attachments,
direction, direction,
i18n, i18n,
isTapToViewExpired, isTapToViewExpired,
@ -1157,7 +1158,11 @@ export class Message extends React.PureComponent<Props, State> {
const incomingString = isTapToViewExpired const incomingString = isTapToViewExpired
? i18n('Message--tap-to-view-expired') ? i18n('Message--tap-to-view-expired')
: i18n('Message--tap-to-view--incoming'); : i18n(
`Message--tap-to-view--incoming${
isVideo(attachments) ? '-video' : ''
}`
);
const outgoingString = i18n('Message--tap-to-view--outgoing'); const outgoingString = i18n('Message--tap-to-view--outgoing');
const isDownloadPending = this.isAttachmentPending(); const isDownloadPending = this.isAttachmentPending();

13
ts/util/formatDuration.ts Normal file
View file

@ -0,0 +1,13 @@
import moment from 'moment';
const HOUR = 1000 * 60 * 60;
export function formatDuration(seconds: number) {
const time = moment.utc(seconds * 1000);
if (seconds > HOUR) {
return time.format('H:mm:ss');
}
return time.format('m:ss');
}

View file

@ -7524,7 +7524,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/Lightbox.js", "path": "ts/components/Lightbox.js",
"line": " this.videoRef = react_1.default.createRef();", "line": " this.videoRef = react_1.default.createRef();",
"lineNumber": 185, "lineNumber": 207,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2019-03-09T00:08:44.242Z", "updated": "2019-03-09T00:08:44.242Z",
"reasonDetail": "Used to auto-start playback on videos" "reasonDetail": "Used to auto-start playback on videos"
@ -7533,7 +7533,7 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/Lightbox.tsx", "path": "ts/components/Lightbox.tsx",
"line": " this.videoRef = React.createRef();", "line": " this.videoRef = React.createRef();",
"lineNumber": 179, "lineNumber": 196,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2019-03-09T00:08:44.242Z", "updated": "2019-03-09T00:08:44.242Z",
"reasonDetail": "Used to auto-start playback on videos" "reasonDetail": "Used to auto-start playback on videos"