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"
|
||||
},
|
||||
"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)",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1893,6 +1893,8 @@
|
|||
return {
|
||||
objectURL: getAbsoluteTempPath(path),
|
||||
contentType,
|
||||
onSave: null, // important so download button is omitted
|
||||
isViewOnce: true,
|
||||
};
|
||||
};
|
||||
this.lightboxView = new Whisper.ReactWrapperView({
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,18 +294,24 @@ export class Lightbox extends React.Component<Props> {
|
|||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div style={styles.navigationContainer}>
|
||||
{onPrevious ? (
|
||||
<IconButton type="previous" onClick={onPrevious} />
|
||||
) : (
|
||||
<IconButtonPlaceholder />
|
||||
)}
|
||||
{onNext ? (
|
||||
<IconButton type="next" onClick={onNext} />
|
||||
) : (
|
||||
<IconButtonPlaceholder />
|
||||
)}
|
||||
</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} />
|
||||
) : (
|
||||
<IconButtonPlaceholder />
|
||||
)}
|
||||
{onNext ? (
|
||||
<IconButton type="next" onClick={onNext} />
|
||||
) : (
|
||||
<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) {
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
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",
|
||||
"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"
|
||||
|
|
Loading…
Reference in a new issue