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"
},
"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)",

View file

@ -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()) {
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;
}

View file

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

View file

@ -175,7 +175,8 @@ message DataMessage {
INITIAL = 0;
MESSAGE_TIMERS = 1;
VIEW_ONCE = 2;
CURRENT = 2;
VIEW_ONCE_VIDEO = 3;
CURRENT = 3;
}
optional string body = 1;

View file

@ -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<Props> {
export class Lightbox extends React.Component<Props, State> {
private readonly containerRef: React.RefObject<HTMLDivElement>;
private readonly videoRef: React.RefObject<HTMLVideoElement>;
@ -178,21 +196,39 @@ export class Lightbox extends React.Component<Props> {
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<Props> {
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<Props> {
caption,
contentType,
i18n,
isViewOnce,
objectURL,
onNext,
onPrevious,
onSave,
} = this.props;
const { videoTime } = this.state;
return (
<div
@ -232,7 +279,7 @@ export class Lightbox extends React.Component<Props> {
<div style={styles.controlsOffsetPlaceholder} />
<div style={styles.objectContainer}>
{!is.undefined(contentType)
? this.renderObject({ objectURL, contentType, i18n })
? this.renderObject({ objectURL, contentType, i18n, isViewOnce })
: null}
{caption ? <div style={styles.caption}>{caption}</div> : null}
</div>
@ -247,6 +294,11 @@ export class Lightbox extends React.Component<Props> {
) : null}
</div>
</div>
{isViewOnce && is.number(videoTime) ? (
<div style={styles.navigationContainer}>
<div style={styles.timestampPill}>{formatDuration(videoTime)}</div>
</div>
) : (
<div style={styles.navigationContainer}>
{onPrevious ? (
<IconButton type="previous" onClick={onPrevious} />
@ -259,6 +311,7 @@ export class Lightbox extends React.Component<Props> {
<IconButtonPlaceholder />
)}
</div>
)}
</div>
);
}
@ -267,10 +320,12 @@ export class Lightbox extends React.Component<Props> {
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<Props> {
<video
role="button"
ref={this.videoRef}
controls={true}
loop={isViewOnce}
controls={!isViewOnce}
style={styles.object}
key={objectURL}
>
@ -326,6 +382,16 @@ export class Lightbox extends React.Component<Props> {
close();
};
private readonly onTimeUpdate = () => {
const video = this.getVideo();
if (!video) {
return;
}
this.setState({
videoTime: video.currentTime,
});
};
private readonly onKeyUp = (event: KeyboardEvent) => {
const { onNext, onPrevious } = this.props;
switch (event.key) {

View file

@ -66,14 +66,15 @@ export class LightboxGallery extends React.Component<Props, State> {
return (
<Lightbox
close={close}
onPrevious={onPrevious}
onNext={onNext}
onSave={saveCallback}
objectURL={objectURL}
caption={captionCallback}
close={close}
contentType={selectedMedia.contentType}
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() {
const {
attachments,
direction,
i18n,
isTapToViewExpired,
@ -1157,7 +1158,11 @@ export class Message extends React.PureComponent<Props, State> {
const incomingString = isTapToViewExpired
? 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 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",
"path": "ts/components/Lightbox.js",
"line": " this.videoRef = react_1.default.createRef();",
"lineNumber": 185,
"lineNumber": 207,
"reasonCategory": "usageTrusted",
"updated": "2019-03-09T00:08:44.242Z",
"reasonDetail": "Used to auto-start playback on videos"
@ -7533,7 +7533,7 @@
"rule": "React-createRef",
"path": "ts/components/Lightbox.tsx",
"line": " this.videoRef = React.createRef();",
"lineNumber": 179,
"lineNumber": 196,
"reasonCategory": "usageTrusted",
"updated": "2019-03-09T00:08:44.242Z",
"reasonDetail": "Used to auto-start playback on videos"