matrix-art

An image gallery for Matrix
git clone git://archive.git.mtrnord.blog/MTRNord/matrix-art.git
Log | Files | Refs | README | LICENSE

commit 0916be7580577e28c466c9f5e01a98adb276e059
parent 02b40bb7f6f09b51c654d903f0e42427eb4f4f3e
Author: MTRNord <mtrnord1@gmail.com>
Date:   Fri, 14 Jan 2022 01:46:29 +0100

Add blurhash support

Diffstat:
Mcomponents/FrontPageImage.tsx | 21+++++++++++++++++----
Mhelpers/event_types.ts | 1+
Mpackage-lock.json | 28++++++++++++++++++++++++++++
Mpackage.json | 1+
Mpages/post/[id].tsx | 33+++++++++++++++++++++++++++++++--
5 files changed, 78 insertions(+), 6 deletions(-)

diff --git a/components/FrontPageImage.tsx b/components/FrontPageImage.tsx @@ -1,5 +1,6 @@ import Link from "next/link"; import { PureComponent } from "react"; +import { Blurhash } from "react-blurhash"; import { ImageEvent, ImageGalleryEvent, MatrixEventBase, MatrixImageEvents } from "../helpers/event_types"; import { constMatrixArtServer } from "../helpers/matrix_client"; import { ClientContext } from "./ClientContext"; @@ -83,7 +84,7 @@ export default class FrontPageImage extends PureComponent<Props, State> { caption_text = (caption[0] as { body: string; mimetype: string; }).body; } return event.content['m.image_gallery'].map(image => { - return this.render_image_box(image['m.thumbnail'][0].url, event.event_id + image['m.file'].url, event.event_id, caption_text); + return this.render_image_box(image['m.thumbnail'][0].url, event.event_id + image['m.file'].url, event.event_id, caption_text, image["xyz.amorgan.blurhash"]); }); } @@ -97,18 +98,30 @@ export default class FrontPageImage extends PureComponent<Props, State> { if (caption.length > 0) { caption_text = (caption[0] as { body: string; mimetype: string; }).body; } - return this.render_image_box(event.content['m.thumbnail'][0].url, event.event_id, event.event_id, caption_text); + return this.render_image_box(event.content['m.thumbnail'][0].url, event.event_id, event.event_id, caption_text, event.content["xyz.amorgan.blurhash"]); } - render_image_box(thumbnail_url: string, id: string, post_id: string, caption: string) { + render_image_box(thumbnail_url: string, id: string, post_id: string, caption: string, blurhash?: string) { // TODO show creators display name instead of mxid and show avatar image // TODO proper alt text const direct_link = `/post/${encodeURIComponent(post_id)}`; + const image = blurhash ? ( + <div className="flex"> + <Blurhash + hash={blurhash} + height={this.state.imageHeight} + width="100%" + /> + <img loading="lazy" alt={caption} title={caption} style={{ height: this.state.imageHeight }} className="relative -ml-[100%] max-w-full object-cover align-bottom" src={this.context.client?.thumbnailLink(thumbnail_url, "scale", Number.parseInt(this.state.imageHeight?.replace("px", "")!), Number.parseInt(this.state.imageHeight?.replace("px", "")!))}></img> + </div> + ) : ( + <img alt={caption} title={caption} style={{ height: this.state.imageHeight }} className={`relative max-w-full object-cover align-bottom z-0`} src={this.context.client?.thumbnailLink(thumbnail_url, "scale", Number.parseInt(this.state.imageHeight?.replace("px", "")!), Number.parseInt(this.state.imageHeight?.replace("px", "")!))}></img> + ); return ( <li style={{ height: this.state.imageHeight }} key={id}> <Link href={direct_link} passHref> <div style={{ height: this.state.imageHeight }} className={`relative cursor-pointer`}> - <img alt={caption} title={caption} style={{ height: this.state.imageHeight }} className={`relative max-w-full object-cover align-bottom z-0`} src={this.context.client?.thumbnailLink(thumbnail_url, "scale", Number.parseInt(this.state.imageHeight?.replace("px", "")!), Number.parseInt(this.state.imageHeight?.replace("px", "")!))}></img> + {image} <div style={{ height: this.state.imageHeight }} className={`flex-col max-w-full opacity-0 hover:opacity-100 duration-300 absolute bg-gradient-to-b from-transparent to-black/[.25] inset-0 z-10 flex justify-end items-start text-white p-4`}> <h2 className='truncate max-w-full text-base font-semibold'>{caption}</h2> <p className='truncate max-w-full text-sm'>{this.state.displayname}</p> diff --git a/helpers/event_types.ts b/helpers/event_types.ts @@ -31,6 +31,7 @@ export type ImageEventContent = { "m.image": ImageFields; "m.thumbnail": ThumbnailFileEvent[]; "matrixart.tags": string[]; + "xyz.amorgan.blurhash": string; }; export type MessageAlike = { "m.text": string; } | { body: string; mimetype: string; }; diff --git a/package-lock.json b/package-lock.json @@ -13,6 +13,7 @@ "node-localstorage": "^2.2.1", "pouchdb": "^7.2.2", "react": "17.0.2", + "react-blurhash": "^0.1.3", "react-dom": "17.0.2" }, "devDependencies": { @@ -2680,6 +2681,12 @@ "node": ">=8" } }, + "node_modules/blurhash": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/blurhash/-/blurhash-1.1.4.tgz", + "integrity": "sha512-MXIPz6zwYUKayju+Uidf83KhH0vodZfeRl6Ich8Gu+KGl0JgKiFq9LsfqV7cVU5fKD/AotmduZqvOfrGKOfTaA==", + "peer": true + }, "node_modules/bn.js": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", @@ -7211,6 +7218,15 @@ "node": ">=0.10.0" } }, + "node_modules/react-blurhash": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/react-blurhash/-/react-blurhash-0.1.3.tgz", + "integrity": "sha512-Q9lqbXg92NU6/2DoIl/cBM8YWL+Z4X66OiG4aT9ozOgjBwx104LHFCH5stf6aF+s0Q9Wf310Ul+dG+VXJltmPg==", + "peerDependencies": { + "blurhash": "^1.1.1", + "react": ">=15" + } + }, "node_modules/react-dom": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", @@ -10534,6 +10550,12 @@ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" }, + "blurhash": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/blurhash/-/blurhash-1.1.4.tgz", + "integrity": "sha512-MXIPz6zwYUKayju+Uidf83KhH0vodZfeRl6Ich8Gu+KGl0JgKiFq9LsfqV7cVU5fKD/AotmduZqvOfrGKOfTaA==", + "peer": true + }, "bn.js": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", @@ -13964,6 +13986,12 @@ "object-assign": "^4.1.1" } }, + "react-blurhash": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/react-blurhash/-/react-blurhash-0.1.3.tgz", + "integrity": "sha512-Q9lqbXg92NU6/2DoIl/cBM8YWL+Z4X66OiG4aT9ozOgjBwx104LHFCH5stf6aF+s0Q9Wf310Ul+dG+VXJltmPg==", + "requires": {} + }, "react-dom": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", diff --git a/package.json b/package.json @@ -18,6 +18,7 @@ "node-localstorage": "^2.2.1", "pouchdb": "^7.2.2", "react": "17.0.2", + "react-blurhash": "^0.1.3", "react-dom": "17.0.2" }, "devDependencies": { diff --git a/pages/post/[id].tsx b/pages/post/[id].tsx @@ -16,6 +16,7 @@ import { get_data } from "../api/directory"; import { isImageEvent, isImageGalleryEvent } from '../../components/FrontPageImage'; import Link from 'next/link'; import Footer from '../../components/Footer'; +import { Blurhash } from 'react-blurhash'; type Props = InferGetServerSidePropsType<typeof getServerSideProps> & { router: NextRouter; @@ -244,6 +245,20 @@ class Post extends PureComponent<Props, State> { // TODO get this from the event itself license: "https://creativecommons.org/licenses/by-nc-nd/4.0/" }; + const blurhash = imageEvent.content['xyz.amorgan.blurhash']; + const image_html = blurhash ? ( + <div className="flex"> + <Blurhash + className="max-w-full lg:max-w-3xl max-h-[54.25rem]" + hash={blurhash} + width="100%" + height="" + /> + <img loading="lazy" alt={caption} title={caption} className="relative -ml-[100%] object-cover align-bottom" src={thumbnail_url} /> + </div> + ) : ( + <img alt={caption} title={caption} src={thumbnail_url} /> + ); return ( <> <Head> @@ -266,7 +281,7 @@ class Post extends PureComponent<Props, State> { thumbnail={true} > <a href={url} title={caption} data-src={url}> - <img alt={caption} title={caption} src={thumbnail_url} /> + {image_html} </a> </LightGallery> </div> @@ -289,9 +304,23 @@ class Post extends PureComponent<Props, State> { // TODO get this from the event itself license: "https://creativecommons.org/licenses/by-nc-nd/4.0/" }); + const blurhash = image["xyz.amorgan.blurhash"]; + const image_html = blurhash ? ( + <div className="flex"> + <Blurhash + className="max-w-full lg:max-w-3xl max-h-[54.25rem]" + hash={blurhash} + width="100%" + height="" + /> + <img loading="lazy" alt={caption} title={caption} className="relative -ml-[100%] object-cover align-bottom" src={thumbnail_url} /> + </div> + ) : ( + <img alt={caption} title={caption} src={thumbnail_url} /> + ); return ( <a key={url} href={url} title={caption} data-src={url}> - <img alt={caption} title={caption} src={thumbnail_url} /> + {image_html} </a> ); });