video uploading + preview in bubbles works

This commit is contained in:
or-else
2022-12-13 17:37:04 -08:00
parent 489a05663a
commit bccf0d3b6b
15 changed files with 235 additions and 156 deletions
+3
View File
@@ -76,6 +76,9 @@ export const MAX_IMAGE_DIM = 1024;
// Linear dimensions of image preview: shrink image under this size for previews.
export const IMAGE_PREVIEW_DIM = 64;
// Linear dimensions of video screencapture: shrink image under this size for previews.
export const VIDEO_PREVIEW_DIM = 96;
// Linear dimensions of image thumbnail: shrink image under this size for thumbnails in reply quote previews.
export const IMAGE_THUMBNAIL_DIM = 36;
+1 -1
View File
@@ -172,7 +172,7 @@ export function imageCrop(mime, objURL, left, top, width, height, scale) {
export function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = (evt) => {
reader.onerror = _ => {
reject(reader.error);
};
reader.onload = _ => {
+10 -14
View File
@@ -40,11 +40,12 @@ const messages = defineMessages({
// The main Drafty formatter: converts Drafty elements into React classes. 'this' is set by the caller.
// 'this' must contain:
// viewportWidth: this.props.viewportWidth;
// authorizeURL: this.props.tinode.authorizeURL
// onImagePreview: this.handleImagePreview
// onFormButtonClick: this.handleFormButtonClick
// onQuoteClick: this.handleQuoteClick (optional)
// viewportWidth:
// authorizeURL:
// onImagePreview:
// onVideoPreview:
// onFormButtonClick:
// onQuoteClick:
export function fullFormatter(style, data, values, key, stack) {
if (stack.includes('QQ')) {
return quoteFormatter.call(this, style, data, values, key);
@@ -192,7 +193,7 @@ function handleImageData(el, data, attr) {
}
attr.loading = 'lazy';
} else {
attr.src = 'img/broken_image.png';
attr.src = null;
}
} else {
// Use custom element instead of <img>.
@@ -226,18 +227,13 @@ function handleVideoData(el, data, attr) {
minHeight: dim.dstHeight + 'px'
};
if (!Drafty.isProcessing(data)) {
/*
attr.src = this.authorizeURL(sanitizeUrlForMime(attr.src, 'video'));
attr.alt = data.name;
if (attr.src) {
attr.onClick = this.onImagePreview;
attr.className += ' image-clickable';
if (data.ref || data.val) {
attr.onClick = this.onVideoPreview;
attr.loading = 'lazy';
attr.controls = 'controls';
} else {
attr.src = 'img/broken_video.png';
attr.src = null;
}
*/
el = InlineVideo;
} else {
// Use custom element instead of <img>.
+7 -3
View File
@@ -34,13 +34,17 @@ export function relativeDateFormat(then, locale) {
// Convert seconds to [hours:]minutes:seconds, i.e. 156 sec -> 2:36, 3756 sec -> 1:02:36.
// If <code>fixedMins</code> is true, then minutes are represented by at least two digits.
export function secondsToTime(seconds, fixedMin) {
let min = Math.floor(seconds / 60) | 0;
let hours = Math.floor(min / 60) | 0;
if (typeof seconds != 'number') {
return '';
}
let min = (Math.floor(seconds / 60) | 0) % 60;
let hours = Math.floor(seconds / 36000) | 0;
if (fixedMin || hours > 0) {
min = min < 10 ? `0${min}` : min;
}
let sec = (seconds % 60) | 0;
let sec = (seconds | 0) % 60;
sec = sec < 10 ? `0${sec}` : sec;
if (hours == 0) {
return `${min}:${sec}`;
+84 -62
View File
@@ -22,7 +22,7 @@ import VideoPreview from '../widgets/video-preview.jsx';
import { DEFAULT_P2P_ACCESS_MODE, EDIT_PREVIEW_LENGTH, IMAGE_PREVIEW_DIM, KEYPRESS_DELAY,
MESSAGES_PAGE, MAX_EXTERN_ATTACHMENT_SIZE, MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE,
READ_DELAY, QUOTED_REPLY_LENGTH } from '../config.js';
READ_DELAY, QUOTED_REPLY_LENGTH, VIDEO_PREVIEW_DIM } from '../config.js';
import { CALL_STATE_OUTGOING_INITATED, CALL_STATE_IN_PROGRESS } from '../constants.js';
import { blobToBase64, fileToBase64, imageScaled, makeImageUrl } from '../lib/blob-helpers.js';
import HashNavigation from '../lib/navigation.js';
@@ -828,7 +828,7 @@ class MessagesView extends React.Component {
// Small enough to send inband.
fileToBase64(file)
.then(b64 => this.sendMessage(Drafty.attachFile(null, {mime: b64.mime, data: b64.bits, filename: b64.name})))
.catch(err => this.props.onError(err));
.catch(err => this.props.onError(err.message, 'err'));
}
}
@@ -885,32 +885,34 @@ class MessagesView extends React.Component {
// Convert tiny image into base64 for serialization and previewing.
.then(scaled => blobToBase64(scaled.blob))
.then(b64 => {
let msg = Drafty.insertImage(null, 0, {
mime: mime,
_tempPreview: b64.bits, // This preview will not be serialized.
width: width,
height: height,
filename: fname,
size: blob.size,
urlPromise: uploadCompletionPromise
});
if (caption) {
msg = Drafty.appendLineBreak(msg);
msg = Drafty.append(msg, Drafty.parse(caption));
}
// Pass data and the uploader to the TinodeWeb.
this.sendMessage(msg, uploadCompletionPromise, uploader);
let msg = Drafty.insertImage(null, 0, {
mime: mime,
_tempPreview: b64.bits, // This preview will not be serialized.
bits: b64.bits, // Image thumbnail.
width: width,
height: height,
filename: fname,
size: blob.size,
urlPromise: uploadCompletionPromise
});
if (caption) {
msg = Drafty.appendLineBreak(msg);
msg = Drafty.append(msg, Drafty.parse(caption));
}
// Pass data and the uploader to the TinodeWeb.
this.sendMessage(msg, uploadCompletionPromise, uploader);
})
.catch(err => this.props.onError(err, 'err'));
return;
}
// Send the image inband if it's not too big.
// Send the image inband if it's not too big. The image has been scaled already
// in image preview.
blobToBase64(blob)
.then(b64 => {
let msg = Drafty.insertImage(null, 0, {
mime: b64.mime,
preview: b64.bits, // Serializable preview
bits: b64.bits,
width: width,
height: height,
filename: fname,
@@ -926,68 +928,88 @@ class MessagesView extends React.Component {
// sendVideoAttachment sends the video bits as Drafty message.
sendVideoAttachment(caption, videoBlob, previewBlob, params) {
const mime = params.mime;
const width = params.width;
const height = params.height;
const fname = params.name;
const duration = params.duration;
// Server-provided limit reduced for base64 encoding and overhead.
const maxInbandAttachmentSize = (this.props.tinode.getServerParam('maxMessageSize',
MAX_INBAND_ATTACHMENT_SIZE) * 0.75 - 1024) | 0;
if (videoBlob.size > maxInbandAttachmentSize) {
// Too large to send inband - uploading out of band and sending as a link.
const uploader = this.props.tinode.getLargeFileHelper();
const uploads = [];
let uploader;
if ((videoBlob.size + previewBlob.size) > maxInbandAttachmentSize) {
// One or both are too large to send inband. Uploading out of band and sending as a link.
uploader = this.props.tinode.getLargeFileHelper();
if (!uploader) {
this.props.onError(this.props.intl.formatMessage(messages.cannot_initiate_upload));
return;
}
const uploadCompletionPromise = uploader.upload(videoBlob);
// Make small preview to show while uploading.
imageScaled(previewBlob, IMAGE_PREVIEW_DIM, IMAGE_PREVIEW_DIM, -1, false)
// Convert tiny image into base64 for serialization and previewing.
.then(scaled => blobToBase64(scaled.blob))
.then(b64 => {
let msg = Drafty.insertVideo(null, 0, {
mime: mime,
_tempPreview: b64.bits, // This preview will not be serialized.
width: width,
height: height,
duration: duration,
filename: fname,
size: videoBlob.size,
urlPromise: uploadCompletionPromise
});
if (caption) {
msg = Drafty.appendLineBreak(msg);
msg = Drafty.append(msg, Drafty.parse(caption));
}
// Pass data and the uploader to the TinodeWeb.
this.sendMessage(msg, uploadCompletionPromise, uploader);
})
.catch(err => /* this.props.onError(err, 'err') */ console.log(err));
uploads[0] = videoBlob.size > maxInbandAttachmentSize * 0.675 ? uploader.upload(videoBlob) : null;
uploads[1] = previewBlob.size > maxInbandAttachmentSize * 0.275 ? uploader.upload(previewBlob) : null;
}
if (uploads.length == 0) {
// Both video and preview are small enough to send inband.
Promise.all(blobToBase64(videoBlob), blobToBase64(previewBlob))
.then(b64s => {
const [v64, i64] = b64s;
let msg = Drafty.insertVideo(null, 0, {
mime: v64.mime,
bits: v64.bits,
preview: i64.bits,
premime: i64.mime,
width: width,
height: height,
duration: params.duration,
filename: params.name,
size: videoBlob.size
});
if (caption) {
msg = Drafty.appendLineBreak(msg);
msg = Drafty.append(msg, Drafty.parse(caption));
}
this.sendMessage(msg);
});
return;
}
// Send the video inband if it's small enough.
blobToBase64(videoBlob)
.then(b64 => {
const uploadCompletionPromise = Promise.all(uploads);
const b64conv = [];
// Small video converted to base64.
b64conv[0] = uploads[0] ? null : blobToBase64(videoBlob);
// Full-size preview fits inline.
b64conv[1] = uploads[1] ? null : imageScaled(previewBlob, MAX_IMAGE_DIM, MAX_IMAGE_DIM, -1, false)
.then(scaled => blobToBase64(scaled.blob));
// Small preview to show while uploading.
b64conv[2] = imageScaled(previewBlob, VIDEO_PREVIEW_DIM, VIDEO_PREVIEW_DIM, -1, false)
.then(scaled => blobToBase64(scaled.blob));
Promise.all(b64conv)
// Convert tiny image into base64 for serialization and previewing.
.then(b64s => {
const [video, img, preview] = b64s;
let msg = Drafty.insertVideo(null, 0, {
mime: b64.mime,
preview: b64.bits, // Serializable preview
mime: params.mime,
bits: video ? video.bits : null,
_tempPreview: preview.bits,
preview: img ? img.bits : preview.bits,
premime: img ? img.mime : preview.mime,
width: width,
height: height,
duration: duration,
filename: fname,
size: blob.size
duration: params.duration,
filename: params.name,
size: videoBlob.size,
urlPromise: uploadCompletionPromise
});
if (caption) {
msg = Drafty.appendLineBreak(msg);
msg = Drafty.append(msg, Drafty.parse(caption));
}
this.sendMessage(msg);
});
// Pass data and the uploader to the TinodeWeb.
this.sendMessage(msg, uploadCompletionPromise, uploader);
})
.catch(err => this.props.onError(err.message, 'err'));
}
// handleAttachImageOrVideo method is called when [Attach image or video] button is clicked: launch image or video preview.
@@ -1018,7 +1040,7 @@ class MessagesView extends React.Component {
mime: scaled.mime
}});
}).catch(err => {
this.props.onError(err, 'err');
this.props.onError(err.message, 'err');
});
}
@@ -1052,15 +1074,15 @@ class MessagesView extends React.Component {
.then(b64 => {
this.sendMessage(Drafty.appendAudio(null, {
mime: b64.mime,
bits: b64.bits,
size: blob.size,
data: b64.bits,
duration: duration,
preview: preview,
}))
})
}
})
.catch(err => {this.props.onError(err)});;
.catch(err => {this.props.onError(err.message, 'err')});;
}
handleCancelUpload(seq, uploader) {
+2 -1
View File
@@ -35,6 +35,7 @@ class BaseChatMessage extends React.PureComponent {
authorizeURL: props.tinode.authorizeURL.bind(props.tinode),
onImagePreview: this.handleImagePreview,
onVideoPreview: this.handleImagePreview,
onFormButtonClick: this.handleFormButtonClick,
onQuoteClick: this.handleQuoteClick
};
@@ -81,7 +82,7 @@ class BaseChatMessage extends React.PureComponent {
if (!this.props.response) {
let immutable = false;
Drafty.entities(this.props.content, (_0, _1, tp) => {
immutable = ['AU', 'EX', 'FM', 'IM', 'VC'].includes(tp);
immutable = ['AU', 'EX', 'FM', 'IM', 'QQ', 'VC', 'VD'].includes(tp);
return immutable;
});
if (!immutable) {
+10 -5
View File
@@ -3,19 +3,24 @@
import React from 'react';
import { secondsToTime } from '../lib/strformat.js'
export default class InlineVideo extends React.PureComponent {
constructor(props) {
super(props);
}
render() {
const duration = secondsToTime(this.props['data-duration'] / 1000);
return (
<div className="inline-image video">
<div className="inline-video">
{React.createElement('img', this.props)}
<div className="play-control">
<i class="material-icons white bigger">play_arrow</i>
</div>
<div className="duration">1:12:45</div>
{this.props.onClick ?
<div className="play-control" onClick={this.props.onClick}>
<i className="material-icons white bigger">play_arrow</i>
</div>
: null}
{duration ? <div className="duration">{duration}</div> : null}
</div>
);
}