From 73c7fa569aa3d302fdb94230a65e5ac934f2e030 Mon Sep 17 00:00:00 2001 From: MTRNord Date: Wed, 30 Mar 2022 21:12:49 +0200 Subject: [PATCH 01/38] chore: Reset to vite template --- .gitignore | 27 +- Dockerfile | 49 - components/ClientContext.tsx | 16 - components/Footer.tsx | 15 - components/FrontPageImage.tsx | 178 - components/Header.tsx | 141 - components/editIcon.tsx | 37 - components/submit_page/main.tsx | 556 - components/submit_page/start.tsx | 37 - components/uploadIcon.tsx | 38 - e2e/checkEnglishWorks.spec.ts | 31 - e2e/navigateToDetails.spec.ts | 32 - helpers/BlurhashEncoder.ts | 55 - helpers/db/Users.ts | 12 - helpers/event_types.ts | 102 - helpers/init-middleware.ts | 25 - helpers/matrix_client.ts | 585 - helpers/ss-well-known.ts | 177 - helpers/storage.ts | 129 - helpers/utils.ts | 60 - helpers/workers/blurhash.worker.ts | 35 - i18next-parser.config.js | 8 - index.html | 13 + next-env.d.ts | 5 - next-i18next.config.js | 10 - next.config.js | 61 - package-lock.json | 13194 ---------------- package.json | 62 +- pages/404.tsx | 27 - pages/500.tsx | 26 - pages/_app.tsx | 129 - pages/_document.tsx | 25 - pages/api/directory.ts | 108 - pages/api/posts/feed.rss.ts | 205 - pages/api/submitSearch.ts | 59 - pages/index.tsx | 188 - pages/login.tsx | 213 - pages/logout.tsx | 44 - pages/post/[id].tsx | 497 - pages/profile/[userid].tsx | 396 - pages/submit.tsx | 138 - playwright.config.ts | 113 - postcss.config.js | 6 - public/.well-known/security.txt | 6 - public/Logo.svg | 11 - public/favicon.ico | Bin 25931 -> 0 bytes public/fonts/Inter-Black.woff2 | Bin 104704 -> 0 bytes public/fonts/Inter-Bold.woff2 | Bin 108700 -> 0 bytes public/fonts/Inter-ExtraBold.woff2 | Bin 108692 -> 0 bytes public/fonts/Inter-ExtraLight.woff2 | Bin 106292 -> 0 bytes public/fonts/Inter-Light.woff2 | Bin 106340 -> 0 bytes public/fonts/Inter-Medium.woff2 | Bin 108052 -> 0 bytes public/fonts/Inter-Regular.woff2 | Bin 100280 -> 0 bytes public/fonts/Inter-SemiBold.woff2 | Bin 108588 -> 0 bytes public/fonts/Inter-Thin.woff2 | Bin 100352 -> 0 bytes .../fonts/Inter-VariableFont_slnt,wght.woff2 | Bin 325608 -> 0 bytes public/locales/de-DE/common.json | 60 - public/locales/en/common.json | 61 - src/app.tsx | 20 + src/favicon.svg | 15 + src/index.css | 30 + src/logo.tsx | 47 + src/main.tsx | 5 + src/preact.d.ts | 1 + src/vite-env.d.ts | 1 + styles/globals.css | 219 - thirdparty-licenses.md | 1119 -- tsconfig.json | 40 +- tsconfig.node.json | 8 + vite.config.ts | 7 + 70 files changed, 194 insertions(+), 19320 deletions(-) delete mode 100644 Dockerfile delete mode 100644 components/ClientContext.tsx delete mode 100644 components/Footer.tsx delete mode 100644 components/FrontPageImage.tsx delete mode 100644 components/Header.tsx delete mode 100644 components/editIcon.tsx delete mode 100644 components/submit_page/main.tsx delete mode 100644 components/submit_page/start.tsx delete mode 100644 components/uploadIcon.tsx delete mode 100644 e2e/checkEnglishWorks.spec.ts delete mode 100644 e2e/navigateToDetails.spec.ts delete mode 100644 helpers/BlurhashEncoder.ts delete mode 100644 helpers/db/Users.ts delete mode 100644 helpers/event_types.ts delete mode 100644 helpers/init-middleware.ts delete mode 100644 helpers/matrix_client.ts delete mode 100644 helpers/ss-well-known.ts delete mode 100644 helpers/storage.ts delete mode 100644 helpers/utils.ts delete mode 100644 helpers/workers/blurhash.worker.ts delete mode 100644 i18next-parser.config.js create mode 100644 index.html delete mode 100644 next-env.d.ts delete mode 100644 next-i18next.config.js delete mode 100644 next.config.js delete mode 100644 package-lock.json delete mode 100644 pages/404.tsx delete mode 100644 pages/500.tsx delete mode 100644 pages/_app.tsx delete mode 100644 pages/_document.tsx delete mode 100644 pages/api/directory.ts delete mode 100644 pages/api/posts/feed.rss.ts delete mode 100644 pages/api/submitSearch.ts delete mode 100644 pages/index.tsx delete mode 100644 pages/login.tsx delete mode 100644 pages/logout.tsx delete mode 100644 pages/post/[id].tsx delete mode 100644 pages/profile/[userid].tsx delete mode 100644 pages/submit.tsx delete mode 100644 playwright.config.ts delete mode 100644 postcss.config.js delete mode 100644 public/.well-known/security.txt delete mode 100644 public/Logo.svg delete mode 100644 public/favicon.ico delete mode 100644 public/fonts/Inter-Black.woff2 delete mode 100644 public/fonts/Inter-Bold.woff2 delete mode 100644 public/fonts/Inter-ExtraBold.woff2 delete mode 100644 public/fonts/Inter-ExtraLight.woff2 delete mode 100644 public/fonts/Inter-Light.woff2 delete mode 100644 public/fonts/Inter-Medium.woff2 delete mode 100644 public/fonts/Inter-Regular.woff2 delete mode 100644 public/fonts/Inter-SemiBold.woff2 delete mode 100644 public/fonts/Inter-Thin.woff2 delete mode 100644 public/fonts/Inter-VariableFont_slnt,wght.woff2 delete mode 100644 public/locales/de-DE/common.json delete mode 100644 public/locales/en/common.json create mode 100644 src/app.tsx create mode 100644 src/favicon.svg create mode 100644 src/index.css create mode 100644 src/logo.tsx create mode 100644 src/main.tsx create mode 100644 src/preact.d.ts create mode 100644 src/vite-env.d.ts delete mode 100644 styles/globals.css delete mode 100644 thirdparty-licenses.md create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore index f40901e..b4c8972 100644 --- a/.gitignore +++ b/.gitignore @@ -30,11 +30,30 @@ yarn-error.log* .env.test.local .env.production.local -# vercel -.vercel +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* -# typescript -*.tsbuildinfo +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? #pouchdb /matrix-art-db diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 33018c2..0000000 --- a/Dockerfile +++ /dev/null @@ -1,49 +0,0 @@ -# Install dependencies only when needed -# Use node:17-slim -FROM node@sha256:5e1c50b7686bcaf01b800966bf52d83a2530ea521290bba6eb0fd4eae3025055 AS deps -# for some reason the $PATH get lost inside kaniko, if we re-set it by hand it seems to work. -ENV PATH="$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" -WORKDIR /app -COPY package.json package-lock.json ./ -RUN npm ci - -# Rebuild the source code only when needed -# Use node:17-slim -FROM node@sha256:5e1c50b7686bcaf01b800966bf52d83a2530ea521290bba6eb0fd4eae3025055 AS builder -WORKDIR /app -COPY . . -COPY --from=deps /app/node_modules ./node_modules -RUN npx browserslist@latest --update-db -RUN mkdir ./localstorage && echo "4" >> ./localstorage/version -RUN npm run build && npm install --only=production --ignore-scripts --prefer-offline - -# Production image, copy all the files and run next -# Use node:17-slim -FROM node@sha256:5e1c50b7686bcaf01b800966bf52d83a2530ea521290bba6eb0fd4eae3025055 AS runner -WORKDIR /app - -ENV NODE_ENV production - -RUN addgroup --gid 1001 --system nodejs -RUN adduser --system nextjs --uid 1001 - -# You only need to copy next.config.js if you are NOT using the default configuration -COPY --from=builder /app/public ./public -COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next -COPY --from=builder /app/node_modules ./node_modules -COPY --from=builder /app/package.json ./package.json -COPY --from=builder /app/next.config.js ./next.config.js -COPY --from=builder /app/next-i18next.config.js ./next-i18next.config.js - -USER nextjs - -EXPOSE 3000 - -ENV PORT 3000 - -# Next.js collects completely anonymous telemetry data about general usage. -# Learn more here: https://nextjs.org/telemetry -# Uncomment the following line in case you want to disable telemetry. -ENV NEXT_TELEMETRY_DISABLED 1 - -CMD ["node_modules/.bin/next", "start"] diff --git a/components/ClientContext.tsx b/components/ClientContext.tsx deleted file mode 100644 index 051380a..0000000 --- a/components/ClientContext.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React, { Context } from "react"; -import MatrixClient from "../helpers/matrix_client"; - -type ContextData = { - client?: MatrixClient; - guest_client?: MatrixClient; - is_generating_guest: boolean; -}; - -const ClientContext: Context = React.createContext({ - client: undefined, - guest_client: undefined, - is_generating_guest: false -}); - -export { ClientContext }; \ No newline at end of file diff --git a/components/Footer.tsx b/components/Footer.tsx deleted file mode 100644 index f622760..0000000 --- a/components/Footer.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import Link from "next/link"; -import { PureComponent } from "react"; -import { i18n } from 'next-i18next'; - -export default class Footer extends PureComponent { - render() { - return ( - - ); - } -} \ No newline at end of file diff --git a/components/FrontPageImage.tsx b/components/FrontPageImage.tsx deleted file mode 100644 index 9b06121..0000000 --- a/components/FrontPageImage.tsx +++ /dev/null @@ -1,178 +0,0 @@ -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"; -import PropTypes from 'prop-types'; -import { toast } from "react-toastify"; -import { i18n } from "next-i18next"; - -type Props = { - event: MatrixImageEvents; - imageHeight?: string; - show_nsfw: boolean; -}; - -type State = { - displayname: string; - imageHeight?: string; - error?: string; -}; - -export default class FrontPageImage extends PureComponent { - declare context: React.ContextType; - - constructor(props: Props) { - super(props); - - this.state = { - displayname: this.props.event.sender, - imageHeight: this.props.imageHeight ? this.props.imageHeight : "270px" - } as State; - } - - static propTypes = { - event: PropTypes.object, - imageHeight: PropTypes.string, - show_nsfw: PropTypes.bool - }; - - componentDidUpdate(prevProps: Props, prevState: State) { - if (this.state.error && this.state.error !== prevState.error) { - toast(() =>

