refactored blob helpers to use promises instead of callbacks

This commit is contained in:
or-else
2021-09-17 12:38:35 -07:00
parent 760289c6cf
commit 8de413c5b5
9 changed files with 516 additions and 402 deletions
+111 -97
View File
@@ -77,132 +77,146 @@ export function fileNameForMime(fname, mime) {
} }
// Scale uploaded image to fit under certain dimensions and byte size, optionally constraining to a square. // Scale uploaded image to fit under certain dimensions and byte size, optionally constraining to a square.
// On success calls onSuccess callback with the scaled image as Blob. // Returns a promise which is resolven on success or rejected on failure.
export function imageScaled(fileOrBlob, maxWidth, maxHeight, maxSize, forceSquare, onSuccess, onError) { export function imageScaled(fileOrBlob, maxWidth, maxHeight, maxSize, forceSquare) {
const img = new Image(); return new Promise((resolve, reject) => {
img.crossOrigin = 'Anonymous'; const img = new Image();
img.onerror = function(err) { img.crossOrigin = 'Anonymous';
onError("Image format unrecognized"); img.onerror = function(err) {
} reject(new Error("Image format unrecognized"));
img.onload = async function() {
// Once the image is loaded, the URL is no longer needed.
URL.revokeObjectURL(img.src);
// Calculate the desired image dimensions.
const dim = fitImageSize(this.width, this.height, maxWidth, maxHeight, forceSquare);
if (!dim) {
onError("Invalid image");
return;
} }
let canvas = document.createElement('canvas'); img.onload = async function() {
canvas.width = dim.dstWidth; // Once the image is loaded, the URL is no longer needed.
canvas.height = dim.dstHeight; URL.revokeObjectURL(img.src);
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);
const mime = SUPPORTED_IMAGE_FORMATS.includes(fileOrBlob.type) ? fileOrBlob.type : 'image/jpeg'; // Calculate the desired image dimensions.
// Generate blob to check size of the image. const dim = fitImageSize(this.width, this.height, maxWidth, maxHeight, forceSquare);
let blob = await new Promise(resolve => canvas.toBlob(resolve, mime)); if (!dim) {
if (!blob) { reject(new Error("Invalid image"));
onError("Unsupported image format"); return;
return; }
} let canvas = document.createElement('canvas');
// Ensure the image is not too large. Shrink the image keeping the aspect ratio.
// Do nothing if maxsize is <= 0.
while (maxSize > 0 && blob.length > maxSize) {
dim.dstWidth = (dim.dstWidth * 0.70710678118) | 0;
dim.dstHeight = (dim.dstHeight * 0.70710678118) | 0;
canvas.width = dim.dstWidth; canvas.width = dim.dstWidth;
canvas.height = dim.dstHeight; canvas.height = dim.dstHeight;
ctx = canvas.getContext('2d'); let ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.imageSmoothingEnabled = true;
ctx.drawImage(this, dim.xoffset, dim.yoffset, dim.srcWidth, dim.srcHeight, ctx.drawImage(this, dim.xoffset, dim.yoffset, dim.srcWidth, dim.srcHeight,
0, 0, dim.dstWidth, dim.dstHeight); 0, 0, dim.dstWidth, dim.dstHeight);
blob = await new Promise(resolve => canvas.toBlob(resolve, mime));
}
canvas = null; const mime = SUPPORTED_IMAGE_FORMATS.includes(fileOrBlob.type) ? fileOrBlob.type : 'image/jpeg';
onSuccess(mime, blob, dim.dstWidth, dim.dstHeight, fileNameForMime(fileOrBlob.name, mime)); // Generate blob to check size of the image.
}; let blob = await new Promise(resolve => canvas.toBlob(resolve, mime));
img.src = URL.createObjectURL(fileOrBlob); if (!blob) {
reject(new Error("Unsupported image format"));
return;
}
// Ensure the image is not too large. Shrink the image keeping the aspect ratio.
// Do nothing if maxsize is <= 0.
while (maxSize > 0 && blob.length > maxSize) {
dim.dstWidth = (dim.dstWidth * 0.70710678118) | 0;
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));
}
canvas = null;
resolve({mime: mime, blob: blob, width: dim.dstWidth, height: dim.dstHeight, name: fileNameForMime(fileOrBlob.name, mime)});
};
img.src = URL.createObjectURL(fileOrBlob);
});
} }
// Scale and crop image according to specified dimensions. // Scale and crop image according to specified dimensions.
// The coordinates are in unscaled image pixels, i.e. cut the rectangle first then scale it. // The coordinates are in unscaled image pixels, i.e. cut the rectangle first then scale it.
export function imageCrop(mime, objURL, left, top, width, height, scale, onSuccess, onError) { // returns a promise.
const img = new Image(); export function imageCrop(mime, objURL, left, top, width, height, scale) {
img.crossOrigin = 'Anonymous'; return new Promise((resolve, reject) => {
img.onerror = function(err) { const img = new Image();
onError("Image format unrecognized"); img.crossOrigin = 'Anonymous';
} img.onerror = (err) => {
img.onload = async function() { reject(new Error("Image format unrecognized"));
// Once the image is loaded, the URL is no longer needed. };
URL.revokeObjectURL(img.src); img.onload = () => {
// Once the image is loaded, the URL is no longer needed.
URL.revokeObjectURL(img.src);
let canvas = document.createElement('canvas'); let canvas = document.createElement('canvas');
canvas.width = width * scale; canvas.width = width * scale;
canvas.height = height * scale; canvas.height = height * scale;
let ctx = canvas.getContext('2d'); let ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = true; ctx.imageSmoothingEnabled = true;
ctx.drawImage(this, left, top, width, height, 0, 0, canvas.width, canvas.height); ctx.drawImage(this, left, top, width, height, 0, 0, canvas.width, canvas.height);
mime = SUPPORTED_IMAGE_FORMATS.includes(mime) ? mime : 'image/jpeg'; mime = SUPPORTED_IMAGE_FORMATS.includes(mime) ? mime : 'image/jpeg';
// Generate blob to check size of the image. // Generate blob to check size of the image.
let blob = await new Promise(resolve => canvas.toBlob(resolve, mime)); canvas.toBlob((blob) => {
if (!blob) { // Allow GC.
onError("Unsupported image format"); canvas = null;
return; if (blob) {
} resolve({mime: mime, blob: blob, width: width, height: height});
// Allow GC. } else {
canvas = null; reject(new Error("Unsupported image format"));
onSuccess(mime, blob, width, height); }
}; }, mime);
img.src = objURL; };
img.src = objURL;
});
} }
// Convert File to base64 string. // Convert file to base64 string.
export function fileToBase64(file, onSuccess) { export function fileToBase64(file) {
const reader = new FileReader(); return new Promise((resolve, reject) => {
reader.addEventListener('load', function() { const reader = new FileReader();
onSuccess(file.type, reader.result.split(',')[1], file.name); reader.onerror = (evt) => {
reject(reader.error);
};
reader.onload = () => {
resolve({mime: file.type, bits: reader.result.split(',')[1], name: file.name});
};
reader.readAsDataURL(file);
}); });
reader.readAsDataURL(file);
} }
// Convert Blob to base64 string. // Convert Blob to base64 string. Returns a promise resolved with the base64 string and mime.
export function blobToBase64(blob, onSuccess) { export function blobToBase64(blob) {
const reader = new FileReader(); return new Promise((resolve, reject) => {
reader.addEventListener('load', function() { const reader = new FileReader();
onSuccess(blob.type, reader.result.split(',')[1]); reader.onerror = (evt) => {
reject(reader.error);
};
reader.onload = () => {
resolve({mime: blob.type, bits: reader.result.split(',')[1]});
};
reader.readAsDataURL(blob);
}); });
reader.readAsDataURL(blob);
} }
// File pasted from the clipboard. It's either an inline image or a file attachment. // 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) { export function filePasted(event, onImageSuccess, onAttachmentSuccess, onError) {
var items = (event.clipboardData || event.originalEvent.clipboardData || {}).items; const items = (event.clipboardData || event.originalEvent.clipboardData || {}).items;
for (var i in items) { if (!Array.isArray(items)) {
var item = items[i]; return false;
}
for (let i in items) {
const item = items[i];
if (item.kind === 'file') { if (item.kind === 'file') {
var file = item.getAsFile(); const file = item.getAsFile();
if (!file) { if (!file) {
console.error("Failed to get file object from pasted file item", item.kind, item.type); console.error("Failed to get file object from pasted file item", item.kind, item.type);
onError("Failed to get file object from pasted file item");
continue; continue;
} }
if (file.type && file.type.split('/')[0] == 'image') { if (file.type && file.type.split('/')[0] == 'image') {
// Handle inline image onImageSuccess(file);
if (file.size > MAX_INBAND_ATTACHMENT_SIZE || SUPPORTED_IMAGE_FORMATS.indexOf(file.type) < 0) {
imageFileScaledToBase64(file, MAX_IMAGE_DIM, MAX_IMAGE_DIM, false, onImageSuccess, onError);
} else {
imageFileToBase64(file, onImageSuccess, onError);
}
} else { } else {
// Handle file attachment onAttachmentSuccess(file);
fileToBase64(file, onAttachmentSuccess, onError)
} }
// Indicate that the pasted data contains a file. // Indicate that the pasted data contains a file.
return true; return true;
@@ -263,7 +277,7 @@ export function base64ToBlob(str, mime) {
return new Blob([buf], { type: mime }); return new Blob([buf], { type: mime });
} catch(err) { } catch(err) {
console.log("Failed to convert base64 to blob: ", err); console.error("Failed to convert base64 to blob: ", err);
} }
return null; return null;
+46 -1
View File
@@ -28,6 +28,7 @@ const messages = defineMessages({
}, },
}); });
// Size the already scaled image.
function handleImageData(el, data, attr) { function handleImageData(el, data, attr) {
if (!data) { if (!data) {
attr.src = 'img/broken_image.png'; attr.src = 'img/broken_image.png';
@@ -74,6 +75,50 @@ function handleImageData(el, data, attr) {
return el; return el;
} }
function quotedImage(data) {
let promise;
// Get the blob from the image data.
if (data.val) {
const blob = base64ToBlob(data.val, data.mime);
promise = blob ? Promise.resolve(blob) : Prmise.reject(new Error("Invalid image"));
} else {
promise = fetch(this.authorizeURL(sanitizeImageUrl(data.ref))).then(evt => {
if (evt.ok) {
return evt.blob();
} else {
throw new Error(`Image fetch unsuccessful: ${evt.status} ${evt.statusText}`);
}
});
}
// Scale the blob.
return promise
.then(blob => {
return imageScaled(blob, IMAGE_THUMBNAIL_DIM, IMAGE_THUMBNAIL_DIM, -1, false)
}).then(scaled => {
data.mime = scaled.mime;
data.size = scaled.blob.size;
data.width = scaled.width;
data.height = scaled.height;
delete data.ref;
// Keeping the original file name, if provided: ex.data.name;
return blobToBase64(scaled.blob);
}).then(b64 => {
data.val = b64.bits;
return data;
}).catch(err => {
delete data.val;
delete data.name;
data.width = IMAGE_THUMBNAIL_DIM;
data.height = IMAGE_THUMBNAIL_DIM;
data.maxWidth = IMAGE_THUMBNAIL_DIM;
data.maxHeight = IMAGE_THUMBNAIL_DIM;
// Rethrow.
throw err;
});
}
function shortenFileName(filename) { function shortenFileName(filename) {
return filename.length > 16 ? filename.slice(0, 7) + '…' + filename.slice(-7) : filename; return filename.length > 16 ? filename.slice(0, 7) + '…' + filename.slice(-7) : filename;
} }
@@ -229,7 +274,7 @@ export function previewFormatter(style, data, values, key) {
return React.createElement(el, attr, values); return React.createElement(el, attr, values);
}; };
// Converts Drafty object into a quoted reply. 'this' is set by the caller. // Displays a portion of Drafty within 'QQ' quotes. 'this' is set by the caller.
// 'this' must contain: // 'this' must contain:
// formatMessage: this.props.intl.formatMessage // formatMessage: this.props.intl.formatMessage
// messages: formatjs messages defined with defineMessages. // messages: formatjs messages defined with defineMessages.
+80 -102
View File
@@ -704,12 +704,9 @@ class MessagesView extends React.Component {
this.sendMessage(msg, uploadCompletionPromise, uploader); this.sendMessage(msg, uploadCompletionPromise, uploader);
} else { } else {
// Small enough to send inband. // Small enough to send inband.
fileToBase64(file, fileToBase64(file)
(mime, bits, fname) => { .then(b64 => this.sendMessage(Drafty.attachFile(null, {mime: b64.mime, data: b64.bits, filename: b64.name})))
this.sendMessage(Drafty.attachFile(null, {mime: mime, data: bits, filename: fname})); .catch(err => this.props.onError(err));
},
this.props.onError
);
} }
} }
@@ -752,13 +749,13 @@ class MessagesView extends React.Component {
const uploadCompletionPromise = uploader.upload(blob); const uploadCompletionPromise = uploader.upload(blob);
// Make small preview to show while uploading. // Make small preview to show while uploading.
imageScaled(blob, IMAGE_PREVIEW_DIM, IMAGE_PREVIEW_DIM, -1, false, imageScaled(blob, IMAGE_PREVIEW_DIM, IMAGE_PREVIEW_DIM, -1, false)
(tinyMine, tinyBlob) => { // Convert tiny image into base64 for serialization and previewing.
// Convert tiny image into base64 for serialization and previewing. .then(scaled => blobToBase64(scaled.blob))
blobToBase64(tinyBlob, (blobMime, tinyBits64) => { .then(b64 => {
let msg = Drafty.insertImage(null, 0, { let msg = Drafty.insertImage(null, 0, {
mime: mime, mime: mime,
_tempPreview: tinyBits64, // This preview will not be serialized. _tempPreview: b64.bits, // This preview will not be serialized.
width: width, width: width,
height: height, height: height,
filename: fname, filename: fname,
@@ -771,32 +768,29 @@ class MessagesView extends React.Component {
} }
// Pass data and the uploader to the TinodeWeb. // Pass data and the uploader to the TinodeWeb.
this.sendMessage(msg, uploadCompletionPromise, uploader); this.sendMessage(msg, uploadCompletionPromise, uploader);
} }).catch((err) => {
)},
// Failure
(err) => {
this.props.onError(err, 'err'); this.props.onError(err, 'err');
} });
); return;
return;
} }
// Upload the image if it's too big to be send inband. // Upload the image if it's too big to be send inband.
blobToBase64(blob, (blobMime, bits64) => { blobToBase64(blob)
let msg = Drafty.insertImage(null, 0, { .then(b64 => {
mime: blobMime, let msg = Drafty.insertImage(null, 0, {
preview: bits64, // Serializable preview mime: b64.mime,
width: width, preview: b64.bits, // Serializable preview
height: height, width: width,
filename: fname, height: height,
size: blob.size filename: fname,
size: blob.size
});
if (caption) {
msg = Drafty.appendLineBreak(msg);
msg = Drafty.append(msg, Drafty.parse(caption));
}
this.sendMessage(msg);
}); });
if (caption) {
msg = Drafty.appendLineBreak(msg);
msg = Drafty.append(msg, Drafty.parse(caption));
}
this.sendMessage(msg);
});
} }
// handleAttachImage method is called when [Attach image] button is clicked. // handleAttachImage method is called when [Attach image] button is clicked.
@@ -804,24 +798,20 @@ class MessagesView extends React.Component {
const maxExternAttachmentSize = this.props.tinode.getServerLimit('maxFileUploadSize', MAX_EXTERN_ATTACHMENT_SIZE); const maxExternAttachmentSize = this.props.tinode.getServerLimit('maxFileUploadSize', MAX_EXTERN_ATTACHMENT_SIZE);
// Get image dimensions and size, optionally scale it down. // Get image dimensions and size, optionally scale it down.
imageScaled(file, MAX_IMAGE_DIM, MAX_IMAGE_DIM, maxExternAttachmentSize, false, imageScaled(file, MAX_IMAGE_DIM, MAX_IMAGE_DIM, maxExternAttachmentSize, false)
// Success .then(scaled => {
(mime, blob, width, height, fname) => {
this.setState({imagePreview: { this.setState({imagePreview: {
url: URL.createObjectURL(blob), url: URL.createObjectURL(scaled.blob),
blob: blob, blob: scaled.blob,
filename: fname, filename: scaled.fname,
width: width, width: scaled.width,
height: height, height: scaled.height,
size: blob.size, size: scaled.blob.size,
type: mime type: scaled.mime
}}); }});
}, }).catch(err => {
// Failure
(err) => {
this.props.onError(err, 'err'); this.props.onError(err, 'err');
} });
);
} }
handleCancelUpload(seq, uploader) { handleCancelUpload(seq, uploader) {
@@ -879,64 +869,52 @@ class MessagesView extends React.Component {
} }
}); });
// Turn all images into thumbnails. const quotedImage = (data) => {
const promises = images.map((ex) => { let promise;
return new Promise((resolve, reject) => { // Get the blob from the image data.
const handleFailure = () => { if (data.val) {
delete ex.data.val; const blob = base64ToBlob(data.val, data.mime);
delete ex.data.name; promise = blob ? Promise.resolve(blob) : Prmise.reject(new Error("Invalid image"));
ex.data.width = IMAGE_THUMBNAIL_DIM; } else {
ex.data.height = IMAGE_THUMBNAIL_DIM; promise = fetch(this.props.tinode.authorizeURL(sanitizeImageUrl(data.ref))).then(evt => {
ex.data.maxWidth = IMAGE_THUMBNAIL_DIM; if (evt.ok) {
ex.data.maxHeight = IMAGE_THUMBNAIL_DIM; return evt.blob();
};
const scale = (origBlob) => {
imageScaled(origBlob, IMAGE_THUMBNAIL_DIM, IMAGE_THUMBNAIL_DIM, -1, false,
// Success
(mime, blob, width, height, unused) => {
ex.data.mime = mime;
ex.data.size = blob.size;
ex.data.width = width;
ex.data.height = height;
delete ex.data.ref;
// Keeping the original file name, if provided: ex.data.name;
blobToBase64(blob, (blobMime, tinyBits64) => {
ex.data.val = tinyBits64;
resolve(true);
});
},
// Failure
(err) => {
handleFailure();
reject(`Could not scale image: ${err}`);
});
}
if (ex.data.val) {
const b = base64ToBlob(ex.data.val, ex.data.mime);
if (b) {
scale(b);
} else { } else {
handleFailure(); throw new Error(`Image fetch unsuccessful: ${evt.status} ${evt.statusText}`);
} }
} else { });
fetch(this.props.tinode.authorizeURL(sanitizeImageUrl(ex.data.ref))) }
.then(e => {
if (e.ok) {
return e.blob();
} else {
handleFailure();
reject(`Image fetch unsuccessful: ${e.status} ${e.statusText}`);
}
})
.then((b) => scale(b))
.catch((err) => reject(`Error fetching image data: ${err}`));
return;
}
});
});
Promise.all(promises) // Scale the blob.
return promise
.then(blob => {
return imageScaled(blob, IMAGE_THUMBNAIL_DIM, IMAGE_THUMBNAIL_DIM, -1, false)
}).then(scaled => {
data.mime = scaled.mime;
data.size = scaled.blob.size;
data.width = scaled.width;
data.height = scaled.height;
delete data.ref;
// Keeping the original file name, if provided: ex.data.name;
return blobToBase64(scaled.blob);
}).then(b64 => {
data.val = b64.bits;
return data;
}).catch(err => {
delete data.val;
delete data.name;
data.width = IMAGE_THUMBNAIL_DIM;
data.height = IMAGE_THUMBNAIL_DIM;
data.maxWidth = IMAGE_THUMBNAIL_DIM;
data.maxHeight = IMAGE_THUMBNAIL_DIM;
// Rethrow.
throw err;
});
}
// Turn all images into thumbnails.
Promise.all(images.map(ex => quotedImage(ex.data)))
.catch((err) => { .catch((err) => {
this.props.onError(err, 'err'); this.props.onError(err, 'err');
}) })
+7 -9
View File
@@ -34,16 +34,14 @@ export default class AvatarCrop extends React.PureComponent {
handleSubmit() { handleSubmit() {
imageCrop(this.props.mime, this.props.avatar, imageCrop(this.props.mime, this.props.avatar,
this.state.left, this.state.top, this.state.left, this.state.top, this.state.width, this.state.height,
this.state.width, this.state.height, this.state.scale)
this.state.scale, .then(img => {
(mime, blob, width, height) => { this.props.onSubmit(img.mime, img.blob, img.width, img.height);
this.props.onSubmit(mime, blob, width, height); })
}, .catch(err => {
(err) => {
this.props.onError(err); this.props.onError(err);
} });
);
} }
render() { render() {
+13 -11
View File
@@ -108,7 +108,8 @@ export default class TopicDescEdit extends React.Component {
// Utility method for converting cropped avatar blob to bytes for sending inband or // Utility method for converting cropped avatar blob to bytes for sending inband or
// for uploading it to the server out of band. // for uploading it to the server out of band.
uploadAvatar(mime, blob, width, height) { uploadAvatar(mime, blob, width, height) {
const readyToUpload = (mime, blob, width, height) => { const readyToUpload = (image) => {
let {mime, blob, width, height} = image;
if (blob.size > MAX_AVATAR_BYTES) { if (blob.size > MAX_AVATAR_BYTES) {
// Too large to send inband - uploading out of band and sending as a link. // Too large to send inband - uploading out of band and sending as a link.
const uploader = this.props.tinode.getLargeFileHelper(); const uploader = this.props.tinode.getLargeFileHelper();
@@ -127,24 +128,25 @@ export default class TopicDescEdit extends React.Component {
} else { } else {
this.setState({uploading: true}); this.setState({uploading: true});
// Convert blob to base64-encoded bits. // Convert blob to base64-encoded bits.
blobToBase64(blob, (unused, base64bits) => { blobToBase64(blob)
const du = makeImageUrl({data: base64bits, type: mime}); .then(b64 => {
this.setState({source: du}); const du = makeImageUrl({data: b64.bits, type: mime});
this.props.onUpdateTopicDesc(this.props.topic, theCard(null, du)); this.setState({source: du});
this.setState({uploading: false}); this.props.onUpdateTopicDesc(this.props.topic, theCard(null, du));
}); this.setState({uploading: false});
});
} }
}; };
if (width > AVATAR_SIZE || height > AVATAR_SIZE || width != height) { if (width > AVATAR_SIZE || height > AVATAR_SIZE || width != height) {
// Avatar is not square or too large even after cropping. Shrink it and make square. // Avatar is not square or too large even after cropping. Shrink it and make square.
imageScaled(blob, AVATAR_SIZE, AVATAR_SIZE, MAX_EXTERN_ATTACHMENT_SIZE, true, imageScaled(blob, AVATAR_SIZE, AVATAR_SIZE, MAX_EXTERN_ATTACHMENT_SIZE, true)
readyToUpload, .then(scaled => readyToUpload)
(err) => { .catch(err => {
this.props.onError(err, 'err'); this.props.onError(err, 'err');
}); });
} else { } else {
readyToUpload(mime, blob, width, height); readyToUpload({mime: mime, blob: blob, width: width, height: height});
} }
} }
+256 -179
View File
@@ -176,120 +176,159 @@ function fileNameForMime(fname, mime) {
return fname + '.' + ext; return fname + '.' + ext;
} }
function imageScaled(fileOrBlob, maxWidth, maxHeight, maxSize, forceSquare, onSuccess, onError) { function imageScaled(fileOrBlob, maxWidth, maxHeight, maxSize, forceSquare) {
const img = new Image(); return new Promise((resolve, reject) => {
img.crossOrigin = 'Anonymous'; const img = new Image();
img.crossOrigin = 'Anonymous';
img.onerror = function (err) { img.onerror = function (err) {
onError("Image format unrecognized"); reject(new Error("Image format unrecognized"));
}; };
img.onload = async function () { img.onload = async function () {
URL.revokeObjectURL(img.src); URL.revokeObjectURL(img.src);
const dim = fitImageSize(this.width, this.height, maxWidth, maxHeight, forceSquare); const dim = fitImageSize(this.width, this.height, maxWidth, maxHeight, forceSquare);
if (!dim) { if (!dim) {
onError("Invalid image"); reject(new Error("Invalid image"));
return; return;
} }
let canvas = document.createElement('canvas'); let canvas = document.createElement('canvas');
canvas.width = dim.dstWidth;
canvas.height = dim.dstHeight;
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);
const mime = SUPPORTED_IMAGE_FORMATS.includes(fileOrBlob.type) ? fileOrBlob.type : 'image/jpeg';
let blob = await new Promise(resolve => canvas.toBlob(resolve, mime));
if (!blob) {
onError("Unsupported image format");
return;
}
while (maxSize > 0 && blob.length > maxSize) {
dim.dstWidth = dim.dstWidth * 0.70710678118 | 0;
dim.dstHeight = dim.dstHeight * 0.70710678118 | 0;
canvas.width = dim.dstWidth; canvas.width = dim.dstWidth;
canvas.height = dim.dstHeight; canvas.height = dim.dstHeight;
ctx = canvas.getContext('2d'); let ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.imageSmoothingEnabled = true;
ctx.drawImage(this, dim.xoffset, dim.yoffset, dim.srcWidth, dim.srcHeight, 0, 0, dim.dstWidth, dim.dstHeight); 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)); const mime = SUPPORTED_IMAGE_FORMATS.includes(fileOrBlob.type) ? fileOrBlob.type : 'image/jpeg';
} let blob = await new Promise(resolve => canvas.toBlob(resolve, mime));
canvas = null; if (!blob) {
onSuccess(mime, blob, dim.dstWidth, dim.dstHeight, fileNameForMime(fileOrBlob.name, mime)); reject(new Error("Unsupported image format"));
}; return;
}
img.src = URL.createObjectURL(fileOrBlob); while (maxSize > 0 && blob.length > maxSize) {
} dim.dstWidth = dim.dstWidth * 0.70710678118 | 0;
function imageCrop(mime, objURL, left, top, width, height, scale, onSuccess, onError) { dim.dstHeight = dim.dstHeight * 0.70710678118 | 0;
const img = new Image(); canvas.width = dim.dstWidth;
img.crossOrigin = 'Anonymous'; 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));
}
img.onerror = function (err) { canvas = null;
onError("Image format unrecognized"); resolve({
}; mime: mime,
blob: blob,
width: dim.dstWidth,
height: dim.dstHeight,
name: fileNameForMime(fileOrBlob.name, mime)
});
};
img.onload = async function () { img.src = URL.createObjectURL(fileOrBlob);
URL.revokeObjectURL(img.src);
let canvas = document.createElement('canvas');
canvas.width = width * scale;
canvas.height = height * scale;
let ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = true;
ctx.drawImage(this, left, top, width, height, 0, 0, canvas.width, canvas.height);
mime = SUPPORTED_IMAGE_FORMATS.includes(mime) ? mime : 'image/jpeg';
let blob = await new Promise(resolve => canvas.toBlob(resolve, mime));
if (!blob) {
onError("Unsupported image format");
return;
}
canvas = null;
onSuccess(mime, blob, width, height);
};
img.src = objURL;
}
function fileToBase64(file, onSuccess) {
const reader = new FileReader();
reader.addEventListener('load', function () {
onSuccess(file.type, reader.result.split(',')[1], file.name);
}); });
reader.readAsDataURL(file);
} }
function blobToBase64(blob, onSuccess) { function imageCrop(mime, objURL, left, top, width, height, scale) {
const reader = new FileReader(); return new Promise((resolve, reject) => {
reader.addEventListener('load', function () { const img = new Image();
onSuccess(blob.type, reader.result.split(',')[1]); img.crossOrigin = 'Anonymous';
img.onerror = err => {
reject(new Error("Image format unrecognized"));
};
img.onload = () => {
URL.revokeObjectURL(img.src);
let canvas = document.createElement('canvas');
canvas.width = width * scale;
canvas.height = height * scale;
let ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = true;
ctx.drawImage(this, left, top, width, height, 0, 0, canvas.width, canvas.height);
mime = SUPPORTED_IMAGE_FORMATS.includes(mime) ? mime : 'image/jpeg';
canvas.toBlob(blob => {
canvas = null;
if (blob) {
resolve({
mime: mime,
blob: blob,
width: width,
height: height
});
} else {
reject(new Error("Unsupported image format"));
}
}, mime);
};
img.src = objURL;
});
}
function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = evt => {
reject(reader.error);
};
reader.onload = () => {
resolve({
mime: file.type,
bits: reader.result.split(',')[1],
name: file.name
});
};
reader.readAsDataURL(file);
});
}
function blobToBase64(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = evt => {
reject(reader.error);
};
reader.onload = () => {
resolve({
mime: blob.type,
bits: reader.result.split(',')[1]
});
};
reader.readAsDataURL(blob);
}); });
reader.readAsDataURL(blob);
} }
function filePasted(event, onImageSuccess, onAttachmentSuccess, onError) { function filePasted(event, onImageSuccess, onAttachmentSuccess, onError) {
var items = (event.clipboardData || event.originalEvent.clipboardData || {}).items; const items = (event.clipboardData || event.originalEvent.clipboardData || {}).items;
for (var i in items) { if (!Array.isArray(items)) {
var item = items[i]; return false;
}
for (let i in items) {
const item = items[i];
if (item.kind === 'file') { if (item.kind === 'file') {
var file = item.getAsFile(); const file = item.getAsFile();
if (!file) { if (!file) {
console.error("Failed to get file object from pasted file item", item.kind, item.type); console.error("Failed to get file object from pasted file item", item.kind, item.type);
onError("Failed to get file object from pasted file item");
continue; continue;
} }
if (file.type && file.type.split('/')[0] == 'image') { if (file.type && file.type.split('/')[0] == 'image') {
if (file.size > _config_js__WEBPACK_IMPORTED_MODULE_0__.MAX_INBAND_ATTACHMENT_SIZE || SUPPORTED_IMAGE_FORMATS.indexOf(file.type) < 0) { onImageSuccess(file);
imageFileScaledToBase64(file, _config_js__WEBPACK_IMPORTED_MODULE_0__.MAX_IMAGE_DIM, _config_js__WEBPACK_IMPORTED_MODULE_0__.MAX_IMAGE_DIM, false, onImageSuccess, onError);
} else {
imageFileToBase64(file, onImageSuccess, onError);
}
} else { } else {
fileToBase64(file, onAttachmentSuccess, onError); onAttachmentSuccess(file);
} }
return true; return true;
@@ -341,7 +380,7 @@ function base64ToBlob(str, mime) {
type: mime type: mime
}); });
} catch (err) { } catch (err) {
console.log("Failed to convert base64 to blob: ", err); console.error("Failed to convert base64 to blob: ", err);
} }
return null; return null;
@@ -449,6 +488,45 @@ function handleImageData(el, data, attr) {
return el; return el;
} }
function quotedImage(data) {
let promise;
if (data.val) {
const blob = base64ToBlob(data.val, data.mime);
promise = blob ? Promise.resolve(blob) : Prmise.reject(new Error("Invalid image"));
} else {
promise = fetch(this.authorizeURL((0,_utils_js__WEBPACK_IMPORTED_MODULE_7__.sanitizeImageUrl)(data.ref))).then(evt => {
if (evt.ok) {
return evt.blob();
} else {
throw new Error(`Image fetch unsuccessful: ${evt.status} ${evt.statusText}`);
}
});
}
return promise.then(blob => {
return imageScaled(blob, _config_js__WEBPACK_IMPORTED_MODULE_4__.IMAGE_THUMBNAIL_DIM, _config_js__WEBPACK_IMPORTED_MODULE_4__.IMAGE_THUMBNAIL_DIM, -1, false);
}).then(scaled => {
data.mime = scaled.mime;
data.size = scaled.blob.size;
data.width = scaled.width;
data.height = scaled.height;
delete data.ref;
return blobToBase64(scaled.blob);
}).then(b64 => {
data.val = b64.bits;
return data;
}).catch(err => {
delete data.val;
delete data.name;
data.width = _config_js__WEBPACK_IMPORTED_MODULE_4__.IMAGE_THUMBNAIL_DIM;
data.height = _config_js__WEBPACK_IMPORTED_MODULE_4__.IMAGE_THUMBNAIL_DIM;
data.maxWidth = _config_js__WEBPACK_IMPORTED_MODULE_4__.IMAGE_THUMBNAIL_DIM;
data.maxHeight = _config_js__WEBPACK_IMPORTED_MODULE_4__.IMAGE_THUMBNAIL_DIM;
throw err;
});
}
function shortenFileName(filename) { function shortenFileName(filename) {
return filename.length > 16 ? filename.slice(0, 7) + '…' + filename.slice(-7) : filename; return filename.length > 16 ? filename.slice(0, 7) + '…' + filename.slice(-7) : filename;
} }
@@ -3836,13 +3914,11 @@ class MessagesView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
}); });
this.sendMessage(msg, uploadCompletionPromise, uploader); this.sendMessage(msg, uploadCompletionPromise, uploader);
} else { } else {
(0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_15__.fileToBase64)(file, (mime, bits, fname) => { (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_15__.fileToBase64)(file).then(b64 => this.sendMessage(Drafty.attachFile(null, {
this.sendMessage(Drafty.attachFile(null, { mime: b64.mime,
mime: mime, data: b64.bits,
data: bits, filename: b64.name
filename: fname }))).catch(err => this.props.onError(err));
}));
}, this.props.onError);
} }
} }
@@ -3882,35 +3958,33 @@ class MessagesView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
} }
const uploadCompletionPromise = uploader.upload(blob); const uploadCompletionPromise = uploader.upload(blob);
(0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_15__.imageScaled)(blob, _config_js__WEBPACK_IMPORTED_MODULE_14__.IMAGE_PREVIEW_DIM, _config_js__WEBPACK_IMPORTED_MODULE_14__.IMAGE_PREVIEW_DIM, -1, false, (tinyMine, tinyBlob) => { (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_15__.imageScaled)(blob, _config_js__WEBPACK_IMPORTED_MODULE_14__.IMAGE_PREVIEW_DIM, _config_js__WEBPACK_IMPORTED_MODULE_14__.IMAGE_PREVIEW_DIM, -1, false).then(scaled => (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_15__.blobToBase64)(scaled.blob)).then(b64 => {
(0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_15__.blobToBase64)(tinyBlob, (blobMime, tinyBits64) => { let msg = Drafty.insertImage(null, 0, {
let msg = Drafty.insertImage(null, 0, { mime: mime,
mime: mime, _tempPreview: b64.bits,
_tempPreview: tinyBits64, width: width,
width: width, height: height,
height: height, filename: fname,
filename: fname, size: blob.size,
size: blob.size, urlPromise: uploadCompletionPromise
urlPromise: uploadCompletionPromise
});
if (caption) {
msg = Drafty.appendLineBreak(msg);
msg = Drafty.append(msg, Drafty.parse(caption));
}
this.sendMessage(msg, uploadCompletionPromise, uploader);
}); });
}, err => {
if (caption) {
msg = Drafty.appendLineBreak(msg);
msg = Drafty.append(msg, Drafty.parse(caption));
}
this.sendMessage(msg, uploadCompletionPromise, uploader);
}).catch(err => {
this.props.onError(err, 'err'); this.props.onError(err, 'err');
}); });
return; return;
} }
(0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_15__.blobToBase64)(blob, (blobMime, bits64) => { (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_15__.blobToBase64)(blob).then(b64 => {
let msg = Drafty.insertImage(null, 0, { let msg = Drafty.insertImage(null, 0, {
mime: blobMime, mime: b64.mime,
preview: bits64, preview: b64.bits,
width: width, width: width,
height: height, height: height,
filename: fname, filename: fname,
@@ -3928,19 +4002,19 @@ class MessagesView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
handleAttachImage(file) { handleAttachImage(file) {
const maxExternAttachmentSize = this.props.tinode.getServerLimit('maxFileUploadSize', _config_js__WEBPACK_IMPORTED_MODULE_14__.MAX_EXTERN_ATTACHMENT_SIZE); const maxExternAttachmentSize = this.props.tinode.getServerLimit('maxFileUploadSize', _config_js__WEBPACK_IMPORTED_MODULE_14__.MAX_EXTERN_ATTACHMENT_SIZE);
(0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_15__.imageScaled)(file, _config_js__WEBPACK_IMPORTED_MODULE_14__.MAX_IMAGE_DIM, _config_js__WEBPACK_IMPORTED_MODULE_14__.MAX_IMAGE_DIM, maxExternAttachmentSize, false, (mime, blob, width, height, fname) => { (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_15__.imageScaled)(file, _config_js__WEBPACK_IMPORTED_MODULE_14__.MAX_IMAGE_DIM, _config_js__WEBPACK_IMPORTED_MODULE_14__.MAX_IMAGE_DIM, maxExternAttachmentSize, false).then(scaled => {
this.setState({ this.setState({
imagePreview: { imagePreview: {
url: URL.createObjectURL(blob), url: URL.createObjectURL(scaled.blob),
blob: blob, blob: scaled.blob,
filename: fname, filename: scaled.fname,
width: width, width: scaled.width,
height: height, height: scaled.height,
size: blob.size, size: scaled.blob.size,
type: mime type: scaled.mime
} }
}); });
}, err => { }).catch(err => {
this.props.onError(err, 'err'); this.props.onError(err, 'err');
}); });
} }
@@ -4003,56 +4077,47 @@ class MessagesView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
}); });
} }
}); });
const promises = images.map(ex => {
return new Promise((resolve, reject) => {
const handleFailure = () => {
delete ex.data.val;
delete ex.data.name;
ex.data.width = _config_js__WEBPACK_IMPORTED_MODULE_14__.IMAGE_THUMBNAIL_DIM;
ex.data.height = _config_js__WEBPACK_IMPORTED_MODULE_14__.IMAGE_THUMBNAIL_DIM;
ex.data.maxWidth = _config_js__WEBPACK_IMPORTED_MODULE_14__.IMAGE_THUMBNAIL_DIM;
ex.data.maxHeight = _config_js__WEBPACK_IMPORTED_MODULE_14__.IMAGE_THUMBNAIL_DIM;
};
const scale = origBlob => { const quotedImage = data => {
(0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_15__.imageScaled)(origBlob, _config_js__WEBPACK_IMPORTED_MODULE_14__.IMAGE_THUMBNAIL_DIM, _config_js__WEBPACK_IMPORTED_MODULE_14__.IMAGE_THUMBNAIL_DIM, -1, false, (mime, blob, width, height, unused) => { let promise;
ex.data.mime = mime;
ex.data.size = blob.size;
ex.data.width = width;
ex.data.height = height;
delete ex.data.ref;
(0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_15__.blobToBase64)(blob, (blobMime, tinyBits64) => {
ex.data.val = tinyBits64;
resolve(true);
});
}, err => {
handleFailure();
reject(`Could not scale image: ${err}`);
});
};
if (ex.data.val) { if (data.val) {
const b = (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_15__.base64ToBlob)(ex.data.val, ex.data.mime); const blob = (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_15__.base64ToBlob)(data.val, data.mime);
promise = blob ? Promise.resolve(blob) : Prmise.reject(new Error("Invalid image"));
if (b) { } else {
scale(b); promise = fetch(this.props.tinode.authorizeURL((0,_lib_utils_js__WEBPACK_IMPORTED_MODULE_18__.sanitizeImageUrl)(data.ref))).then(evt => {
if (evt.ok) {
return evt.blob();
} else { } else {
handleFailure(); throw new Error(`Image fetch unsuccessful: ${evt.status} ${evt.statusText}`);
} }
} else { });
fetch(this.props.tinode.authorizeURL((0,_lib_utils_js__WEBPACK_IMPORTED_MODULE_18__.sanitizeImageUrl)(ex.data.ref))).then(e => { }
if (e.ok) {
return e.blob(); return promise.then(blob => {
} else { return (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_15__.imageScaled)(blob, _config_js__WEBPACK_IMPORTED_MODULE_14__.IMAGE_THUMBNAIL_DIM, _config_js__WEBPACK_IMPORTED_MODULE_14__.IMAGE_THUMBNAIL_DIM, -1, false);
handleFailure(); }).then(scaled => {
reject(`Image fetch unsuccessful: ${e.status} ${e.statusText}`); data.mime = scaled.mime;
} data.size = scaled.blob.size;
}).then(b => scale(b)).catch(err => reject(`Error fetching image data: ${err}`)); data.width = scaled.width;
return; data.height = scaled.height;
} delete data.ref;
return (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_15__.blobToBase64)(scaled.blob);
}).then(b64 => {
data.val = b64.bits;
return data;
}).catch(err => {
delete data.val;
delete data.name;
data.width = _config_js__WEBPACK_IMPORTED_MODULE_14__.IMAGE_THUMBNAIL_DIM;
data.height = _config_js__WEBPACK_IMPORTED_MODULE_14__.IMAGE_THUMBNAIL_DIM;
data.maxWidth = _config_js__WEBPACK_IMPORTED_MODULE_14__.IMAGE_THUMBNAIL_DIM;
data.maxHeight = _config_js__WEBPACK_IMPORTED_MODULE_14__.IMAGE_THUMBNAIL_DIM;
throw err;
}); });
}); };
Promise.all(promises).catch(err => {
Promise.all(images.map(ex => quotedImage(ex.data))).catch(err => {
this.props.onError(err, 'err'); this.props.onError(err, 'err');
}).finally(() => { }).finally(() => {
this.setState({ this.setState({
@@ -7321,9 +7386,9 @@ class AvatarCrop extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompo
} }
handleSubmit() { handleSubmit() {
(0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_3__.imageCrop)(this.props.mime, this.props.avatar, this.state.left, this.state.top, this.state.width, this.state.height, this.state.scale, (mime, blob, width, height) => { (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_3__.imageCrop)(this.props.mime, this.props.avatar, this.state.left, this.state.top, this.state.width, this.state.height, this.state.scale).then(img => {
this.props.onSubmit(mime, blob, width, height); this.props.onSubmit(img.mime, img.blob, img.width, img.height);
}, err => { }).catch(err => {
this.props.onError(err); this.props.onError(err);
}); });
} }
@@ -12216,7 +12281,14 @@ class TopicDescEdit extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compon
} }
uploadAvatar(mime, blob, width, height) { uploadAvatar(mime, blob, width, height) {
const readyToUpload = (mime, blob, width, height) => { const readyToUpload = image => {
let {
mime,
blob,
width,
height
} = image;
if (blob.size > _config_js__WEBPACK_IMPORTED_MODULE_7__.MAX_AVATAR_BYTES) { if (blob.size > _config_js__WEBPACK_IMPORTED_MODULE_7__.MAX_AVATAR_BYTES) {
const uploader = this.props.tinode.getLargeFileHelper(); const uploader = this.props.tinode.getLargeFileHelper();
this.setState({ this.setState({
@@ -12235,9 +12307,9 @@ class TopicDescEdit extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compon
this.setState({ this.setState({
uploading: true uploading: true
}); });
(0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_8__.blobToBase64)(blob, (unused, base64bits) => { (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_8__.blobToBase64)(blob).then(b64 => {
const du = (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_8__.makeImageUrl)({ const du = (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_8__.makeImageUrl)({
data: base64bits, data: b64.bits,
type: mime type: mime
}); });
this.setState({ this.setState({
@@ -12252,11 +12324,16 @@ class TopicDescEdit extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compon
}; };
if (width > _config_js__WEBPACK_IMPORTED_MODULE_7__.AVATAR_SIZE || height > _config_js__WEBPACK_IMPORTED_MODULE_7__.AVATAR_SIZE || width != height) { if (width > _config_js__WEBPACK_IMPORTED_MODULE_7__.AVATAR_SIZE || height > _config_js__WEBPACK_IMPORTED_MODULE_7__.AVATAR_SIZE || width != height) {
(0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_8__.imageScaled)(blob, _config_js__WEBPACK_IMPORTED_MODULE_7__.AVATAR_SIZE, _config_js__WEBPACK_IMPORTED_MODULE_7__.AVATAR_SIZE, _config_js__WEBPACK_IMPORTED_MODULE_7__.MAX_EXTERN_ATTACHMENT_SIZE, true, readyToUpload, err => { (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_8__.imageScaled)(blob, _config_js__WEBPACK_IMPORTED_MODULE_7__.AVATAR_SIZE, _config_js__WEBPACK_IMPORTED_MODULE_7__.AVATAR_SIZE, _config_js__WEBPACK_IMPORTED_MODULE_7__.MAX_EXTERN_ATTACHMENT_SIZE, true).then(scaled => readyToUpload).catch(err => {
this.props.onError(err, 'err'); this.props.onError(err, 'err');
}); });
} else { } else {
readyToUpload(mime, blob, width, height); readyToUpload({
mime: mime,
blob: blob,
width: width,
height: height
});
} }
} }
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long