commit e7daa2a0cecae6dfaf019316faec9701de3da7ed
parent 15f27a3d5655eb8308875a83d522d58c3843c919
Author: MTRNord <mtrnord1@gmail.com>
Date: Fri, 28 Jan 2022 00:10:20 +0100
Fix image uploading with loading animation
Diffstat:
6 files changed, 132 insertions(+), 24 deletions(-)
diff --git a/components/FrontPageImage.tsx b/components/FrontPageImage.tsx
@@ -4,10 +4,12 @@ import { Blurhash } from "react-blurhash";
import { ImageEvent, ImageGalleryEvent, MatrixEventBase, MatrixImageEvents } from "../helpers/event_types";
import { constMatrixArtServer } from "../helpers/matrix_client";
import { ClientContext } from "./ClientContext";
+import PropTypes from 'prop-types';
type Props = {
event: MatrixImageEvents;
imageHeight?: string;
+ show_nsfw: boolean;
};
type State = {
@@ -28,6 +30,12 @@ export default class FrontPageImage extends PureComponent<Props, State> {
} as State;
}
+ static propTypes = {
+ event: PropTypes.object,
+ imageHeight: PropTypes.number,
+ show_nsfw: PropTypes.bool
+ };
+
async componentDidMount() {
// auto-register as a guest if not logged in
if (!this.context.client?.accessToken) {
@@ -85,8 +93,8 @@ export default class FrontPageImage extends PureComponent<Props, State> {
return (possible_html_caption.body && possible_html_caption.mimetype === "text/html") ? possible_html_caption.body : possible_text_caption["m.text"];
})[0];
return event.content['m.image_gallery'].map(image => {
- if (image["matrixart.nsfw"]) {
- return undefined;
+ if (image["matrixart.nsfw"] && !this.props.show_nsfw) {
+ return;
}
const thumbnail_url = image['m.thumbnail'] ? (image['m.thumbnail'].length > 0 ? image['m.thumbnail'][0].url : image['m.file'].url) : image['m.file'].url;
return this.render_image_box(thumbnail_url, event.event_id + image['m.file'].url, event.event_id, caption_text, image['m.image'].height, image['m.image'].width, image["xyz.amorgan.blurhash"]);
@@ -95,8 +103,8 @@ export default class FrontPageImage extends PureComponent<Props, State> {
render_image(event: ImageEvent) {
- if (event.content["matrixart.nsfw"]) {
- return undefined;
+ if (event.content["matrixart.nsfw"] && !this.props.show_nsfw) {
+ return;
}
const caption_text = event.content['m.caption'].filter(cap => {
const possible_html_caption = (cap as { body: string; mimetype: string; });
diff --git a/components/submit_page/main.tsx b/components/submit_page/main.tsx
@@ -1,4 +1,4 @@
-import { createRef, PureComponent, RefObject } from "react";
+import { PureComponent } from "react";
import { DropCallbacks } from "./start";
import PropTypes from 'prop-types';
import { PreviewWithDataFile } from "../../pages/submit";
@@ -8,8 +8,10 @@ import { SearchMedia } from "../../pages/api/submitSearch";
import { withRouter } from "next/router";
import { WithRouterProps } from "next/dist/client/with-router";
import { BlurhashEncoder } from "../../helpers/BlurhashEncoder";
-import { ImageEventContent, MatrixContents, ThumbnailData } from "../../helpers/event_types";
+import { ImageEventContent, ThumbnailData } from "../../helpers/event_types";
import { toast } from "react-toastify";
+// @ts-ignore This has no types
+import extractPngChunks from "png-chunks-extract";
type ThumbnailableElement = HTMLImageElement | HTMLVideoElement;
type ThumbnailTransmissionData = {
@@ -18,6 +20,11 @@ type ThumbnailTransmissionData = {
};
const MAX_WIDTH = 800;
const MAX_HEIGHT = 600;
+
+// scraped out of a macOS hidpi (5660ppm) screenshot png
+// 5669 px (x-axis) , 5669 px (y-axis) , per metre
+const PHYS_HIDPI = [0x00, 0x00, 0x16, 0x25, 0x00, 0x00, 0x16, 0x25, 0x01];
+
// Minimum size for image files before we generate a thumbnail for them.
const IMAGE_SIZE_THRESHOLD_THUMBNAIL = 1 << 15; // 32KB
// Minimum size improvement for image thumbnails, if both are not met then don't bother uploading thumbnail.
@@ -36,7 +43,6 @@ type State = {
[key: string]: any;
};
class MainSubmissionForm extends PureComponent<Props, State> {
- private image_refs: RefObject<HTMLImageElement>[] = [];
declare context: React.ContextType<typeof ClientContext>;
constructor(props: Props) {
@@ -95,16 +101,12 @@ class MainSubmissionForm extends PureComponent<Props, State> {
return classes_base.join(" ");
};
- if (!this.image_refs[index]) {
- this.image_refs[index] = createRef();
- }
-
// TODO FIXME do make this work with keyboard presses!
/* eslint-disable jsx-a11y/click-events-have-key-events */
return (
<div aria-label={file.name} className={classes.bind(this)()} onClick={setIndex} style={{ height: "144px" }} key={file.name} role="radio" tabIndex={index} aria-checked={this.state.currentFileIndex == index ? true : false}>
- <img alt={file.name} ref={this.image_refs[index]} className="h-full w-full object-cover align-middle aspect-video" src={file.preview_url} />
+ <img alt={file.name} className="h-full w-full object-cover align-middle aspect-video" src={file.preview_url} />
</div>
);
/* eslint-enable jsx-a11y/click-events-have-key-events */
@@ -167,9 +169,10 @@ class MainSubmissionForm extends PureComponent<Props, State> {
}
}
+ const image_infos = await this.getImageInfos();
const ids = await this.doUpload();
- const thumbnails = await this.generateThumbnailsAndUpload();
+ const thumbnails = await this.generateThumbnailsAndUpload(image_infos);
// Handle uploads
for (const index of range) {
@@ -200,8 +203,8 @@ class MainSubmissionForm extends PureComponent<Props, State> {
size: file.size
},
"m.image": {
- height: this.image_refs[index].current?.naturalHeight!,
- width: this.image_refs[index].current?.naturalWidth!,
+ height: image_infos[index].height,
+ width: image_infos[index].width,
},
"matrixart.description": this.state[description],
"matrixart.nsfw": this.state[nsfw] === "yes" ? true : false,
@@ -231,7 +234,75 @@ class MainSubmissionForm extends PureComponent<Props, State> {
this.setState({ submit_in_process: false });
}
- // THis is aken from matrix-react-sdk commit efa1667d7e9de9e429a72396a5105d0219006db2
+ /**
+ * Read the file as an ArrayBuffer.
+ * @param {File} file The file to read
+ * @return {Promise} A promise that resolves with an ArrayBuffer when the file
+ * is read.
+ */
+ private async readFileAsArrayBuffer(file: File | Blob): Promise<ArrayBuffer> {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.addEventListener("load", (e: ProgressEvent<FileReader>) => {
+ resolve(e.target?.result as ArrayBuffer);
+ });
+ reader.addEventListener("error", (e) => {
+ reject(e);
+ });
+ reader.readAsArrayBuffer(file);
+ });
+ }
+
+ private async getImageInfos() {
+ const image_infos = [];
+ const range = [...Array(this.props.files.length).keys()]; // eslint-disable-line unicorn/new-for-builtins
+ for (const index of range) {
+ const imageFile = this.props.files[index];
+ // Load the file into an html element
+ const img = document.createElement("img");
+ const objectUrl = URL.createObjectURL(imageFile);
+ const imgPromise = new Promise((resolve, reject) => {
+ img.addEventListener("load", () => {
+ URL.revokeObjectURL(objectUrl);
+ resolve(img);
+ });
+ img.addEventListener("error", (e) => {
+ reject(e);
+ });
+ });
+ img.src = objectUrl;
+
+ // check for hi-dpi PNGs and fudge display resolution as needed.
+ // this is mainly needed for macOS screencaps
+ let parsePromise;
+ if (imageFile.type === "image/png") {
+ // in practice macOS happens to order the chunks so they fall in
+ // the first 0x1000 bytes (thanks to a massive ICC header).
+ // Thus we could slice the file down to only sniff the first 0x1000
+ // bytes (but this makes extractPngChunks choke on the corrupt file)
+ const headers = imageFile; //.slice(0, 0x1000);
+ parsePromise = this.readFileAsArrayBuffer(headers).then(arrayBuffer => {
+ const buffer = new Uint8Array(arrayBuffer);
+ const chunks = extractPngChunks(buffer);
+ for (const chunk of chunks) {
+ if (chunk.name === 'pHYs') {
+ if (chunk.data.byteLength !== PHYS_HIDPI.length) return;
+ return chunk.data.every((val: number, i: number) => val === PHYS_HIDPI[i]);
+ }
+ }
+ return false;
+ });
+ }
+
+ const [hidpi] = await Promise.all([parsePromise, imgPromise]);
+ const width = hidpi ? (img.width >> 1) : img.width;
+ const height = hidpi ? (img.height >> 1) : img.height;
+ image_infos.push({ width, height, img });
+ }
+ return image_infos;
+ }
+
+ // This is taken from matrix-react-sdk commit efa1667d7e9de9e429a72396a5105d0219006db2
private async createThumbnail(
element: ThumbnailableElement,
inputWidth: number,
@@ -300,17 +371,16 @@ class MainSubmissionForm extends PureComponent<Props, State> {
};
}
- private async generateThumbnailsAndUpload(): Promise<{ index: number; meta: ThumbnailData; }[]> {
+ private async generateThumbnailsAndUpload(image_infos: { width: number; height: number; img: HTMLImageElement; }[]): Promise<{ index: number; meta: ThumbnailData; }[]> {
const thumbnails = [];
if (!this.context.client.isGuest) {
const range = [...Array(this.props.files.length).keys()]; // eslint-disable-line unicorn/new-for-builtins
for (const index of range) {
const file = this.props.files[index];
- const image = this.image_refs[index];
const thumbnail_data = await this.createThumbnail(
- image.current!,
- image.current!.naturalWidth,
- image.current!.naturalHeight,
+ image_infos[index].img,
+ image_infos[index].width,
+ image_infos[index].height,
file.type
);
if (!thumbnail_data) {
@@ -469,7 +539,6 @@ class MainSubmissionForm extends PureComponent<Props, State> {
}
}
-
MainSubmissionForm.contextType = ClientContext;
// @ts-ignore Typescript is wrong
diff --git a/package-lock.json b/package-lock.json
@@ -13,6 +13,7 @@
"meilisearch": "0.24.0",
"next": "12.0.9",
"node-localstorage": "2.2.1",
+ "png-chunks-extract": "^1.0.0",
"prop-types": "15.8.1",
"react": "17.0.2",
"react-blurhash": "0.1.3",
@@ -2389,6 +2390,14 @@
"node": ">=10"
}
},
+ "node_modules/crc-32": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-0.3.0.tgz",
+ "integrity": "sha1-aj02h/W67EH36bmf4ZU6Ll0Zd14=",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/cross-env": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
@@ -5765,6 +5774,14 @@
"node": ">=4"
}
},
+ "node_modules/png-chunks-extract": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/png-chunks-extract/-/png-chunks-extract-1.0.0.tgz",
+ "integrity": "sha1-+tSpBeZmUhlzUcZeNbksZDEeRy0=",
+ "dependencies": {
+ "crc-32": "^0.3.0"
+ }
+ },
"node_modules/pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
@@ -9236,6 +9253,11 @@
"yaml": "^1.10.0"
}
},
+ "crc-32": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-0.3.0.tgz",
+ "integrity": "sha1-aj02h/W67EH36bmf4ZU6Ll0Zd14="
+ },
"cross-env": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
@@ -11781,6 +11803,14 @@
"integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==",
"dev": true
},
+ "png-chunks-extract": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/png-chunks-extract/-/png-chunks-extract-1.0.0.tgz",
+ "integrity": "sha1-+tSpBeZmUhlzUcZeNbksZDEeRy0=",
+ "requires": {
+ "crc-32": "^0.3.0"
+ }
+ },
"pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
diff --git a/package.json b/package.json
@@ -24,6 +24,7 @@
"meilisearch": "0.24.0",
"next": "12.0.9",
"node-localstorage": "2.2.1",
+ "png-chunks-extract": "^1.0.0",
"prop-types": "15.8.1",
"react": "17.0.2",
"react-blurhash": "0.1.3",
diff --git a/pages/index.tsx b/pages/index.tsx
@@ -110,7 +110,7 @@ class Home extends PureComponent<Props, State>{
</div>
<div className='m-10'>
<ul className='flex flex-wrap gap-1'>
- {image_events.map(event => <FrontPageImage event={event} key={(event as MatrixEventBase).event_id} />)}
+ {image_events.map(event => <FrontPageImage show_nsfw={false} event={event} key={(event as MatrixEventBase).event_id} />)}
<li className='grow-[10]'></li>
</ul>
</div>
diff --git a/pages/profile/[userid].tsx b/pages/profile/[userid].tsx
@@ -211,7 +211,7 @@ class Profile extends PureComponent<Props, State> {
</div>
</div>
<div>
- <ul className='flex flex-wrap gap-1'>{image_events.map(event => <FrontPageImage event={event} imageHeight="286px" key={(event as MatrixEventBase).event_id} />)}
+ <ul className='flex flex-wrap gap-1'>{image_events.map(event => <FrontPageImage show_nsfw={true} event={event} imageHeight="286px" key={(event as MatrixEventBase).event_id} />)}
<li className='grow-[10]'></li>
</ul>
</div>