merge next
This commit is contained in:
+25
-1
@@ -186,7 +186,7 @@ export function fileToBase64(file) {
|
||||
export function blobToBase64(blob) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onerror = (evt) => {
|
||||
reader.onerror = _ => {
|
||||
reject(reader.error);
|
||||
};
|
||||
reader.onload = () => {
|
||||
@@ -281,3 +281,27 @@ export function base64ToBlob(str, mime) {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function intArrayToBase64(arr) {
|
||||
if (!Array.isArray(arr)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
let bin = '';
|
||||
new Uint8Array(arr).forEach(b => bin += String.fromCharCode(b));
|
||||
return window.btoa(bin);
|
||||
} catch (err) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function base64ToIntArray(b64) {
|
||||
const arr = [];
|
||||
try {
|
||||
const bin = window.atob(b64);
|
||||
[...bin].forEach(c => {
|
||||
arr.push(c.charCodeAt(0));
|
||||
});
|
||||
return arr;
|
||||
} catch (err) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
+48
-59
@@ -3,33 +3,39 @@ import { defineMessages } from 'react-intl';
|
||||
|
||||
import { Drafty } from 'tinode-sdk';
|
||||
|
||||
import AudioPlayer from '../widgets/audio-player.jsx'
|
||||
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 { base64ToBlob, blobToBase64, fitImageSize, imageScaled } from './blob-helpers.js';
|
||||
import { idToColorClass, shortenFileName } from './strformat.js';
|
||||
import { cancelablePromise, sanitizeImageUrl } from './utils.js';
|
||||
import { idToColorClass, secondsToTime, shortenFileName } from './strformat.js';
|
||||
import { cancelablePromise, sanitizeUrlForMime } from './utils.js';
|
||||
|
||||
const messages = defineMessages({
|
||||
drafty_form: {
|
||||
id: 'drafty_form',
|
||||
defaultMessage: 'Form: ',
|
||||
description: 'Comment for form in drafty preview'
|
||||
description: 'Comment for form in drafty'
|
||||
},
|
||||
drafty_attachment: {
|
||||
id: 'drafty_attachment',
|
||||
defaultMessage: 'Attachment',
|
||||
description: 'Comment for attachment in drafty preview'
|
||||
description: 'Comment for attachment in drafty'
|
||||
},
|
||||
drafty_image: {
|
||||
id: 'drafty_image',
|
||||
defaultMessage: 'Picture',
|
||||
description: 'Comment for embedded images in drafty preview'
|
||||
description: 'Comment for embedded images in drafty'
|
||||
},
|
||||
drafty_unknown: {
|
||||
id: 'drafty_unknown',
|
||||
defaultMessage: 'Unsupported',
|
||||
description: 'Unsupported entity in drafty'
|
||||
}
|
||||
});
|
||||
|
||||
// Size the already scaled image.
|
||||
// Additional processing of image data.
|
||||
function handleImageData(el, data, attr) {
|
||||
if (!data) {
|
||||
attr.src = 'img/broken_image.png';
|
||||
@@ -53,7 +59,7 @@ function handleImageData(el, data, attr) {
|
||||
minHeight: dim.dstHeight + 'px'
|
||||
};
|
||||
if (!Drafty.isProcessing(data)) {
|
||||
attr.src = this.authorizeURL(sanitizeImageUrl(attr.src));
|
||||
attr.src = this.authorizeURL(sanitizeUrlForMime(attr.src, 'image'));
|
||||
attr.alt = data.name;
|
||||
if (attr.src) {
|
||||
if (Math.max(data.width || 0, data.height || 0) > IMAGE_THUMBNAIL_DIM) {
|
||||
@@ -73,50 +79,6 @@ function handleImageData(el, data, attr) {
|
||||
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, 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.
|
||||
// 'this' must contain:
|
||||
// viewportWidth: this.props.viewportWidth;
|
||||
@@ -135,12 +97,27 @@ export function fullFormatter(style, data, values, key, stack) {
|
||||
}
|
||||
|
||||
let el = Drafty.tagName(style);
|
||||
const attr = Drafty.attrValue(style, data) || {};
|
||||
let attr = Drafty.attrValue(style, data) || {};
|
||||
attr.key = key;
|
||||
switch (style) {
|
||||
case 'AU':
|
||||
// Show audio player.
|
||||
if (attr.src) {
|
||||
attr.src = this.authorizeURL(sanitizeUrlForMime(attr.src, 'audio'));
|
||||
attr.duration = data.duration > 0 ? (data.duration | 0) : undefined;
|
||||
attr.preview = data.preview;
|
||||
attr.loading = 'lazy';
|
||||
}
|
||||
el = AudioPlayer;
|
||||
// Audio element cannot have content.
|
||||
values = null;
|
||||
break;
|
||||
case 'BR':
|
||||
values = null;
|
||||
break;
|
||||
case 'EX':
|
||||
// Ignore.
|
||||
break;
|
||||
case 'HL':
|
||||
// Highlighted text. Assign class name.
|
||||
attr.className = 'highlight';
|
||||
@@ -187,10 +164,16 @@ export function fullFormatter(style, data, values, key, stack) {
|
||||
attr.onClick = this.onQuoteClick;
|
||||
break;
|
||||
default:
|
||||
if (el == '_UNKN') {
|
||||
if (!el) {
|
||||
// Unknown element.
|
||||
el = React.Fragment;
|
||||
values = [<i className="material-icons gray">extension</i>, ' '].concat(values || []);
|
||||
attr = {key: key};
|
||||
// Generate comment for unknown element.
|
||||
let body = values;
|
||||
if (!Array.isArray(values) || !values.join('').trim()) {
|
||||
body = [<span key="x1" className="gray">{this.formatMessage(messages.drafty_unknown)}</span>];
|
||||
}
|
||||
values = [<i key="x0" className="material-icons gray">extension</i>, ' '].concat(body);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -213,6 +196,11 @@ export function previewFormatter(style, data, values, key) {
|
||||
let el = Drafty.tagName(style);
|
||||
const attr = { key: key };
|
||||
switch (style) {
|
||||
case 'AU':
|
||||
// Voicemail as '[mic] 0:00'.
|
||||
el = React.Fragment;
|
||||
values = [<i key="au" className="material-icons">mic</i>, ' ', secondsToTime(data.duration/1000)];
|
||||
break;
|
||||
case 'BR':
|
||||
// Replace new line with a space.
|
||||
el = React.Fragment;
|
||||
@@ -230,7 +218,7 @@ export function previewFormatter(style, data, values, key) {
|
||||
case 'IM':
|
||||
// Replace image with '[icon] Image'.
|
||||
el = React.Fragment;
|
||||
values = [<i key="im" className="material-icons">photo</i>, this.formatMessage(messages.drafty_image)];
|
||||
values = [<i key="im" className="material-icons">photo</i>, ' ', this.formatMessage(messages.drafty_image)];
|
||||
break;
|
||||
case 'BN':
|
||||
el = 'span';
|
||||
@@ -255,7 +243,7 @@ export function previewFormatter(style, data, values, key) {
|
||||
delete data.ref;
|
||||
}
|
||||
el = React.Fragment;
|
||||
values = [<i key="ex" className="material-icons">attachment</i>, this.formatMessage(messages.drafty_attachment)];
|
||||
values = [<i key="ex" className="material-icons">attachment</i>, ' ', this.formatMessage(messages.drafty_attachment)];
|
||||
break;
|
||||
case 'QQ':
|
||||
case 'HD':
|
||||
@@ -263,9 +251,10 @@ export function previewFormatter(style, data, values, key) {
|
||||
values = null;
|
||||
break;
|
||||
default:
|
||||
if (el == '_UNKN') {
|
||||
if (!el) {
|
||||
// Unknown element.
|
||||
el = React.Fragment;
|
||||
values = [<i key="unkn" className="material-icons">extension</i>, ' '].concat(values || []);
|
||||
values = [<i key="x0" className="material-icons gray">extension</i>, ' ', this.formatMessage(messages.drafty_unknown)];
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -354,7 +343,7 @@ function quoteImage(data) {
|
||||
}
|
||||
promise = Promise.resolve(blob);
|
||||
} else if (data.ref) {
|
||||
promise = fetch(this.authorizeURL(sanitizeImageUrl(data.ref)))
|
||||
promise = fetch(this.authorizeURL(sanitizeUrlForMime(data.ref, 'image')))
|
||||
.then(evt => {
|
||||
if (evt.ok) {
|
||||
return evt.blob();
|
||||
|
||||
@@ -17,9 +17,13 @@ export function shortDateFormat(then, locale) {
|
||||
}
|
||||
|
||||
// Convert seconds to minutes:seconds, i.e. 156 sec -> 2:36.
|
||||
export function secondsToTime(seconds) {
|
||||
const min = Math.floor(seconds / 60);
|
||||
let sec = seconds % 60;
|
||||
// If <code>fixedMins</code> is true, then minutes are represented by at least two digits.
|
||||
export function secondsToTime(seconds, fixedMin) {
|
||||
let min = Math.floor(seconds / 60) | 0;
|
||||
if (fixedMin) {
|
||||
min = min < 10 ? `0${min}` : min;
|
||||
}
|
||||
let sec = (seconds % 60) | 0;
|
||||
sec = sec < 10 ? `0${sec}` : sec;
|
||||
return `${min}:${sec}`;
|
||||
}
|
||||
|
||||
+7
-6
@@ -1,6 +1,6 @@
|
||||
// Odds and ends
|
||||
|
||||
import Tinode from 'tinode-sdk';
|
||||
import { Tinode } from 'tinode-sdk';
|
||||
|
||||
// Make shortcut icon appear with a green dot + show unread count in title.
|
||||
export function updateFavicon(count) {
|
||||
@@ -144,10 +144,10 @@ export function sanitizeUrl(url, allowedSchemes) {
|
||||
return url;
|
||||
}
|
||||
|
||||
// Ensure URL is suitable for <img src="url"> field: the URL must be a relative URL or
|
||||
// Ensure URL is suitable as a source like <img src="url"> field: the URL must be a relative URL or
|
||||
// have http:, https:, blob: or data: scheme.
|
||||
// In case of data: scheme, the URL must start with a 'data:image/XXXX;base64,'.
|
||||
export function sanitizeImageUrl(url) {
|
||||
// In case of data: scheme, the URL must must be of the right MIME type such as 'data:{mimeMajor}/XXXX;base64,'.
|
||||
export function sanitizeUrlForMime(url, mimeMajor) {
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
@@ -157,8 +157,9 @@ export function sanitizeImageUrl(url) {
|
||||
return sanitizedUrl;
|
||||
}
|
||||
|
||||
// Is this a data: URL of an image?
|
||||
if (/data:image\/[a-z0-9.-]+;base64,/i.test(url.trim())) {
|
||||
// Is this a data: URL of the appropriate mime type?
|
||||
const re = new RegExp(`data:${mimeMajor}\/[a-z0-9.-]+;base64,`, 'i');
|
||||
if (re.test(url.trim())) {
|
||||
return url;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user