Skip to content

Commit

Permalink
Blurhash support (#701)
Browse files Browse the repository at this point in the history
* Generate blurhash client side

* Make blurhash generation faster

* Simple blurhash display support

* Make image display simpler

* Support non square images

* Don't attach video blurhash to thumbnail

* Add video display support

* Ignore alt tag missing warning

Co-authored-by: Ajay Bura <[email protected]>
  • Loading branch information
ginnyTheCat and ajbura authored Aug 6, 2022
1 parent edace32 commit 04f910e
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 29 deletions.
46 changes: 39 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.8.tgz",
"@tippyjs/react": "^4.2.6",
"babel-polyfill": "^6.26.0",
"blurhash": "^1.1.5",
"browser-encrypt-attachment": "^0.3.0",
"dateformat": "^5.0.3",
"emojibase-data": "^7.0.1",
Expand All @@ -40,6 +41,7 @@
"prop-types": "^15.8.1",
"react": "^17.0.2",
"react-autosize-textarea": "^7.1.0",
"react-blurhash": "^0.1.3",
"react-dnd": "^15.1.2",
"react-dnd-html5-backend": "^15.1.3",
"react-dom": "^17.0.2",
Expand Down
24 changes: 17 additions & 7 deletions src/app/molecules/media/Media.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import './Media.scss';

import encrypt from 'browser-encrypt-attachment';

import { BlurhashCanvas } from 'react-blurhash';
import Text from '../../atoms/text/Text';
import IconButton from '../../atoms/button/IconButton';
import Spinner from '../../atoms/spinner/Spinner';
Expand Down Expand Up @@ -154,7 +155,7 @@ File.propTypes = {
};

function Image({
name, width, height, link, file, type,
name, width, height, link, file, type, blurhash,
}) {
const [url, setUrl] = useState(null);

Expand All @@ -175,6 +176,7 @@ function Image({
<div className="file-container">
<FileHeader name={name} link={url || link} type={type} external />
<div style={{ height: width !== null ? getNativeHeight(width, height) : 'unset' }} className="image-container">
{ blurhash && <BlurhashCanvas hash={blurhash} punch={1} />}
{ url !== null && <img src={url || link} alt={name} />}
</div>
</div>
Expand All @@ -185,6 +187,7 @@ Image.defaultProps = {
width: null,
height: null,
type: '',
blurhash: '',
};
Image.propTypes = {
name: PropTypes.string.isRequired,
Expand All @@ -193,6 +196,7 @@ Image.propTypes = {
link: PropTypes.string.isRequired,
file: PropTypes.shape({}),
type: PropTypes.string,
blurhash: PropTypes.string,
};

function Sticker({
Expand Down Expand Up @@ -278,8 +282,8 @@ Audio.propTypes = {
};

function Video({
name, link, thumbnail,
width, height, file, type, thumbnailFile, thumbnailType,
name, link, thumbnail, thumbnailFile, thumbnailType,
width, height, file, type, blurhash,
}) {
const [isLoading, setIsLoading] = useState(false);
const [url, setUrl] = useState(null);
Expand Down Expand Up @@ -315,10 +319,14 @@ function Video({
<div
style={{
height: width !== null ? getNativeHeight(width, height) : 'unset',
backgroundImage: thumbUrl === null ? 'none' : `url(${thumbUrl}`,
}}
className="video-container"
>
{ url === null && blurhash && <BlurhashCanvas hash={blurhash} punch={1} />}
{ url === null && thumbUrl !== null && (
/* eslint-disable-next-line jsx-a11y/alt-text */
<img src={thumbUrl} />
)}
{ url === null && isLoading && <Spinner size="small" /> }
{ url === null && !isLoading && <IconButton onClick={handlePlayVideo} tooltip="Play video" src={PlaySVG} />}
{ url !== null && (
Expand All @@ -336,20 +344,22 @@ Video.defaultProps = {
height: null,
file: null,
thumbnail: null,
type: '',
thumbnailType: null,
thumbnailFile: null,
type: '',
blurhash: null,
};
Video.propTypes = {
name: PropTypes.string.isRequired,
link: PropTypes.string.isRequired,
thumbnail: PropTypes.string,
thumbnailFile: PropTypes.shape({}),
thumbnailType: PropTypes.string,
width: PropTypes.number,
height: PropTypes.number,
file: PropTypes.shape({}),
type: PropTypes.string,
thumbnailFile: PropTypes.shape({}),
thumbnailType: PropTypes.string,
blurhash: PropTypes.string,
};

export {
Expand Down
31 changes: 19 additions & 12 deletions src/app/molecules/media/Media.scss
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
font-size: 0;
line-height: 0;

position: relative;

display: flex;
justify-content: center;
align-items: center;
Expand All @@ -42,34 +44,39 @@
background-size: cover;
}

.sticker-container {
display: inline-flex;
max-width: 128px;
width: 100%;
& img {
.image-container,
.video-container {
& img,
& canvas {
position: absolute;
max-width: unset !important;
width: 100% !important;
height: 100%;
border-radius: 0 !important;
margin: 0 !important;
}
}

.image-container {
.sticker-container {
display: inline-flex;
max-width: 128px;
width: 100%;
& img {
max-width: unset !important;
width: 100% !important;
border-radius: 0 !important;
margin: 0 !important;
}
}

.video-container {
& .ic-btn-surface {
background-color: var(--bg-surface-low);
position: absolute;
}
video {
width: 100%
width: 100%;
}
}
.audio-container {
audio {
width: 100%
width: 100%;
}
}
}
4 changes: 4 additions & 0 deletions src/app/molecules/message/Message.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,8 @@ function genMediaContent(mE) {
let msgType = mE.getContent()?.msgtype;
if (mE.getType() === 'm.sticker') msgType = 'm.sticker';

const blurhash = mContent?.info?.['xyz.amorgan.blurhash'];

switch (msgType) {
case 'm.file':
return (
Expand All @@ -629,6 +631,7 @@ function genMediaContent(mE) {
link={mx.mxcUrlToHttp(mediaMXC)}
file={isEncryptedFile ? mContent.file : null}
type={mContent.info?.mimetype}
blurhash={blurhash}
/>
);
case 'm.sticker':
Expand Down Expand Up @@ -666,6 +669,7 @@ function genMediaContent(mE) {
height={typeof mContent.info?.h === 'number' ? mContent.info?.h : null}
file={isEncryptedFile ? mContent.file : null}
type={mContent.info?.mimetype}
blurhash={blurhash}
/>
);
default:
Expand Down
32 changes: 29 additions & 3 deletions src/client/state/RoomsInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,34 @@ import { micromark } from 'micromark';
import { gfm, gfmHtml } from 'micromark-extension-gfm';
import encrypt from 'browser-encrypt-attachment';
import { math } from 'micromark-extension-math';
import { encode } from 'blurhash';
import { getShortcodeToEmoji } from '../../app/organisms/emoji-board/custom-emoji';
import { mathExtensionHtml, spoilerExtension, spoilerExtensionHtml } from '../../util/markdown';
import { getImageDimension } from '../../util/common';
import cons from './cons';
import settings from './settings';

const blurhashField = 'xyz.amorgan.blurhash';

function encodeBlurhash(img) {
const canvas = document.createElement('canvas');
canvas.width = 100;
canvas.height = 100;
const context = canvas.getContext('2d');
context.drawImage(img, 0, 0, canvas.width, canvas.height);
const data = context.getImageData(0, 0, canvas.width, canvas.height);
return encode(data.data, data.width, data.height, 4, 4);
}

function loadImage(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = (err) => reject(err);
img.src = url;
});
}

function loadVideo(videoFile) {
return new Promise((resolve, reject) => {
const video = document.createElement('video');
Expand Down Expand Up @@ -300,10 +322,11 @@ class RoomsInput extends EventEmitter {
let uploadData = null;

if (fileType === 'image') {
const imgDimension = await getImageDimension(file);
const img = await loadImage(URL.createObjectURL(file));

info.w = imgDimension.w;
info.h = imgDimension.h;
info.w = img.width;
info.h = img.height;
info[blurhashField] = encodeBlurhash(img);

content.msgtype = 'm.image';
content.body = file.name || 'Image';
Expand All @@ -313,8 +336,11 @@ class RoomsInput extends EventEmitter {

try {
const video = await loadVideo(file);

info.w = video.videoWidth;
info.h = video.videoHeight;
info[blurhashField] = encodeBlurhash(video);

const thumbnailData = await getVideoThumbnail(video, video.videoWidth, video.videoHeight, 'image/jpeg');
const thumbnailUploadData = await this.uploadFile(roomId, thumbnailData.thumbnail);
info.thumbnail_info = thumbnailData.info;
Expand Down

0 comments on commit 04f910e

Please sign in to comment.