refactoring formatters for lazy display of scaled image previews (WIP)
This commit is contained in:
+78
-9
@@ -3,10 +3,11 @@ import { defineMessages } from 'react-intl';
|
||||
|
||||
import { Drafty } from 'tinode-sdk';
|
||||
|
||||
import LazyImage from '../widgets/lazy-image.jsx'
|
||||
import UploadingImage from '../widgets/uploading-image.jsx'
|
||||
|
||||
import { IMAGE_THUMBNAIL_DIM, BROKEN_IMAGE_SIZE, REM_SIZE } from '../config.js';
|
||||
import { fitImageSize } from './blob-helpers.js';
|
||||
import { blobToBase64, fitImageSize, imageScaled } from './blob-helpers.js';
|
||||
import { idToColorClass } from './strformat.js';
|
||||
import { sanitizeImageUrl } from './utils.js';
|
||||
|
||||
@@ -34,9 +35,6 @@ function handleImageData(el, data, attr) {
|
||||
attr.style = {
|
||||
width: IMAGE_THUMBNAIL_DIM + 'px',
|
||||
height: IMAGE_THUMBNAIL_DIM + 'px',
|
||||
// Looks like a Chrome bug: broken image does not respect 'width' and 'height'.
|
||||
minWidth: IMAGE_THUMBNAIL_DIM + 'px',
|
||||
minHeight: IMAGE_THUMBNAIL_DIM + 'px'
|
||||
};
|
||||
return el;
|
||||
}
|
||||
@@ -124,8 +122,9 @@ export function fullFormatter(style, data, values, key) {
|
||||
break;
|
||||
case 'MN':
|
||||
// Mention
|
||||
if (data && data.hasOwnProperty('colorId')) {
|
||||
attr.className = 'mn-dark-color' + data.colorId;
|
||||
attr.className = 'mention'
|
||||
if (data) {
|
||||
attr.className += ' ' + idToColorClass(data.val, false, true);
|
||||
}
|
||||
break;
|
||||
case 'FM':
|
||||
@@ -230,6 +229,21 @@ export function previewFormatter(style, data, values, key) {
|
||||
};
|
||||
|
||||
// Converts Drafty object into a quoted reply. 'this' is set by the caller.
|
||||
function inlineImageAttr(attr, data) {
|
||||
attr.style = {
|
||||
width: IMAGE_THUMBNAIL_DIM + 'px',
|
||||
height: IMAGE_THUMBNAIL_DIM + 'px',
|
||||
}
|
||||
attr.className = 'inline-image';
|
||||
attr.alt = shortenFileName((data && data.name) || this.formatMessage(messages.drafty_image));
|
||||
if (!data) {
|
||||
attr.src = 'img/broken_image.png';
|
||||
}
|
||||
attr.title = attr.alt;
|
||||
return attr;
|
||||
}
|
||||
|
||||
// Displays a portion of Drafty within 'QQ' quotes. 'this' is set by the caller.
|
||||
// 'this' must contain:
|
||||
// formatMessage: this.props.intl.formatMessage
|
||||
// messages: formatjs messages defined with defineMessages.
|
||||
@@ -242,9 +256,8 @@ export function quoteFormatter(style, data, values, key) {
|
||||
attr.key = key;
|
||||
switch(style) {
|
||||
case 'IM':
|
||||
const img = handleImageData.call(this, el, data, attr);
|
||||
values = [React.createElement(img, attr, null), ' ',
|
||||
shortenFileName((data && data.name) || this.formatMessage(messages.drafty_image))];
|
||||
attr = inlineImageAttr.call(this, attr, data);
|
||||
values = [React.createElement('img', attr, null), ' ', attr.alt];
|
||||
el = React.Fragment;
|
||||
// Fragment attributes.
|
||||
attr = {key: key};
|
||||
@@ -281,3 +294,59 @@ export function quoteFormatter(style, data, values, key) {
|
||||
}
|
||||
return previewFormatter.call(this, style, data, values, key);
|
||||
}
|
||||
|
||||
// Create image thumbnail suitable for inclusion in a quote.
|
||||
function quoteImage(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) : Promise.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 => {
|
||||
// Cut the square from the center of the image and shrink it.
|
||||
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;
|
||||
|
||||
data.src = URL.createObjectURL(scaled.blob);
|
||||
return blobToBase64(scaled.blob);
|
||||
}).then(b64 => {
|
||||
data.val = b64.bits;
|
||||
return data;
|
||||
}).catch(err => {
|
||||
delete data.val;
|
||||
delete data.src;
|
||||
data.width = IMAGE_THUMBNAIL_DIM;
|
||||
data.height = IMAGE_THUMBNAIL_DIM;
|
||||
// Rethrow.
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
// Create a preview of a reply.
|
||||
export function replyFormatter(style, data, values, key) {
|
||||
if (style != 'IM') {
|
||||
return quoteFormatter.call(this, style, data, values, key);
|
||||
}
|
||||
const attr = inlineImageAttr.call(this, {key: key}, data);
|
||||
attr.whenDone = quoteImage.call(this, data);
|
||||
values = [React.createElement(LazyImage, attr, null), ' ', attr.alt];
|
||||
return React.createElement(React.Fragment, {key: key}, values);
|
||||
}
|
||||
|
||||
@@ -871,7 +871,7 @@ class MessagesView extends React.Component {
|
||||
// Make small image previews.
|
||||
const images = [];
|
||||
Drafty.entities(content, (data, idx, tp) => {
|
||||
if (tp == 'IM') {
|
||||
if (tp == 'IM-disable') {
|
||||
images.push({
|
||||
tp: tp,
|
||||
data: data
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// Image with a placeholder which is replaced when the promise is resolved.
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
export default class LazyImage extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
src: 'img/placeholder.png',
|
||||
style: this.props.style,
|
||||
className: this.props.className,
|
||||
loading: 'lazy',
|
||||
alt: this.props.alt,
|
||||
onClick: this.props.onClick,
|
||||
};
|
||||
|
||||
this.props.whenDone
|
||||
.then(data => this.setState({src: data.src}))
|
||||
.catch(() => this.setState({src: 'img/broken_image.png'}));
|
||||
}
|
||||
|
||||
render() {
|
||||
return React.createElement('img', this.state);
|
||||
}
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import { Drafty } from 'tinode-sdk';
|
||||
|
||||
import { KEYPRESS_DELAY } from '../config.js';
|
||||
import { filePasted } from '../lib/blob-helpers.js';
|
||||
import { quoteFormatter } from '../lib/formatters.js';
|
||||
import { quoteFormatter, replyFormatter } from '../lib/formatters.js';
|
||||
|
||||
const messages = defineMessages({
|
||||
messaging_disabled: {
|
||||
@@ -162,7 +162,7 @@ class SendMessage extends React.PureComponent {
|
||||
formatMessage(messages.type_new_message));
|
||||
|
||||
const quote = this.props.replyTo ?
|
||||
Drafty.format(this.props.replyTo.content, quoteFormatter, {
|
||||
Drafty.format(this.props.replyTo.content, replyFormatter, {
|
||||
formatMessage: formatMessage.bind(this.props.intl),
|
||||
authorizeURL: this.props.tinode.authorizeURL.bind(this.props.tinode)
|
||||
}) :
|
||||
|
||||
Reference in New Issue
Block a user