{i18n?.t("Error")}


{this.state.error}
, { - autoClose: false - }); - } - } - - async componentDidMount() { - // auto-register as a guest if not logged in - if (!this.context.client?.accessToken) { - this.registerAsGuest();; - } else { - console.log("Already logged in"); - if (!this.props.event) { - return; - } - try { - const profile = await this.context.client.getProfile(this.props.event.sender); - this.setState({ - displayname: profile.displayname ?? this.props.event.sender, - }); - } catch (error) { - console.debug(`Failed to fetch profile for user ${this.props.event.sender}:`, error); - } - } - - } - - async registerAsGuest() { - if (this.context.is_generating_guest) { - return; - } - this.context.is_generating_guest = true; - try { - let serverUrl = constMatrixArtServer + "/_matrix/client"; - await this.context.client?.registerAsGuest(serverUrl); - this.context.is_generating_guest = false; - } catch (error) { - console.error("Failed to register as guest:", error); - this.setState({ - error: "Failed to register as guest: " + JSON.stringify(error), - }); - } - this.context.is_generating_guest = false; - } - - render() { - const event = this.props.event; - return ( - <> - {isImageGalleryEvent(event) ? this.render_gallery(event) : (isImageEvent(event) ? this.render_image(event) :
)} - - ); - } - - - render_gallery(event: ImageGalleryEvent) { - const caption_text = event.content['m.caption'].filter(cap => { - const possible_html_caption = (cap as { body: string; mimetype: string; }); - const possible_text_caption = (cap as { "m.text": string; }); - return (possible_html_caption.body && possible_html_caption.mimetype === "text/html") ?? possible_text_caption["m.text"]; - }).map(cap => { - const possible_html_caption = (cap as { body: string; mimetype: string; }); - const possible_text_caption = (cap as { "m.text": string; }); - 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"] && !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"]); - }); - } - - - render_image(event: ImageEvent) { - 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; }); - const possible_text_caption = (cap as { "m.text": string; }); - return (possible_html_caption.body && possible_html_caption.mimetype === "text/html") ?? possible_text_caption["m.text"]; - }).map(cap => { - const possible_html_caption = (cap as { body: string; mimetype: string; }); - const possible_text_caption = (cap as { "m.text": string; }); - return (possible_html_caption.body && possible_html_caption.mimetype === "text/html") ? possible_html_caption.body : possible_text_caption["m.text"]; - })[0]; - const thumbnail_url = event.content['m.thumbnail'] ? (event.content['m.thumbnail'].length > 0 ? event.content['m.thumbnail'][0].url : event.content['m.file'].url) : event.content['m.file'].url; - return this.render_image_box(thumbnail_url, event.event_id, event.event_id, caption_text, event.content['m.image'].height, event.content['m.image'].width, event.content["xyz.amorgan.blurhash"]); - } - - render_image_box(thumbnail_url: string, id: string, post_id: string, caption: string, h: number, w: number, 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 ? ( -
- - {caption} -
- ) : ( - {caption} - ); - return ( -
  • - - -
    - {image} -
    -

    {caption}

    -

    {this.state.displayname}

    -
    -
    -
    - -
  • - ); - } -} -FrontPageImage.contextType = ClientContext; - -// TODO also render the edits properly later on -export function isImageGalleryEvent(event: MatrixImageEvents): event is ImageGalleryEvent { - return event.type === "m.image_gallery" && !event.unsigned?.redacted_because; -} - - -export function isImageEvent(event: MatrixImageEvents): event is ImageEvent { - return event.type === "m.image" && !event.unsigned?.redacted_because; -}; \ No newline at end of file diff --git a/components/Header.tsx b/components/Header.tsx deleted file mode 100644 index 4637c95..0000000 --- a/components/Header.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import Link from "next/link"; -import { PureComponent } from "react"; -import User from "../helpers/db/Users"; -import { ClientContext } from "./ClientContext"; -import { i18n } from 'next-i18next'; -import { toast } from "react-toastify"; -import dynamic from "next/dynamic"; - -type Props = { -}; - -type State = { - directory_data: User[]; - loading: boolean; - error?: string; -}; -export default class Header extends PureComponent { - declare context: React.ContextType; - constructor(props: Props) { - super(props); - - this.state = { - directory_data: [], - loading: true - } as State; - } - - componentDidUpdate(prevProps: Props, prevState: State) { - if (this.state.error && this.state.error !== prevState.error) { - toast(() =>

    {i18n?.t("Error")}


    {this.state.error}
    , { - autoClose: false - }); - } - } - - async componentDidMount() { - try { - const data = await fetch("/api/directory", { method: "GET" }); - const data_parsed = await data.json(); - const directory_data = data_parsed.data; - this.setState({ directory_data: directory_data, loading: false }); - } catch { - console.error("Failed to get directory"); - this.setState({ error: "Failed to get directory" }); - } - } - render() { - if (this.state.loading) { - return <>; - } - return ( - <> -
    - - - - - -
    -
    -
    -
    - - - - -
    -
    -
    - - -
    - - - {value => { - const isGuest = value.client?.isGuest === null || value.client?.isGuest === undefined ? true : value.client.isGuest; - if (typeof window !== "undefined" && !isGuest) { - return (); - } else { - return (); - } - }} - - -
    -
    - - {value => { - const isGuest = value.client?.isGuest === null || value.client?.isGuest === undefined ? true : value.client.isGuest; - if (typeof window !== "undefined" && !isGuest) { - if (this.state.directory_data.some(thing => thing.mxid == this.context.client?.userId)) { - return {i18n?.t('Submit') ?? 'Submit'}; - } else { - return {i18n?.t('Setup Account') ?? 'Setup Account'}; - } - } - }} - -
    -
    -
    - - ); - } -} -Header.contextType = ClientContext; \ No newline at end of file diff --git a/components/editIcon.tsx b/components/editIcon.tsx deleted file mode 100644 index 8419146..0000000 --- a/components/editIcon.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { PureComponent } from "react"; -import PropTypes from 'prop-types'; - -type Props = { - onClick?: () => void; - className?: string; -}; - -type State = { - className: string; - onClick: () => void; -}; - -export class EditIcon extends PureComponent { - - constructor(props: Props) { - super(props); - - this.state = { - className: (props.className ?? "") + " dark:fill-white fill-black", - onClick: props.onClick ?? (() => {/*noop*/ }) - }; - } - static propTypes = { - onClick: PropTypes.func, - className: PropTypes.string - }; - - render() { - return ( - - - - - ); - } -} \ No newline at end of file diff --git a/components/submit_page/main.tsx b/components/submit_page/main.tsx deleted file mode 100644 index c01754f..0000000 --- a/components/submit_page/main.tsx +++ /dev/null @@ -1,556 +0,0 @@ -import { PureComponent } from "react"; -import { DropCallbacks } from "./start"; -import PropTypes from 'prop-types'; -import { PreviewWithDataFile } from "../../pages/submit"; -import { ClientContext } from "../ClientContext"; -import Dropzone from "react-dropzone"; -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, ThumbnailData } from "../../helpers/event_types"; -import { toast } from "react-toastify"; -// @ts-ignore This has no types -import extractPngChunks from "png-chunks-extract"; -import { i18n } from "next-i18next"; - -type ThumbnailableElement = HTMLImageElement | HTMLVideoElement; -type ThumbnailTransmissionData = { - thumbnail_meta: ThumbnailData; - thumbnail: Blob; -}; -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. -const IMAGE_THUMBNAIL_MIN_REDUCTION_SIZE = 1 << 16; // 1MB -const IMAGE_THUMBNAIL_MIN_REDUCTION_PERCENT = 0.1; // 10% - -interface Props extends DropCallbacks, WithRouterProps { - files: PreviewWithDataFile[]; -}; - -type State = { - currentFileIndex: number; - hasSubmit: boolean; - hasBack: boolean; - submit_in_process: boolean; - [key: string]: any; -}; -class MainSubmissionForm extends PureComponent { - declare context: React.ContextType; - - constructor(props: Props) { - super(props); - this.state = { - currentFileIndex: 0, - hasSubmit: props.files.length === 1 ? true : false, - hasBack: false, - submit_in_process: false, - }; - const range = [...Array(this.props.files.length).keys()]; // eslint-disable-line unicorn/new-for-builtins - for (const index of range) { - // @ts-ignore Its fine in the constructor - this.state[`${index}_valid`] = false; - } - } - static propTypes = { - onDrop: PropTypes.func, - onDropError: PropTypes.func, - files: PropTypes.array, - hasSubmit: PropTypes.bool, - hasBack: PropTypes.bool - }; - - renderThumb() { - const file = this.props.files[this.state.currentFileIndex]; - return ( -
    - {file.name} -
    - ); - } - - renderThumbs() { - return this.props.files.map((file, index) => { - const setIndex = () => { - if (this.props.files.length - 1 > index) { - const hasBack = index > 0 ? true : false; - this.setState({ currentFileIndex: index, hasBack, hasSubmit: false }); - } else if (this.props.files.length - 1 === index) { - const hasBack = index > 0 ? true : false; - this.setState({ currentFileIndex: index, hasBack, hasSubmit: true }); - } - }; - - const classes = () => { - const classes_base = ["cursor-pointer", "aspect-video", "my-2"]; - - if (this.state.currentFileIndex == index) { - classes_base.push("border", "p-2", "border-2", "border-white"); - } else if (this.state[`${index}_valid`]) { - classes_base.push("border", "p-2", "border-2", "border-teal-600"); - } else { - classes_base.push("border", "p-2", "border-2", "border-yellow-700"); - } - return classes_base.join(" "); - }; - - - // TODO FIXME do make this work with keyboard presses! - /* eslint-disable jsx-a11y/click-events-have-key-events */ - return ( -
    - {file.name} -
    - ); - /* eslint-enable jsx-a11y/click-events-have-key-events */ - }); - } - - onNext(ev: { preventDefault: () => void; }) { - ev.preventDefault(); - const newIndex = this.state.currentFileIndex + 1; - if (this.props.files.length - 1 > newIndex) { - const hasBack = newIndex > 0 ? true : false; - this.setState({ currentFileIndex: newIndex, hasBack }); - } else if (this.props.files.length - 1 === newIndex) { - this.setState({ currentFileIndex: newIndex, hasSubmit: true }); - } - } - - onPrev(ev: { preventDefault: () => void; }) { - ev.preventDefault(); - const newIndex = this.state.currentFileIndex - 1; - if (this.props.files.length - 1 > newIndex) { - const hasBack = newIndex > 0 ? true : false; - this.setState({ currentFileIndex: newIndex, hasBack }); - } else if (this.props.files.length - 1 === newIndex) { - this.setState({ currentFileIndex: newIndex, hasSubmit: true }); - } - } - - - async handleSubmit() { - if (this.state.submit_in_process) { - return; - } - this.setState({ submit_in_process: true }); - - const range = [...Array(this.props.files.length).keys()]; // eslint-disable-line unicorn/new-for-builtins - const posts_for_search: SearchMedia[] = []; - - // Clear all errors before showing new - toast.dismiss(); - - - if (this.context.client?.isGuest) { - toast.error(() =>

    {i18n?.t("Error")}


    {i18n?.t("You are not logged in!")}
    , { - autoClose: false - }); - this.setState({ submit_in_process: false }); - return; - } - - // If any image is invalid do exit submit for now. - for (const index of range) { - const valid = this.state[`${index}_valid`]; - if (!valid) { - toast.error(() =>

    {i18n?.t("Error")}


    {i18n?.t("You did not fill the required fields for all images. Please fix this!")}
    , { - autoClose: false - }); - this.setState({ submit_in_process: false }); - return; - } - } - - const image_infos = await this.getImageInfos(); - const ids = await this.doUpload(); - - const thumbnails = await this.generateThumbnailsAndUpload(image_infos); - - // Handle uploads - for (const index of range) { - const title = `${index}_title`; - const description = `${index}_description`; - const tags = `${index}_tags`; - const license = `${index}_license`; - const nsfw = `${index}_nsfw`; - const file = this.props.files[index]; - - if (!this.context.client?.profileRoomId) { - toast.error(() =>

    {i18n?.t("Error")}


    {i18n?.t("Unable to find your profile Room. Please log in again!")}
    , { - autoClose: false - }); - this.setState({ submit_in_process: false }); - return; - } - - const event = { - "m.text": this.state[title], - "m.caption": [{ - "m.text": this.state[title] - }], - "m.file": { - mimetype: file.type, - name: file.name, - url: ids[index].url, - size: file.size - }, - "m.image": { - height: image_infos[index].height, - width: image_infos[index].width, - }, - "matrixart.description": this.state[description], - "matrixart.nsfw": this.state[nsfw] === "yes" ? true : false, - "matrixart.license": this.state[license], - "matrixart.tags": this.state[tags].split(",").map((x: string) => x.trimStart().trimEnd()), - } as unknown as ImageEventContent; - const thumbnailData = thumbnails.find(item => item.index == index); - event["m.thumbnail"] = thumbnailData?.meta["m.thumbnail"]; - event["xyz.amorgan.blurhash"] = thumbnailData?.meta["xyz.amorgan.blurhash"]!; - - const event_id = await this.context.client.sendEvent(this.context.client.profileRoomId, 'm.image', event); - - posts_for_search.push({ - mxc_url: ids[index].url, - event_id: event_id, - title: this.state[title], - description: this.state[description], - tags: this.state[tags].trimStart().trimEnd(), - nsfw: this.state[nsfw] === "yes" ? "true" : "false", - license: this.state[license], - sender: this.context.client.userId! - }); - } - const token = await this.context.client?.getOpenidToken(); - await fetch("/api/submitSearch", { - method: "POST", body: JSON.stringify({ - access_token: token, - user_id: this.context.client?.userId, - docs: posts_for_search - }) - }); - await this.props.router.replace("/"); - this.setState({ submit_in_process: false }); - } - - /** - * 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 { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.addEventListener("load", (e: ProgressEvent) => { - 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, - inputHeight: number, - mimeType: string - ): Promise { - let targetWidth = inputWidth; - let targetHeight = inputHeight; - if (targetHeight > MAX_HEIGHT) { - targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight)); - targetHeight = MAX_HEIGHT; - } - if (targetWidth > MAX_WIDTH) { - targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth)); - targetWidth = MAX_WIDTH; - } - - let canvas: HTMLCanvasElement | OffscreenCanvas; - let context: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D | null; - try { - canvas = new window.OffscreenCanvas(targetWidth, targetHeight); - context = canvas.getContext("2d"); - } catch { - // Fallback support for other browsers (Safari and Firefox for now) - canvas = document.createElement("canvas"); - (canvas as HTMLCanvasElement).width = targetWidth; - (canvas as HTMLCanvasElement).height = targetHeight; - context = canvas.getContext("2d"); - } - - if (!context) { - return; - } - context?.drawImage(element, 0, 0, targetWidth, targetHeight); - - let thumbnailPromise: Promise; - - if (window.OffscreenCanvas) { - thumbnailPromise = (canvas as OffscreenCanvas).convertToBlob({ type: mimeType }); - } else { - thumbnailPromise = new Promise(resolve => (canvas as HTMLCanvasElement).toBlob(resolve, mimeType)); - } - - const imageData = context.getImageData(0, 0, targetWidth, targetHeight); - // thumbnailPromise and blurhash promise are being awaited concurrently - const blurhash = await BlurhashEncoder.instance.getBlurhash(imageData); - const thumbnail = await thumbnailPromise; - if (!thumbnail) { - return; - } - - return { - thumbnail_meta: { - "m.thumbnail": [ - { - width: targetWidth, - height: targetHeight, - mimetype: thumbnail.type, - size: thumbnail.size, - url: "" - } - ], - "xyz.amorgan.blurhash": blurhash, - }, - thumbnail, - }; - } - - 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 thumbnail_data = await this.createThumbnail( - image_infos[index].img, - image_infos[index].width, - image_infos[index].height, - file.type - ); - if (!thumbnail_data) { - continue; - } - - // we do all sizing checks here because we still rely on thumbnail generation for making a blurhash from. - const sizeDifference = file.size - thumbnail_data.thumbnail_meta["m.thumbnail"]![0].size; - if ( - file.size <= IMAGE_SIZE_THRESHOLD_THUMBNAIL || // image is small enough already - (sizeDifference <= IMAGE_THUMBNAIL_MIN_REDUCTION_SIZE && // thumbnail is not sufficiently smaller than original - sizeDifference <= (file.size * IMAGE_THUMBNAIL_MIN_REDUCTION_PERCENT)) - ) { - delete thumbnail_data.thumbnail_meta["m.thumbnail"]; - thumbnails.push({ index: index, meta: thumbnail_data.thumbnail_meta }); - } - - const result = await this.context.client?.uploadFile(thumbnail_data.thumbnail); - if (result) { - thumbnail_data.thumbnail_meta["m.thumbnail"]![0].url = result; - } - thumbnails.push({ index: index, meta: thumbnail_data.thumbnail_meta }); - } - } - return thumbnails; - } - - private async doUpload() { - const urls = []; - 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 result = await this.context.client?.uploadFile(file); - urls.push({ index: index, url: result }); - } - } - return urls; - } - - componentDidUpdate() { - const title = `${this.state.currentFileIndex}_title`; - const description = `${this.state.currentFileIndex}_description`; - const license = `${this.state.currentFileIndex}_license`; - const nsfw = `${this.state.currentFileIndex}_nsfw`; - let validated = false; - if (this.state[title] && this.state[license] && this.state[description] && this.state[nsfw]) { - validated = true; - } - if (this.state[`${this.state.currentFileIndex}_valid`] !== validated) { - this.setState({ - [`${this.state.currentFileIndex}_valid`]: validated - } as State); - } - } - - handleInputChange(event: { target: any; }) { - const target = event.target; - const value = target.type === 'checkbox' ? target.checked : target.value; - const name = `${this.state.currentFileIndex}_${target.name}`; - - this.setState({ - [name]: value, - } as State); - } - - render() { - if (this.state.submit_in_process) { - return ( -
    -
    -
    {i18n?.t('Loading')}...
    -
    -
    - ); - } - return ( -
    -
    -
    - {this.renderThumb()} -
    -
    -
    - - - - -