mirror of
https://github.com/element-hq/element-web
synced 2024-11-26 19:26:04 +03:00
Add support for blurhash (MSC2448)
MSC: https://github.com/matrix-org/matrix-doc/pull/2448 While the image loads, we can show a blurred version of it (calculated at upload time) so we don't have a blank space in the timeline.
This commit is contained in:
parent
be2d0c9de7
commit
53db386731
5 changed files with 81 additions and 9 deletions
|
@ -56,6 +56,7 @@
|
||||||
"@babel/runtime": "^7.10.5",
|
"@babel/runtime": "^7.10.5",
|
||||||
"await-lock": "^2.0.1",
|
"await-lock": "^2.0.1",
|
||||||
"blueimp-canvas-to-blob": "^3.27.0",
|
"blueimp-canvas-to-blob": "^3.27.0",
|
||||||
|
"blurhash": "^1.1.3",
|
||||||
"browser-encrypt-attachment": "^0.3.0",
|
"browser-encrypt-attachment": "^0.3.0",
|
||||||
"browser-request": "^0.3.3",
|
"browser-request": "^0.3.3",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
|
|
|
@ -334,6 +334,7 @@ function uploadFile(matrixClient: MatrixClient, roomId: string, file: File | Blo
|
||||||
if (file.type) {
|
if (file.type) {
|
||||||
encryptInfo.mimetype = file.type;
|
encryptInfo.mimetype = file.type;
|
||||||
}
|
}
|
||||||
|
// TODO: Blurhash for encrypted media?
|
||||||
return {"file": encryptInfo};
|
return {"file": encryptInfo};
|
||||||
});
|
});
|
||||||
(prom as IAbortablePromise<any>).abort = () => {
|
(prom as IAbortablePromise<any>).abort = () => {
|
||||||
|
@ -344,11 +345,15 @@ function uploadFile(matrixClient: MatrixClient, roomId: string, file: File | Blo
|
||||||
} else {
|
} else {
|
||||||
const basePromise = matrixClient.uploadContent(file, {
|
const basePromise = matrixClient.uploadContent(file, {
|
||||||
progressHandler: progressHandler,
|
progressHandler: progressHandler,
|
||||||
|
onlyContentUri: false,
|
||||||
});
|
});
|
||||||
const promise1 = basePromise.then(function(url) {
|
const promise1 = basePromise.then(function(body) {
|
||||||
if (canceled) throw new UploadCanceledError();
|
if (canceled) throw new UploadCanceledError();
|
||||||
// If the attachment isn't encrypted then include the URL directly.
|
// If the attachment isn't encrypted then include the URL directly.
|
||||||
return {"url": url};
|
return {
|
||||||
|
"url": body.content_uri,
|
||||||
|
"blurhash": body["xyz.amorgan.blurhash"], // TODO: Use `body.blurhash` when MSC2448 lands
|
||||||
|
};
|
||||||
});
|
});
|
||||||
promise1.abort = () => {
|
promise1.abort = () => {
|
||||||
canceled = true;
|
canceled = true;
|
||||||
|
@ -550,6 +555,7 @@ export default class ContentMessages {
|
||||||
return upload.promise.then(function(result) {
|
return upload.promise.then(function(result) {
|
||||||
content.file = result.file;
|
content.file = result.file;
|
||||||
content.url = result.url;
|
content.url = result.url;
|
||||||
|
content.info['xyz.amorgan.blurhash'] = result.blurhash; // TODO: Use `blurhash` when MSC2448 lands
|
||||||
});
|
});
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
// Await previous message being sent into the room
|
// Await previous message being sent into the room
|
||||||
|
|
56
src/components/views/elements/BlurhashPlaceholder.tsx
Normal file
56
src/components/views/elements/BlurhashPlaceholder.tsx
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {decode} from "blurhash";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
blurhash: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class BlurhashPlaceholder extends React.PureComponent<IProps> {
|
||||||
|
private canvas: React.RefObject<HTMLCanvasElement> = React.createRef();
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
this.draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidUpdate() {
|
||||||
|
this.draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
private draw() {
|
||||||
|
if (!this.canvas.current) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {width, height} = this.props;
|
||||||
|
|
||||||
|
const pixels = decode(this.props.blurhash, Math.ceil(width), Math.ceil(height));
|
||||||
|
const ctx = this.canvas.current.getContext("2d");
|
||||||
|
const imgData = ctx.createImageData(width, height);
|
||||||
|
imgData.data.set(pixels);
|
||||||
|
ctx.putImageData(imgData, 0, 0);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error rendering blurhash: ", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return <canvas height={this.props.height} width={this.props.width} ref={this.canvas} />;
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,6 +27,7 @@ import { _t } from '../../../languageHandler';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import InlineSpinner from '../elements/InlineSpinner';
|
import InlineSpinner from '../elements/InlineSpinner';
|
||||||
|
import BlurhashPlaceholder from "../elements/BlurhashPlaceholder";
|
||||||
|
|
||||||
export default class MImageBody extends React.Component {
|
export default class MImageBody extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -53,6 +54,8 @@ export default class MImageBody extends React.Component {
|
||||||
this.onClick = this.onClick.bind(this);
|
this.onClick = this.onClick.bind(this);
|
||||||
this._isGif = this._isGif.bind(this);
|
this._isGif = this._isGif.bind(this);
|
||||||
|
|
||||||
|
const imageInfo = this.props.mxEvent.getContent().info;
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
decryptedUrl: null,
|
decryptedUrl: null,
|
||||||
decryptedThumbnailUrl: null,
|
decryptedThumbnailUrl: null,
|
||||||
|
@ -63,6 +66,7 @@ export default class MImageBody extends React.Component {
|
||||||
loadedImageDimensions: null,
|
loadedImageDimensions: null,
|
||||||
hover: false,
|
hover: false,
|
||||||
showImage: SettingsStore.getValue("showImages"),
|
showImage: SettingsStore.getValue("showImages"),
|
||||||
|
blurhash: imageInfo ? imageInfo['xyz.amorgan.blurhash'] : null, // TODO: Use `blurhash` when MSC2448 lands.
|
||||||
};
|
};
|
||||||
|
|
||||||
this._image = createRef();
|
this._image = createRef();
|
||||||
|
@ -329,7 +333,8 @@ export default class MImageBody extends React.Component {
|
||||||
infoWidth = content.info.w;
|
infoWidth = content.info.w;
|
||||||
infoHeight = content.info.h;
|
infoHeight = content.info.h;
|
||||||
} else {
|
} else {
|
||||||
// Whilst the image loads, display nothing.
|
// Whilst the image loads, display nothing. We also don't display a blurhash image
|
||||||
|
// because we don't really know what size of image we'll end up with.
|
||||||
//
|
//
|
||||||
// Once loaded, use the loaded image dimensions stored in `loadedImageDimensions`.
|
// Once loaded, use the loaded image dimensions stored in `loadedImageDimensions`.
|
||||||
//
|
//
|
||||||
|
@ -368,8 +373,7 @@ export default class MImageBody extends React.Component {
|
||||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||||
placeholder = <InlineSpinner w={32} h={32} />;
|
placeholder = <InlineSpinner w={32} h={32} />;
|
||||||
} else if (!this.state.imgLoaded) {
|
} else if (!this.state.imgLoaded) {
|
||||||
// Deliberately, getSpinner is left unimplemented here, MStickerBody overides
|
placeholder = this.getPlaceholder(maxWidth, maxHeight);
|
||||||
placeholder = this.getPlaceholder();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let showPlaceholder = Boolean(placeholder);
|
let showPlaceholder = Boolean(placeholder);
|
||||||
|
@ -391,7 +395,7 @@ export default class MImageBody extends React.Component {
|
||||||
|
|
||||||
if (!this.state.showImage) {
|
if (!this.state.showImage) {
|
||||||
img = <HiddenImagePlaceholder style={{ maxWidth: maxWidth + "px" }} />;
|
img = <HiddenImagePlaceholder style={{ maxWidth: maxWidth + "px" }} />;
|
||||||
showPlaceholder = false; // because we're hiding the image, so don't show the sticker icon.
|
showPlaceholder = false; // because we're hiding the image, so don't show the placeholder.
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._isGif() && !SettingsStore.getValue("autoplayGifsAndVideos") && !this.state.hover) {
|
if (this._isGif() && !SettingsStore.getValue("autoplayGifsAndVideos") && !this.state.hover) {
|
||||||
|
@ -433,9 +437,9 @@ export default class MImageBody extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overidden by MStickerBody
|
// Overidden by MStickerBody
|
||||||
getPlaceholder() {
|
getPlaceholder(width, height) {
|
||||||
// MImageBody doesn't show a placeholder whilst the image loads, (but it could do)
|
if (!this.state.blurhash) return null;
|
||||||
return null;
|
return <BlurhashPlaceholder blurhash={this.state.blurhash} width={width} height={height} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overidden by MStickerBody
|
// Overidden by MStickerBody
|
||||||
|
|
|
@ -2493,6 +2493,11 @@ blueimp-canvas-to-blob@^3.27.0:
|
||||||
resolved "https://registry.yarnpkg.com/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.27.0.tgz#a2bd5c43587b95dedf0f6998603452d1bfcc9b9e"
|
resolved "https://registry.yarnpkg.com/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.27.0.tgz#a2bd5c43587b95dedf0f6998603452d1bfcc9b9e"
|
||||||
integrity sha512-AcIj+hCw6WquxzJuzC6KzgYmqxLFeTWe88KuY2BEIsW1zbEOfoinDAGlhyvFNGt+U3JElkVSK7anA1FaSdmmfA==
|
integrity sha512-AcIj+hCw6WquxzJuzC6KzgYmqxLFeTWe88KuY2BEIsW1zbEOfoinDAGlhyvFNGt+U3JElkVSK7anA1FaSdmmfA==
|
||||||
|
|
||||||
|
blurhash@^1.1.3:
|
||||||
|
version "1.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-1.1.3.tgz#dc325af7da836d07a0861d830bdd63694382483e"
|
||||||
|
integrity sha512-yUhPJvXexbqbyijCIE/T2NCXcj9iNPhWmOKbPTuR/cm7Q5snXYIfnVnz6m7MWOXxODMz/Cr3UcVkRdHiuDVRDw==
|
||||||
|
|
||||||
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0:
|
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0:
|
||||||
version "4.11.9"
|
version "4.11.9"
|
||||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828"
|
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828"
|
||||||
|
|
Loading…
Reference in a new issue