wip: out of band inline images are starting to work

This commit is contained in:
or-else
2020-10-14 16:55:54 -07:00
parent f9d622a8d8
commit de1028e141
13 changed files with 293 additions and 145 deletions
+18 -4
View File
@@ -74,7 +74,7 @@ export function imageScaled(file, maxWidth, maxHeight, maxSize, forceSquare, onS
img.onerror = function(err) {
onError("Image format unrecognized");
}
img.onload = function() {
img.onload = async function() {
// Once the image is loaded, the URL is no longer needed.
URL.revokeObjectURL(img.src);
@@ -87,7 +87,7 @@ export function imageScaled(file, maxWidth, maxHeight, maxSize, forceSquare, onS
let canvas = document.createElement('canvas');
canvas.width = dim.dstWidth;
canvas.height = dim.dstHeight;
const ctx = canvas.getContext('2d');
let ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = true;
ctx.drawImage(this, dim.xoffset, dim.yoffset, dim.srcWidth, dim.srcHeight,
0, 0, dim.dstWidth, dim.dstHeight);
@@ -106,6 +106,10 @@ export function imageScaled(file, maxWidth, maxHeight, maxSize, forceSquare, onS
dim.dstHeight = (dim.dstHeight * 0.70710678118) | 0;
canvas.width = dim.dstWidth;
canvas.height = dim.dstHeight;
ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(this, dim.xoffset, dim.yoffset, dim.srcWidth, dim.srcHeight,
0, 0, dim.dstWidth, dim.dstHeight);
blob = await new Promise(resolve => canvas.toBlob(resolve, mime));
}
@@ -196,14 +200,24 @@ export function imageFileToBase64(file, onSuccess, onError) {
reader.readAsDataURL(file);
}
export function fileToBase64(file, onSuccess, onError) {
var reader = new FileReader();
// Convert File to base64 string.
export function fileToBase64(file, onSuccess) {
const reader = new FileReader();
reader.addEventListener('load', function() {
onSuccess(file.type, reader.result.split(',')[1], file.name);
});
reader.readAsDataURL(file);
}
// Convert Blob to base64 string.
export function blobToBase64(blob, onSuccess) {
const reader = new FileReader();
reader.addEventListener('load', function() {
onSuccess(blob.type, reader.result.split(',')[1]);
});
reader.readAsDataURL(blob);
}
// File pasted from the clipboard. It's either an inline image or a file attachment.
// FIXME: handle large files out of band.
export function filePasted(event, onImageSuccess, onAttachmentSuccess, onError) {
+1 -1
View File
@@ -1,3 +1,3 @@
// This is a generated file. Don't edit.
export const PACKAGE_VERSION = "0.16.8";
export const PACKAGE_VERSION = "0.16.9-beta1";
+53 -63
View File
@@ -17,8 +17,8 @@ import SendMessage from '../widgets/send-message.jsx';
import { DEFAULT_P2P_ACCESS_MODE, KEYPRESS_DELAY, MESSAGES_PAGE, MAX_EXTERN_ATTACHMENT_SIZE,
MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE, READ_DELAY } from '../config.js';
import { SUPPORTED_IMAGE_FORMATS, filePasted, fileToBase64, imageFileToBase64,
imageFileScaledToBase64, makeImageUrl } from '../lib/blob-helpers.js';
import { SUPPORTED_IMAGE_FORMATS, blobToBase64, filePasted, fileToBase64, imageFileToBase64,
imageScaled, makeImageUrl } from '../lib/blob-helpers.js';
import HashNavigation from '../lib/navigation.js';
import { bytesToHumanSize, shortDateFormat } from '../lib/strformat.js';
@@ -559,6 +559,7 @@ class MessagesView extends React.Component {
}
handleClosePreview() {
URL.revokeObjectURL(this.state.imagePreview.url);
this.setState({ imagePostview: null, imagePreview: null, docPreview: null });
}
@@ -667,73 +668,62 @@ class MessagesView extends React.Component {
}
// sendImageAttachment sends the image bits inband as Drafty message.
sendImageAttachment(objectUrl, caption, mime, width, height, fname) {
// Upload or scale the image.
// Check if the uploaded file is indeed an image and if it isn't too large.
if (file.size > MAX_INBAND_ATTACHMENT_SIZE || SUPPORTED_IMAGE_FORMATS.indexOf(file.type) < 0) {
// Convert image for size or format.
imageFileScaledToBase64(file, MAX_IMAGE_DIM, MAX_IMAGE_DIM, false,
// Success
(bits, mime, width, height, fname) => {
this.setState({imagePreview: {
url: URL.createObjectURL(file),
filename: fname,
width: width,
height: height,
size: bits.length,
type: mime
}});
},
// Failure
(err) => {
this.props.onError(err, 'err');
});
} else {
// Image can be uploaded as is. No conversion is needed.
imageFileToBase64(file,
// Success
(bits, mime, width, height, fname) => {
this.setState({imagePreview: {
url: URL.createObjectURL(file),
bits: bits,
filename: fname,
width: width,
height: height,
size: bits.length,
type: mime
}});
},
// Failure
(err) => {
this.props.onError(err, 'err');
}
);
sendImageAttachment(caption, blob) {
const mime = this.state.imagePreview.type;
const width = this.state.imagePreview.width;
const height = this.state.imagePreview.height;
const fname = this.state.imagePreview.filename;
// Upload the image if it's too big to be send inband.
if (blob.size > MAX_INBAND_ATTACHMENT_SIZE) {
// Too large to send inband - uploading out of band and sending as a link.
const uploader = this.props.tinode.getLargeFileHelper();
if (!uploader) {
this.props.onError(this.props.intl.formatMessage(messages.cannot_initiate_upload));
return;
}
const uploadCompletionPromise = uploader.upload(blob);
let msg = Drafty.insertImage(null, 0, mime, null, width, height, fname, blob.size, uploadCompletionPromise);
if (caption) {
msg = Drafty.appendLineBreak(msg);
msg = Drafty.append(msg, Drafty.init(caption));
}
// Pass data and the uploader to the TinodeWeb.
this.props.sendMessage(msg, uploadCompletionPromise, uploader);
return;
}
let msg = Drafty.insertImage(null, 0, mime, bits, width, height, fname);
if (caption) {
msg = Drafty.appendLineBreak(msg);
msg = Drafty.append(msg, Drafty.init(caption));
}
this.props.sendMessage(msg);
blobToBase64(blob, (bits) => {
let msg = Drafty.insertImage(null, 0, mime, bits, width, height, fname, blob.size);
if (caption) {
msg = Drafty.appendLineBreak(msg);
msg = Drafty.append(msg, Drafty.init(caption));
}
this.props.sendMessage(msg);
});
}
// handleAttachImage method is called when [Attach image] button is clicked.
handleAttachImage(file) {
let size = file.size;
// If image is too big scale it down before previewing.
if (size > MAX_EXTERN_ATTACHMENT_SIZE) {
}
// Launch image preview screen.
this.setState({imagePreview: {
url: URL.createObjectURL(file),
filename: fname,
width: width, // FIXME: width is not defined.
height: height, // FIXME: height is not defined.
size: size,
type: mime // FIXME: mime type is not defined.
}});
// Get image dimensions and size, optionally scale it down.
imageScaled(file, MAX_IMAGE_DIM, MAX_IMAGE_DIM, MAX_EXTERN_ATTACHMENT_SIZE, false,
// Success
(blob, mime, width, height, fname) => {
this.setState({imagePreview: {
url: URL.createObjectURL(blob),
blob: blob,
filename: fname,
width: width,
height: height,
size: blob.length,
type: mime
}});
},
// Failure
(err) => {
this.props.onError(err, 'err');
}
);
}
render() {
+3
View File
@@ -774,6 +774,9 @@ class TinodeWeb extends React.Component {
// User is sending a message, either plain text or a drafty object, possibly
// with attachments.
// - msg - Drafty message with content
// - promise - Promise to be resolved when the upload is completed
// - uploader - for tracking progress
handleSendMessage(msg, promise, uploader) {
const topic = this.tinode.getTopic(this.state.topicSelected);
+11 -8
View File
@@ -2,8 +2,8 @@ import React from 'react';
import LetterTile from './letter-tile.jsx';
import { AVATAR_SIZE } from '../config.js';
import { imageFileScaledToBase64, makeImageUrl } from '../lib/blob-helpers.js';
import { AVATAR_SIZE, MAX_EXTERN_ATTACHMENT_SIZE } from '../config.js';
import { imageScaled, blobToBase64, makeImageUrl } from '../lib/blob-helpers.js';
export default class AvatarUpload extends React.Component {
constructor(props) {
@@ -23,12 +23,15 @@ export default class AvatarUpload extends React.Component {
}
handleFileUpload(e) {
imageFileScaledToBase64(e.target.files[0], AVATAR_SIZE, AVATAR_SIZE, true,
// Success
(base64bits, mime) => {
var du = makeImageUrl({data: base64bits, type: mime});
this.setState({dataUrl: du});
this.props.onImageChanged(du);
imageScaled(e.target.files[0], AVATAR_SIZE, AVATAR_SIZE, MAX_EXTERN_ATTACHMENT_SIZE, true,
// Image successfully scaled and converted.
(mime, blob) => {
// Convert blob to base64-encoded bits.
blobToBase64(blob, (unused, base64bits) => {
const du = makeImageUrl({data: base64bits, type: mime});
this.setState({dataUrl: du});
this.props.onImageChanged(du);
});
},
// Failure
(err) => {
+6 -1
View File
@@ -6,6 +6,7 @@ import { Drafty } from 'tinode-sdk'
import Attachment from './attachment.jsx';
import LetterTile from './letter-tile.jsx';
import ReceivedMarker from './received-marker.jsx'
import UploadingImage from './uploading-image.jsx'
import { sanitizeImageUrl, sanitizeUrl } from '../lib/utils.js';
export default class ChatMessage extends React.Component {
@@ -167,12 +168,16 @@ import { fitImageSize } from '../lib/blob-helpers.js';
function draftyFormatter(style, data, values, key) {
let el = Drafty.tagName(style);
if (el) {
let attr = Drafty.attrValue(style, data) || {};
const attr = Drafty.attrValue(style, data) || {};
attr.key = key;
switch (style) {
case 'IM':
// Additional processing for images
if (data) {
if (Drafty.isUploading(data)) {
// Use custom element instead of <img>.
el = 'UploadingImage';
}
attr.className = 'inline-image';
let dim = fitImageSize(data.width, data.height,
Math.min(this.props.viewportWidth - REM_SIZE * 4, REM_SIZE * 36), REM_SIZE * 24, false);
+1 -2
View File
@@ -30,8 +30,7 @@ export default class ImagePreview extends React.PureComponent {
handleSendImage(caption) {
this.props.onClose();
this.props.onSendMessage(caption, this.props.content.type,
this.props.content.width, this.props.content.height, this.props.content.filename);
this.props.onSendMessage(caption, this.props.content.blob);
}
render() {
+20
View File
@@ -0,0 +1,20 @@
// Image view with progress bar and a cancel button.
import React from 'react';
import { FormattedMessage } from 'react-intl';
import FileProgress from './file-progress.jsx';
export default class UploadingImage extends React.PureComponent {
constructor(props) {
super(props);
}
render() {
return (
<div className="inline-image">
<img src={this.props.src} />
<FileProgress progress={this.props.progress} onCancel={this.props.onCancelUpload} />
</div>
);
}
};