everything seems to work as expected

This commit is contained in:
or-else
2021-09-18 15:02:27 -07:00
19 changed files with 599 additions and 541 deletions
+8
View File
@@ -2513,6 +2513,14 @@ ul.tabbar li.active > a {
text-align: center; text-align: center;
} }
#reply-quote-preview .inline-image {
background-color: #ccc;
}
#reply-quote-preview img.inline-image::before {
content: '';
}
.mention { .mention {
font-weight: bolder; font-weight: bolder;
} }
+1 -1
View File
File diff suppressed because one or more lines are too long
+3
View File
@@ -93,6 +93,9 @@ export const MAX_TOPIC_DESCRIPTION_LENGTH = 360;
// Length of message previews in chat list, in characters. // Length of message previews in chat list, in characters.
export const MESSAGE_PREVIEW_LENGTH = 80; export const MESSAGE_PREVIEW_LENGTH = 80;
// Length of a quote in quoted reply (for outgoing messages).
export const QUOTED_REPLY_LENGTH = 30;
// Link for "Contact Us". // Link for "Contact Us".
export const LINK_CONTACT_US = 'email:support@tinode.co'; export const LINK_CONTACT_US = 'email:support@tinode.co';
+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;
+49 -6
View File
@@ -7,8 +7,8 @@ import LazyImage from '../widgets/lazy-image.jsx'
import UploadingImage from '../widgets/uploading-image.jsx' import UploadingImage from '../widgets/uploading-image.jsx'
import { IMAGE_THUMBNAIL_DIM, BROKEN_IMAGE_SIZE, REM_SIZE } from '../config.js'; import { IMAGE_THUMBNAIL_DIM, BROKEN_IMAGE_SIZE, REM_SIZE } from '../config.js';
import { blobToBase64, fitImageSize, imageScaled } from './blob-helpers.js'; import { base64ToBlob, blobToBase64, fitImageSize, imageScaled } from './blob-helpers.js';
import { idToColorClass } from './strformat.js'; import { idToColorClass, shortenFileName } from './strformat.js';
import { sanitizeImageUrl } from './utils.js'; import { sanitizeImageUrl } from './utils.js';
const messages = defineMessages({ const messages = defineMessages({
@@ -29,6 +29,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';
@@ -72,8 +73,48 @@ function handleImageData(el, data, attr) {
return el; return el;
} }
function shortenFileName(filename) { function quotedImage(data) {
return filename.length > 16 ? filename.slice(0, 7) + '…' + filename.slice(-7) : filename; 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, true)
}).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;
});
} }
// The main Drafty formatter: converts Drafty elements into React classes. 'this' is set by the caller. // The main Drafty formatter: converts Drafty elements into React classes. 'this' is set by the caller.
@@ -233,9 +274,11 @@ function inlineImageAttr(attr, data) {
attr.style = { attr.style = {
width: IMAGE_THUMBNAIL_DIM + 'px', width: IMAGE_THUMBNAIL_DIM + 'px',
height: IMAGE_THUMBNAIL_DIM + 'px', height: IMAGE_THUMBNAIL_DIM + 'px',
maxWidth: IMAGE_THUMBNAIL_DIM + 'px',
maxHeight: IMAGE_THUMBNAIL_DIM + 'px',
} }
attr.className = 'inline-image'; attr.className = 'inline-image';
attr.alt = shortenFileName((data && data.name) || this.formatMessage(messages.drafty_image)); attr.alt = shortenFileName(data && data.name, 16) || this.formatMessage(messages.drafty_image);
if (!data) { if (!data) {
attr.src = 'img/broken_image.png'; attr.src = 'img/broken_image.png';
} }
@@ -287,7 +330,7 @@ export function quoteFormatter(style, data, values, key) {
} }
el = React.Fragment; el = React.Fragment;
values = [<i key="ex" className="material-icons">attachment</i>, values = [<i key="ex" className="material-icons">attachment</i>,
shortenFileName(fname || this.formatMessage(messages.drafty_attachment))]; shortenFileName(fname, 16) || this.formatMessage(messages.drafty_attachment)];
break; break;
} }
return React.createElement(el, attr, values); return React.createElement(el, attr, values);
+9
View File
@@ -37,6 +37,15 @@ export function bytesToHumanSize(bytes) {
return count.toFixed(round) + ' ' + sizes[bucket]; return count.toFixed(round) + ' ' + sizes[bucket];
} }
// Shorten a file name to be under maxLength by clipping out the middle.
export function shortenFileName(filename, maxLength) {
if (typeof filename != 'string') {
return filename;
}
return filename.length > maxLength ?
filename.slice(0, maxLength/2 - 1) + '…' + filename.slice(1 - maxLength/2) : filename;
}
// Get 32 bit integer hash value for a string. Ideally it should produce the same value // Get 32 bit integer hash value for a string. Ideally it should produce the same value
// as Java's String#hash(). // as Java's String#hash().
function stringToColorHash(value) { function stringToColorHash(value) {
+55 -138
View File
@@ -16,9 +16,9 @@ import LoadSpinner from '../widgets/load-spinner.jsx';
import LogoView from './logo-view.jsx'; import LogoView from './logo-view.jsx';
import SendMessage from '../widgets/send-message.jsx'; import SendMessage from '../widgets/send-message.jsx';
import { DEFAULT_P2P_ACCESS_MODE, IMAGE_PREVIEW_DIM, KEYPRESS_DELAY, MESSAGES_PAGE, import { DEFAULT_P2P_ACCESS_MODE, IMAGE_PREVIEW_DIM, IMAGE_THUMBNAIL_DIM, KEYPRESS_DELAY,
MAX_EXTERN_ATTACHMENT_SIZE, MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE, READ_DELAY, MESSAGES_PAGE, MAX_EXTERN_ATTACHMENT_SIZE, MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE,
IMAGE_THUMBNAIL_DIM } from '../config.js'; READ_DELAY, QUOTED_REPLY_LENGTH } from '../config.js';
import { SUPPORTED_IMAGE_FORMATS, blobToBase64, base64ToBlob, filePasted, fileToBase64, import { SUPPORTED_IMAGE_FORMATS, blobToBase64, base64ToBlob, filePasted, fileToBase64,
imageScaled, makeImageUrl } from '../lib/blob-helpers.js'; imageScaled, makeImageUrl } from '../lib/blob-helpers.js';
import HashNavigation from '../lib/navigation.js'; import HashNavigation from '../lib/navigation.js';
@@ -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
);
} }
} }
@@ -722,21 +719,23 @@ class MessagesView extends React.Component {
this.props.onError(this.props.intl.formatMessage(messages.file_attachment_too_large, this.props.onError(this.props.intl.formatMessage(messages.file_attachment_too_large,
{size: bytesToHumanSize(file.size), limit: bytesToHumanSize(maxExternAttachmentSize)}), 'err'); {size: bytesToHumanSize(file.size), limit: bytesToHumanSize(maxExternAttachmentSize)}), 'err');
} else { } else {
this.setState({ docPreview: { this.setState({
file: file, docPreview: {
filename: file.name, file: file,
size: file.size, name: file.name,
type: file.type size: file.size,
}}); type: file.type
}
});
} }
} }
// sendImageAttachment sends the image bits inband as Drafty message. // sendImageAttachment sends the image bits inband as Drafty message.
sendImageAttachment(caption, blob) { sendImageAttachment(caption, blob) {
const mime = this.state.imagePreview.type; const mime = this.state.imagePreview.mime;
const width = this.state.imagePreview.width; const width = this.state.imagePreview.width;
const height = this.state.imagePreview.height; const height = this.state.imagePreview.height;
const fname = this.state.imagePreview.filename; const fname = this.state.imagePreview.name;
// Server-provided limit reduced for base64 encoding and overhead. // Server-provided limit reduced for base64 encoding and overhead.
const maxInbandAttachmentSize = (this.props.tinode.getServerLimit('maxMessageSize', const maxInbandAttachmentSize = (this.props.tinode.getServerLimit('maxMessageSize',
@@ -752,13 +751,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 +770,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 +800,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, name: scaled.name,
width: width, width: scaled.width,
height: height, height: scaled.height,
size: blob.size, size: scaled.blob.size,
type: mime mime: scaled.mime
}}); }});
}, }).catch(err => {
// Failure
(err) => {
this.props.onError(err, 'err'); this.props.onError(err, 'err');
} });
);
} }
handleCancelUpload(seq, uploader) { handleCancelUpload(seq, uploader) {
@@ -844,7 +836,7 @@ class MessagesView extends React.Component {
content = Drafty.init(content); content = Drafty.init(content);
} }
if (Drafty.isValid(content)) { if (Drafty.isValid(content)) {
content = Drafty.preview(content, 30); content = Drafty.preview(content, QUOTED_REPLY_LENGTH);
} else { } else {
// /!\ invalid content. // /!\ invalid content.
content = Drafty.append(Drafty.init('\u26A0 '), content = Drafty.append(Drafty.init('\u26A0 '),
@@ -868,87 +860,12 @@ class MessagesView extends React.Component {
} }
} }
// Make small image previews. this.setState({
const images = []; reply: {
Drafty.entities(content, (data, idx, tp) => { content: Drafty.quote(senderName, senderId, content),
if (tp == 'IM-disable') { seq: seq
images.push({
tp: tp,
data: data
});
} }
}); });
// Turn all images into thumbnails.
const promises = images.map((ex) => {
return new Promise((resolve, reject) => {
const handleFailure = () => {
delete ex.data.val;
delete ex.data.name;
ex.data.width = IMAGE_THUMBNAIL_DIM;
ex.data.height = IMAGE_THUMBNAIL_DIM;
ex.data.maxWidth = IMAGE_THUMBNAIL_DIM;
ex.data.maxHeight = IMAGE_THUMBNAIL_DIM;
};
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 {
handleFailure();
}
} 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)
.catch((err) => {
this.props.onError(err, 'err');
})
.finally(() => {
// All done. Create a reply quote.
this.setState({
reply: {
content: Drafty.quote(senderName, senderId, content),
seq: seq
}
});
});
} }
handleCancelReply() { handleCancelReply() {
+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() {
+4 -2
View File
@@ -2,7 +2,7 @@ import React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import SendMessage from '../widgets/send-message.jsx'; import SendMessage from '../widgets/send-message.jsx';
import { bytesToHumanSize } from '../lib/strformat.js'; import { bytesToHumanSize, shortenFileName } from '../lib/strformat.js';
// Get material icon name from mime type. // Get material icon name from mime type.
function iconFromMime(mime) { function iconFromMime(mime) {
@@ -31,12 +31,14 @@ export default class DocPreview extends React.PureComponent {
return ( return (
<div id="image-preview"> <div id="image-preview">
<div id="image-preview-caption-panel"> <div id="image-preview-caption-panel">
<span>{this.props.content.filename}</span> <span>{this.props.content.name}</span>
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onClose();}}><i className="material-icons gray">close</i></a> <a href="#" onClick={(e) => {e.preventDefault(); this.props.onClose();}}><i className="material-icons gray">close</i></a>
</div> </div>
<div id="image-preview-container"> <div id="image-preview-container">
<div className="flex-column narrow"> <div className="flex-column narrow">
<i className="material-icons gray">{iconFromMime(this.props.content.type)}</i> <i className="material-icons gray">{iconFromMime(this.props.content.type)}</i>
<div><b><FormattedMessage id="label_file_name" defaultMessage="File name:"
description="Label for a file name" /></b> {shortenFileName(this.props.content.name, 24) || '-'}</div>
<div><b><FormattedMessage id="label_content_type" defaultMessage="Content type:" <div><b><FormattedMessage id="label_content_type" defaultMessage="Content type:"
description="Label for file content type (mime)" /></b> {this.props.content.type || 'application/octet-stream'}</div> description="Label for file content type (mime)" /></b> {this.props.content.type || 'application/octet-stream'}</div>
<div><b><FormattedMessage id="label_size" defaultMessage="Size:" <div><b><FormattedMessage id="label_size" defaultMessage="Size:"
+7 -9
View File
@@ -4,7 +4,7 @@ import SendMessage from '../widgets/send-message.jsx';
import { REM_SIZE } from '../config.js'; import { REM_SIZE } from '../config.js';
import { fitImageSize } from '../lib/blob-helpers.js'; import { fitImageSize } from '../lib/blob-helpers.js';
import { bytesToHumanSize } from '../lib/strformat.js'; import { bytesToHumanSize, shortenFileName } from '../lib/strformat.js';
export default class ImagePreview extends React.PureComponent { export default class ImagePreview extends React.PureComponent {
constructor(props) { constructor(props) {
@@ -45,29 +45,27 @@ export default class ImagePreview extends React.PureComponent {
size.maxWidth = '100%'; size.maxWidth = '100%';
size.maxHeight = '100%'; size.maxHeight = '100%';
let filename = this.props.content.filename;
// Average font aspect ratio is ~0.5; File name takes 1/3 of the viewport width. // Average font aspect ratio is ~0.5; File name takes 1/3 of the viewport width.
const maxlength = Math.max(((this.state.width / REM_SIZE / 1.5) | 0) - 2, 12); const maxlength = Math.max(((this.state.width / REM_SIZE / 1.5) | 0) - 2, 12);
if (filename.length > maxlength) { const fname = shortenFileName(this.props.content.name, maxlength) || '-';
filename = filename.slice(0, maxlength/2 - 1) + '…' + filename.slice(1 - maxlength/2);
}
const width = this.props.content.width || '-'; const width = this.props.content.width || '-';
const height = this.props.content.height || '-'; const height = this.props.content.height || '-';
return ( return (
<div id="image-preview"> <div id="image-preview">
<div id="image-preview-caption-panel"> <div id="image-preview-caption-panel">
{!this.props.onSendMessage ? {!this.props.onSendMessage ?
<a href={this.props.content.url} download={this.props.content.filename}> <a href={this.props.content.url} download={this.props.content.name}>
<i className="material-icons">file_download</i> <FormattedMessage <i className="material-icons">file_download</i> <FormattedMessage
id="download_action" defaultMessage="download" description="Call to action [download]" /> id="download_action" defaultMessage="download" description="Call to action [download]" />
</a> </a>
: :
<span>{this.props.content.filename}</span> <span>{fname}</span>
} }
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onClose();}}><i className="material-icons gray">close</i></a> <a href="#" onClick={(e) => {e.preventDefault(); this.props.onClose();}}><i className="material-icons gray">close</i></a>
</div> </div>
<div id="image-preview-container" ref={(node) => this.assignWidth(node)}> <div id="image-preview-container" ref={(node) => this.assignWidth(node)}>
<img src={this.props.content.url} style={size} className="image-preview" alt={this.props.content.filename} /> <img src={this.props.content.url} style={size} className="image-preview" alt={this.props.content.name} />
</div> </div>
{this.props.onSendMessage ? {this.props.onSendMessage ?
<SendMessage <SendMessage
@@ -83,7 +81,7 @@ export default class ImagePreview extends React.PureComponent {
<div> <div>
<div><b><FormattedMessage id="label_file_name" defaultMessage="File name:" <div><b><FormattedMessage id="label_file_name" defaultMessage="File name:"
description="Label for a file name" /></b></div> description="Label for a file name" /></b></div>
<div><span title={this.props.content.filename}>{filename ? filename : '-'}</span></div> <div><span title={this.props.content.name}>{fname}</span></div>
</div> </div>
<div> <div>
<div><b><FormattedMessage id="label_content_type" defaultMessage="Content type:" <div><b><FormattedMessage id="label_content_type" defaultMessage="Content type:"
+13 -3
View File
@@ -8,18 +8,28 @@ export default class LazyImage extends React.PureComponent {
this.state = { this.state = {
src: 'img/placeholder.png', src: 'img/placeholder.png',
style: this.props.style, style: Object.assign({padding: '4px'}, this.props.style),
className: this.props.className, className: this.props.className,
loading: 'lazy',
alt: this.props.alt, alt: this.props.alt,
onClick: this.props.onClick, onClick: this.props.onClick,
}; };
}
componentDidMount() {
this.props.whenDone this.props.whenDone
.then(data => this.setState({src: data.src})) .then(data => this.setState({src: data.src, style: {...this.state.style, padding: 0}}))
.catch(() => this.setState({src: 'img/broken_image.png'})); .catch(() => this.setState({src: 'img/broken_image.png'}));
} }
componentDidUpdate(prevProps) {
if (prevProps.whenDone != this.props.whenDone) {
this.setState({src: 'img/placeholder.png', style: {...this.state.style, padding: '4px'}});
this.props.whenDone
.then(data => this.setState({src: data.src, style: {...this.state.style, padding: 0}}))
.catch(() => this.setState({src: 'img/broken_image.png'}));
}
}
render() { render() {
return React.createElement('img', this.state); return React.createElement('img', this.state);
} }
+12 -10
View File
@@ -67,10 +67,19 @@ class SendMessage extends React.PureComponent {
} }
} }
componentDidUpdate() { componentDidUpdate(prevProps) {
if (this.messageEditArea) { if (this.messageEditArea) {
this.messageEditArea.focus(); this.messageEditArea.focus();
} }
if (prevProps.replyTo != this.props.replyTo) {
this.setState({quote: this.props.replyTo ?
Drafty.format(this.props.replyTo.content, replyFormatter, {
formatMessage: this.props.intl.formatMessage.bind(this.props.intl),
authorizeURL: this.props.tinode.authorizeURL.bind(this.props.tinode)
}) :
null});
}
} }
handlePasteEvent(e) { handlePasteEvent(e) {
@@ -161,21 +170,14 @@ class SendMessage extends React.PureComponent {
formatMessage(messages[this.props.messagePrompt]) : formatMessage(messages[this.props.messagePrompt]) :
formatMessage(messages.type_new_message)); formatMessage(messages.type_new_message));
const quote = this.props.replyTo ?
Drafty.format(this.props.replyTo.content, replyFormatter, {
formatMessage: formatMessage.bind(this.props.intl),
authorizeURL: this.props.tinode.authorizeURL.bind(this.props.tinode)
}) :
null;
return ( return (
<div id="send-message-wrapper"> <div id="send-message-wrapper">
{quote ? {this.state.quote ?
<div id="reply-quote-preview"> <div id="reply-quote-preview">
<div className="cancel"> <div className="cancel">
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onCancelReply();}}><i className="material-icons gray">close</i></a> <a href="#" onClick={(e) => {e.preventDefault(); this.props.onCancelReply();}}><i className="material-icons gray">close</i></a>
</div> </div>
{quote} {this.state.quote}
</div> </div>
: :
null} null}
+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});
} }
} }
+296 -232
View File
@@ -40,6 +40,7 @@ __webpack_require__.r(__webpack_exports__);
/* harmony export */ "MAX_TITLE_LENGTH": () => (/* binding */ MAX_TITLE_LENGTH), /* harmony export */ "MAX_TITLE_LENGTH": () => (/* binding */ MAX_TITLE_LENGTH),
/* harmony export */ "MAX_TOPIC_DESCRIPTION_LENGTH": () => (/* binding */ MAX_TOPIC_DESCRIPTION_LENGTH), /* harmony export */ "MAX_TOPIC_DESCRIPTION_LENGTH": () => (/* binding */ MAX_TOPIC_DESCRIPTION_LENGTH),
/* harmony export */ "MESSAGE_PREVIEW_LENGTH": () => (/* binding */ MESSAGE_PREVIEW_LENGTH), /* harmony export */ "MESSAGE_PREVIEW_LENGTH": () => (/* binding */ MESSAGE_PREVIEW_LENGTH),
/* harmony export */ "QUOTED_REPLY_LENGTH": () => (/* binding */ QUOTED_REPLY_LENGTH),
/* harmony export */ "LINK_CONTACT_US": () => (/* binding */ LINK_CONTACT_US), /* harmony export */ "LINK_CONTACT_US": () => (/* binding */ LINK_CONTACT_US),
/* harmony export */ "LINK_PRIVACY_POLICY": () => (/* binding */ LINK_PRIVACY_POLICY), /* harmony export */ "LINK_PRIVACY_POLICY": () => (/* binding */ LINK_PRIVACY_POLICY),
/* harmony export */ "LINK_TERMS_OF_SERVICE": () => (/* binding */ LINK_TERMS_OF_SERVICE) /* harmony export */ "LINK_TERMS_OF_SERVICE": () => (/* binding */ LINK_TERMS_OF_SERVICE)
@@ -79,6 +80,7 @@ const MAX_ONLINE_IN_TOPIC = 4;
const MAX_TITLE_LENGTH = 60; const MAX_TITLE_LENGTH = 60;
const MAX_TOPIC_DESCRIPTION_LENGTH = 360; const MAX_TOPIC_DESCRIPTION_LENGTH = 360;
const MESSAGE_PREVIEW_LENGTH = 80; const MESSAGE_PREVIEW_LENGTH = 80;
const QUOTED_REPLY_LENGTH = 30;
const LINK_CONTACT_US = 'email:support@tinode.co'; const LINK_CONTACT_US = 'email:support@tinode.co';
const LINK_PRIVACY_POLICY = 'https://tinode.co/privacy.html'; const LINK_PRIVACY_POLICY = 'https://tinode.co/privacy.html';
const LINK_TERMS_OF_SERVICE = 'https://tinode.co/terms.html'; const LINK_TERMS_OF_SERVICE = 'https://tinode.co/terms.html';
@@ -176,120 +178,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 +382,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;
@@ -450,8 +491,43 @@ function handleImageData(el, data, attr) {
return el; return el;
} }
function shortenFileName(filename) { function quotedImage(data) {
return filename.length > 16 ? filename.slice(0, 7) + '…' + filename.slice(-7) : filename; let promise;
if (data.val) {
const blob = (0,_blob_helpers_js__WEBPACK_IMPORTED_MODULE_6__.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_8__.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 (0,_blob_helpers_js__WEBPACK_IMPORTED_MODULE_6__.imageScaled)(blob, _config_js__WEBPACK_IMPORTED_MODULE_5__.IMAGE_THUMBNAIL_DIM, _config_js__WEBPACK_IMPORTED_MODULE_5__.IMAGE_THUMBNAIL_DIM, -1, true);
}).then(scaled => {
data.mime = scaled.mime;
data.size = scaled.blob.size;
data.width = scaled.width;
data.height = scaled.height;
delete data.ref;
return (0,_blob_helpers_js__WEBPACK_IMPORTED_MODULE_6__.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_5__.IMAGE_THUMBNAIL_DIM;
data.height = _config_js__WEBPACK_IMPORTED_MODULE_5__.IMAGE_THUMBNAIL_DIM;
data.maxWidth = _config_js__WEBPACK_IMPORTED_MODULE_5__.IMAGE_THUMBNAIL_DIM;
data.maxHeight = _config_js__WEBPACK_IMPORTED_MODULE_5__.IMAGE_THUMBNAIL_DIM;
throw err;
});
} }
function fullFormatter(style, data, values, key) { function fullFormatter(style, data, values, key) {
@@ -623,10 +699,12 @@ function previewFormatter(style, data, values, key) {
function inlineImageAttr(attr, data) { function inlineImageAttr(attr, data) {
attr.style = { attr.style = {
width: _config_js__WEBPACK_IMPORTED_MODULE_5__.IMAGE_THUMBNAIL_DIM + 'px', width: _config_js__WEBPACK_IMPORTED_MODULE_5__.IMAGE_THUMBNAIL_DIM + 'px',
height: _config_js__WEBPACK_IMPORTED_MODULE_5__.IMAGE_THUMBNAIL_DIM + 'px' height: _config_js__WEBPACK_IMPORTED_MODULE_5__.IMAGE_THUMBNAIL_DIM + 'px',
maxWidth: _config_js__WEBPACK_IMPORTED_MODULE_5__.IMAGE_THUMBNAIL_DIM + 'px',
maxHeight: _config_js__WEBPACK_IMPORTED_MODULE_5__.IMAGE_THUMBNAIL_DIM + 'px'
}; };
attr.className = 'inline-image'; attr.className = 'inline-image';
attr.alt = shortenFileName(data && data.name || this.formatMessage(messages.drafty_image)); attr.alt = (0,_strformat_js__WEBPACK_IMPORTED_MODULE_7__.shortenFileName)(data && data.name, 16) || this.formatMessage(messages.drafty_image);
if (!data) { if (!data) {
attr.src = 'img/broken_image.png'; attr.src = 'img/broken_image.png';
@@ -684,7 +762,7 @@ function quoteFormatter(style, data, values, key) {
values = [react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", { values = [react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {
key: "ex", key: "ex",
className: "material-icons" className: "material-icons"
}, "attachment"), shortenFileName(fname || this.formatMessage(messages.drafty_attachment))]; }, "attachment"), (0,_strformat_js__WEBPACK_IMPORTED_MODULE_7__.shortenFileName)(fname, 16) || this.formatMessage(messages.drafty_attachment)];
break; break;
} }
@@ -698,7 +776,7 @@ function quoteImage(data) {
let promise; let promise;
if (data.val) { if (data.val) {
const blob = base64ToBlob(data.val, data.mime); const blob = (0,_blob_helpers_js__WEBPACK_IMPORTED_MODULE_6__.base64ToBlob)(data.val, data.mime);
promise = blob ? Promise.resolve(blob) : Promise.reject(new Error("Invalid image")); promise = blob ? Promise.resolve(blob) : Promise.reject(new Error("Invalid image"));
} else { } else {
promise = fetch(this.authorizeURL((0,_utils_js__WEBPACK_IMPORTED_MODULE_8__.sanitizeImageUrl)(data.ref))).then(evt => { promise = fetch(this.authorizeURL((0,_utils_js__WEBPACK_IMPORTED_MODULE_8__.sanitizeImageUrl)(data.ref))).then(evt => {
@@ -935,6 +1013,7 @@ __webpack_require__.r(__webpack_exports__);
/* harmony export */ "shortDateFormat": () => (/* binding */ shortDateFormat), /* harmony export */ "shortDateFormat": () => (/* binding */ shortDateFormat),
/* harmony export */ "secondsToTime": () => (/* binding */ secondsToTime), /* harmony export */ "secondsToTime": () => (/* binding */ secondsToTime),
/* harmony export */ "bytesToHumanSize": () => (/* binding */ bytesToHumanSize), /* harmony export */ "bytesToHumanSize": () => (/* binding */ bytesToHumanSize),
/* harmony export */ "shortenFileName": () => (/* binding */ shortenFileName),
/* harmony export */ "letterTileColorId": () => (/* binding */ letterTileColorId), /* harmony export */ "letterTileColorId": () => (/* binding */ letterTileColorId),
/* harmony export */ "idToColorClass": () => (/* binding */ idToColorClass) /* harmony export */ "idToColorClass": () => (/* binding */ idToColorClass)
/* harmony export */ }); /* harmony export */ });
@@ -983,6 +1062,13 @@ function bytesToHumanSize(bytes) {
const round = bucket > 0 ? count < 3 ? 2 : count < 30 ? 1 : 0 : 0; const round = bucket > 0 ? count < 3 ? 2 : count < 30 ? 1 : 0 : 0;
return count.toFixed(round) + ' ' + sizes[bucket]; return count.toFixed(round) + ' ' + sizes[bucket];
} }
function shortenFileName(filename, maxLength) {
if (typeof filename != 'string') {
return filename;
}
return filename.length > maxLength ? filename.slice(0, maxLength / 2 - 1) + '…' + filename.slice(1 - maxLength / 2) : filename;
}
function stringToColorHash(value) { function stringToColorHash(value) {
let hash = 0; let hash = 0;
@@ -3909,13 +3995,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);
} }
} }
@@ -3931,7 +4015,7 @@ class MessagesView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
this.setState({ this.setState({
docPreview: { docPreview: {
file: file, file: file,
filename: file.name, name: file.name,
size: file.size, size: file.size,
type: file.type type: file.type
} }
@@ -3940,10 +4024,10 @@ class MessagesView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
} }
sendImageAttachment(caption, blob) { sendImageAttachment(caption, blob) {
const mime = this.state.imagePreview.type; const mime = this.state.imagePreview.mime;
const width = this.state.imagePreview.width; const width = this.state.imagePreview.width;
const height = this.state.imagePreview.height; const height = this.state.imagePreview.height;
const fname = this.state.imagePreview.filename; const fname = this.state.imagePreview.name;
const maxInbandAttachmentSize = this.props.tinode.getServerLimit('maxMessageSize', _config_js__WEBPACK_IMPORTED_MODULE_14__.MAX_INBAND_ATTACHMENT_SIZE) * 0.75 - 1024 | 0; const maxInbandAttachmentSize = this.props.tinode.getServerLimit('maxMessageSize', _config_js__WEBPACK_IMPORTED_MODULE_14__.MAX_INBAND_ATTACHMENT_SIZE) * 0.75 - 1024 | 0;
if (blob.size > maxInbandAttachmentSize) { if (blob.size > maxInbandAttachmentSize) {
@@ -3955,35 +4039,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,
@@ -4001,19 +4083,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, name: scaled.name,
width: width, width: scaled.width,
height: height, height: scaled.height,
size: blob.size, size: scaled.blob.size,
type: mime mime: scaled.mime
} }
}); });
}, err => { }).catch(err => {
this.props.onError(err, 'err'); this.props.onError(err, 'err');
}); });
} }
@@ -4043,7 +4125,7 @@ class MessagesView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
} }
if (Drafty.isValid(content)) { if (Drafty.isValid(content)) {
content = Drafty.preview(content, 30); content = Drafty.preview(content, _config_js__WEBPACK_IMPORTED_MODULE_14__.QUOTED_REPLY_LENGTH);
} else { } else {
content = Drafty.append(Drafty.init('\u26A0 '), Drafty.wrapInto(this.props.intl.formatMessage(messages.invalid_content), 'EM')); content = Drafty.append(Drafty.init('\u26A0 '), Drafty.wrapInto(this.props.intl.formatMessage(messages.invalid_content), 'EM'));
} }
@@ -4067,74 +4149,12 @@ class MessagesView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
} }
} }
const images = []; this.setState({
Drafty.entities(content, (data, idx, tp) => { reply: {
if (tp == 'IM-disable') { content: Drafty.quote(senderName, senderId, content),
images.push({ seq: seq
tp: tp,
data: data
});
} }
}); });
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 => {
(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) => {
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) {
const b = (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_15__.base64ToBlob)(ex.data.val, ex.data.mime);
if (b) {
scale(b);
} else {
handleFailure();
}
} 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();
} 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).catch(err => {
this.props.onError(err, 'err');
}).finally(() => {
this.setState({
reply: {
content: Drafty.quote(senderName, senderId, content),
seq: seq
}
});
});
} }
handleCancelReply() { handleCancelReply() {
@@ -7394,9 +7414,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);
}); });
} }
@@ -9384,7 +9404,7 @@ class DocPreview extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompo
id: "image-preview" id: "image-preview"
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { }, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
id: "image-preview-caption-panel" id: "image-preview-caption-panel"
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null, this.props.content.filename), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", { }, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null, this.props.content.name), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", {
href: "#", href: "#",
onClick: e => { onClick: e => {
e.preventDefault(); e.preventDefault();
@@ -9399,6 +9419,12 @@ class DocPreview extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompo
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", { }, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {
className: "material-icons gray" className: "material-icons gray"
}, iconFromMime(this.props.content.type)), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("b", null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__.FormattedMessage, { }, iconFromMime(this.props.content.type)), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("b", null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__.FormattedMessage, {
id: "label_file_name",
defaultMessage: [{
"type": 0,
"value": "File name:"
}]
})), " ", (0,_lib_strformat_js__WEBPACK_IMPORTED_MODULE_3__.shortenFileName)(this.props.content.name, 24) || '-'), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("b", null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__.FormattedMessage, {
id: "label_content_type", id: "label_content_type",
defaultMessage: [{ defaultMessage: [{
"type": 0, "type": 0,
@@ -10028,13 +10054,8 @@ class ImagePreview extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCom
}; };
size.maxWidth = '100%'; size.maxWidth = '100%';
size.maxHeight = '100%'; size.maxHeight = '100%';
let filename = this.props.content.filename;
const maxlength = Math.max((this.state.width / _config_js__WEBPACK_IMPORTED_MODULE_3__.REM_SIZE / 1.5 | 0) - 2, 12); const maxlength = Math.max((this.state.width / _config_js__WEBPACK_IMPORTED_MODULE_3__.REM_SIZE / 1.5 | 0) - 2, 12);
const fname = (0,_lib_strformat_js__WEBPACK_IMPORTED_MODULE_5__.shortenFileName)(this.props.content.name, maxlength) || '-';
if (filename.length > maxlength) {
filename = filename.slice(0, maxlength / 2 - 1) + '…' + filename.slice(1 - maxlength / 2);
}
const width = this.props.content.width || '-'; const width = this.props.content.width || '-';
const height = this.props.content.height || '-'; const height = this.props.content.height || '-';
return react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { return react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
@@ -10043,7 +10064,7 @@ class ImagePreview extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCom
id: "image-preview-caption-panel" id: "image-preview-caption-panel"
}, !this.props.onSendMessage ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", { }, !this.props.onSendMessage ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", {
href: this.props.content.url, href: this.props.content.url,
download: this.props.content.filename download: this.props.content.name
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", { }, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {
className: "material-icons" className: "material-icons"
}, "file_download"), " ", react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__.FormattedMessage, { }, "file_download"), " ", react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__.FormattedMessage, {
@@ -10052,7 +10073,7 @@ class ImagePreview extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCom
"type": 0, "type": 0,
"value": "download" "value": "download"
}] }]
})) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null, this.props.content.filename), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", { })) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null, fname), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", {
href: "#", href: "#",
onClick: e => { onClick: e => {
e.preventDefault(); e.preventDefault();
@@ -10067,7 +10088,7 @@ class ImagePreview extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCom
src: this.props.content.url, src: this.props.content.url,
style: size, style: size,
className: "image-preview", className: "image-preview",
alt: this.props.content.filename alt: this.props.content.name
})), this.props.onSendMessage ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_widgets_send_message_jsx__WEBPACK_IMPORTED_MODULE_2__.default, { })), this.props.onSendMessage ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_widgets_send_message_jsx__WEBPACK_IMPORTED_MODULE_2__.default, {
messagePrompt: "add_image_caption", messagePrompt: "add_image_caption",
acceptBlank: true, acceptBlank: true,
@@ -10085,8 +10106,8 @@ class ImagePreview extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCom
"value": "File name:" "value": "File name:"
}] }]
}))), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { }))), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", {
title: this.props.content.filename title: this.props.content.name
}, filename ? filename : '-'))), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("b", null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__.FormattedMessage, { }, fname))), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("b", null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__.FormattedMessage, {
id: "label_content_type", id: "label_content_type",
defaultMessage: [{ defaultMessage: [{
"type": 0, "type": 0,
@@ -10364,19 +10385,45 @@ class LazyImage extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompon
super(props); super(props);
this.state = { this.state = {
src: 'img/placeholder.png', src: 'img/placeholder.png',
style: this.props.style, style: Object.assign({
padding: '4px'
}, this.props.style),
className: this.props.className, className: this.props.className,
loading: 'lazy',
alt: this.props.alt, alt: this.props.alt,
onClick: this.props.onClick onClick: this.props.onClick
}; };
}
componentDidMount() {
this.props.whenDone.then(data => this.setState({ this.props.whenDone.then(data => this.setState({
src: data.src src: data.src,
style: { ...this.state.style,
padding: 0
}
})).catch(() => this.setState({ })).catch(() => this.setState({
src: 'img/broken_image.png' src: 'img/broken_image.png'
})); }));
} }
componentDidUpdate(prevProps) {
if (prevProps.whenDone != this.props.whenDone) {
this.setState({
src: 'img/placeholder.png',
style: { ...this.state.style,
padding: '4px'
}
});
this.props.whenDone.then(data => this.setState({
src: data.src,
style: { ...this.state.style,
padding: 0
}
})).catch(() => this.setState({
src: 'img/broken_image.png'
}));
}
}
render() { render() {
return react__WEBPACK_IMPORTED_MODULE_0___default().createElement('img', this.state); return react__WEBPACK_IMPORTED_MODULE_0___default().createElement('img', this.state);
} }
@@ -11474,10 +11521,19 @@ class SendMessage extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureComp
} }
} }
componentDidUpdate() { componentDidUpdate(prevProps) {
if (this.messageEditArea) { if (this.messageEditArea) {
this.messageEditArea.focus(); this.messageEditArea.focus();
} }
if (prevProps.replyTo != this.props.replyTo) {
this.setState({
quote: this.props.replyTo ? tinode_sdk__WEBPACK_IMPORTED_MODULE_2__.Drafty.format(this.props.replyTo.content, _lib_formatters_js__WEBPACK_IMPORTED_MODULE_5__.replyFormatter, {
formatMessage: this.props.intl.formatMessage.bind(this.props.intl),
authorizeURL: this.props.tinode.authorizeURL.bind(this.props.tinode)
}) : null
});
}
} }
handlePasteEvent(e) { handlePasteEvent(e) {
@@ -11568,13 +11624,9 @@ class SendMessage extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureComp
formatMessage formatMessage
} = this.props.intl; } = this.props.intl;
const prompt = this.props.disabled ? formatMessage(messages.messaging_disabled) : this.props.messagePrompt ? formatMessage(messages[this.props.messagePrompt]) : formatMessage(messages.type_new_message); const prompt = this.props.disabled ? formatMessage(messages.messaging_disabled) : this.props.messagePrompt ? formatMessage(messages[this.props.messagePrompt]) : formatMessage(messages.type_new_message);
const quote = this.props.replyTo ? tinode_sdk__WEBPACK_IMPORTED_MODULE_2__.Drafty.format(this.props.replyTo.content, _lib_formatters_js__WEBPACK_IMPORTED_MODULE_5__.replyFormatter, {
formatMessage: formatMessage.bind(this.props.intl),
authorizeURL: this.props.tinode.authorizeURL.bind(this.props.tinode)
}) : null;
return react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { return react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
id: "send-message-wrapper" id: "send-message-wrapper"
}, quote ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { }, this.state.quote ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
id: "reply-quote-preview" id: "reply-quote-preview"
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { }, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "cancel" className: "cancel"
@@ -11586,7 +11638,7 @@ class SendMessage extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureComp
} }
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", { }, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {
className: "material-icons gray" className: "material-icons gray"
}, "close"))), quote) : null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { }, "close"))), this.state.quote) : null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
id: "send-message-panel" id: "send-message-panel"
}, !this.props.disabled ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, this.props.onAttachFile ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", { }, !this.props.disabled ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, this.props.onAttachFile ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", {
href: "#", href: "#",
@@ -12332,7 +12384,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({
@@ -12351,9 +12410,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({
@@ -12368,11 +12427,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
+7 -19
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