Support for receiving View Once Video
This commit is contained in:
parent
9229824c24
commit
5c00b89600
9 changed files with 156 additions and 48 deletions
|
@ -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)",
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
13
ts/util/formatDuration.ts
Normal 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');
|
||||||
|
}
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue