From 075cae3d55197b96d997d72cfb498c870788453a Mon Sep 17 00:00:00 2001
From: tobi <31960611+tsmethurst@users.noreply.github.com>
Date: Tue, 22 Apr 2025 12:20:54 +0200
Subject: [chore/frontend] Reorder JS a little bit to avoid visible text
changes (#4039)
---
web/source/blurhash/index.js | 146 ------------------
web/source/frontend/index.js | 116 ++-------------
web/source/frontend_prerender/index.js | 263 +++++++++++++++++++++++++++++++++
web/source/index.js | 6 +-
4 files changed, 275 insertions(+), 256 deletions(-)
delete mode 100644 web/source/blurhash/index.js
create mode 100644 web/source/frontend_prerender/index.js
(limited to 'web/source')
diff --git a/web/source/blurhash/index.js b/web/source/blurhash/index.js
deleted file mode 100644
index c964f69c4..000000000
--- a/web/source/blurhash/index.js
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
- GoToSocial
- Copyright (C) GoToSocial Authors admin@gotosocial.org
- SPDX-License-Identifier: AGPL-3.0-or-later
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-*/
-
-import { decode } from "blurhash";
-
-// Generate a blurhash canvas for each image for
-// each blurhash container and put it in the summary.
-Array.from(document.getElementsByClassName('blurhash-container')).forEach(blurhashContainer => {
- const hash = blurhashContainer.dataset.blurhashHash;
- const thumbHeight = blurhashContainer.dataset.blurhashHeight;
- const thumbWidth = blurhashContainer.dataset.blurhashWidth;
- const thumbAspect = blurhashContainer.dataset.blurhashAspect;
-
- /*
- It's very expensive to draw big canvases
- with blurhashes, so use tiny ones, keeping
- aspect ratio of the original thumbnail.
- */
- var useWidth = 32;
- var useHeight = 32;
- switch (true) {
- case thumbWidth > thumbHeight:
- useHeight = Math.round(useWidth / thumbAspect);
- break;
- case thumbHeight > thumbWidth:
- useWidth = Math.round(useHeight * thumbAspect);
- break;
- }
-
- const pixels = decode(
- hash,
- useWidth,
- useHeight,
- );
-
- // Create canvas of appropriate size.
- const canvas = document.createElement("canvas");
- canvas.width = useWidth;
- canvas.height = useHeight;
-
- // Draw the image data into the canvas.
- const ctx = canvas.getContext("2d");
- const imageData = ctx.createImageData(
- useWidth,
- useHeight,
- );
- imageData.data.set(pixels);
- ctx.putImageData(imageData, 0, 0);
-
- // Put the canvas inside the container.
- blurhashContainer.appendChild(canvas);
-});
-
-// Add a smooth transition from blurhash
-// to image for each sensitive image.
-Array.from(document.getElementsByTagName('img')).forEach(img => {
- if (!img.dataset.blurhashHash) {
- // Has no blurhash,
- // can't transition smoothly.
- return;
- }
-
- if (img.dataset.sensitive !== "true") {
- // Not sensitive, smooth
- // transition doesn't matter.
- return;
- }
-
- if (img.complete) {
- // Image already loaded,
- // don't stub it with anything.
- return;
- }
-
- const parentSlide = img.closest(".photoswipe-slide");
- if (!parentSlide) {
- // Parent slide was nil,
- // can't do anything.
- return;
- }
-
- const blurhashContainer = document.querySelector("div[data-blurhash-hash=\"" + img.dataset.blurhashHash + "\"]");
- if (!blurhashContainer) {
- // Blurhash div was nil,
- // can't do anything.
- return;
- }
-
- const canvas = blurhashContainer.children[0];
- if (!canvas) {
- // Canvas was nil,
- // can't do anything.
- return;
- }
-
- // "Replace" the hidden img with a canvas
- // that will show initially when it's clicked.
- const clone = canvas.cloneNode(true);
- clone.getContext("2d").drawImage(canvas, 0, 0);
- parentSlide.prepend(clone);
- img.className = img.className + " hidden";
-
- // Add a listener so that when the spoiler
- // is opened, loading of the image begins.
- const parentSummary = img.closest(".media-spoiler");
- parentSummary.addEventListener("toggle", (_) => {
- if (parentSummary.hasAttribute("open") && !img.complete) {
- img.loading = "eager";
- }
- });
-
- // Add a callback that triggers
- // when image loading is complete.
- img.addEventListener("load", () => {
- // Show the image now that it's loaded.
- img.className = img.className.replace(" hidden", "");
-
- // Reset the lazy loading tag to its initial
- // value. This doesn't matter too much since
- // it's already loaded but it feels neater.
- img.loading = "lazy";
-
- // Remove the temporary blurhash
- // canvas so only the image shows.
- const canvas = parentSlide.getElementsByTagName("canvas")[0];
- if (canvas) {
- canvas.remove();
- }
- });
-});
diff --git a/web/source/frontend/index.js b/web/source/frontend/index.js
index da158ed77..47879b2e2 100644
--- a/web/source/frontend/index.js
+++ b/web/source/frontend/index.js
@@ -17,6 +17,15 @@
along with this program. If not, see .
*/
+/*
+ WHAT SHOULD GO IN THIS FILE?
+
+ This script is loaded in the document head, and deferred + async,
+ so it's *usually* run after the user is already looking at the page.
+ Put stuff in here that doesn't shift the layout, and it doesn't really
+ matter whether it loads immediately. So, progressive enhancement stuff.
+*/
+
const Photoswipe = require("photoswipe/dist/umd/photoswipe.umd.min.js");
const PhotoswipeLightbox = require("photoswipe/dist/umd/photoswipe-lightbox.umd.min.js");
const PhotoswipeCaptionPlugin = require("photoswipe-dynamic-caption-plugin").default;
@@ -165,89 +174,6 @@ lightbox.on('uiRegister', function() {
lightbox.init();
-function dynamicSpoiler(className, updateFunc) {
- Array.from(document.getElementsByClassName(className)).forEach((spoiler) => {
- const update = updateFunc(spoiler);
- if (update) {
- update();
- spoiler.addEventListener("toggle", update);
- }
- });
-}
-
-dynamicSpoiler("text-spoiler", (details) => {
- const summary = details.children[0];
- const button = details.querySelector(".button");
-
- // Use button *instead of summary*
- // to toggle post visibility.
- summary.tabIndex = "-1";
- button.tabIndex = "0";
- button.setAttribute("aria-role", "button");
- button.onclick = (e) => {
- e.preventDefault();
- return details.hasAttribute("open")
- ? details.removeAttribute("open")
- : details.setAttribute("open", "");
- };
-
- // Let enter also trigger the button
- // (for those using keyboard to navigate).
- button.addEventListener("keydown", (e) => {
- if (e.key === "Enter") {
- e.preventDefault();
- button.click();
- }
- });
-
- // Change button text depending on
- // whether spoiler is open or closed rn.
- return () => {
- button.textContent = details.open
- ? "Show less"
- : "Show more";
- };
-});
-
-dynamicSpoiler("media-spoiler", (details) => {
- const summary = details.children[0];
- const button = details.querySelector(".eye.button");
- const video = details.querySelector(".plyr-video");
- const loopingAuto = !reduceMotion.matches && video != null && video.classList.contains("gifv");
-
- // Use button *instead of summary*
- // to toggle media visibility.
- summary.tabIndex = "-1";
- button.tabIndex = "0";
- button.setAttribute("aria-role", "button");
- button.onclick = (e) => {
- e.preventDefault();
- return details.hasAttribute("open")
- ? details.removeAttribute("open")
- : details.setAttribute("open", "");
- };
-
- // Let enter also trigger the button
- // (for those using keyboard to navigate).
- button.addEventListener("keydown", (e) => {
- if (e.key === "Enter") {
- e.preventDefault();
- button.click();
- }
- });
-
- return () => {
- if (details.open) {
- button.setAttribute("aria-label", "Hide media");
- } else {
- button.setAttribute("aria-label", "Show media");
- if (video && !loopingAuto) {
- video.pause();
- }
- }
- };
-});
-
Array.from(document.getElementsByClassName("plyr-video")).forEach((video) => {
const loopingAuto = !reduceMotion.matches && video.classList.contains("gifv");
let player = new Plyr(video, {
@@ -315,30 +241,6 @@ function inLightbox(element) {
lightbox.pswp.currSlide.data.attachmentId;
}
-// Define + reuse one DateTimeFormat (cheaper).
-const dateTimeFormat = Intl.DateTimeFormat(
- undefined,
- {
- year: 'numeric',
- month: 'short',
- day: '2-digit',
- hour: '2-digit',
- minute: '2-digit',
- hour12: false
- },
-);
-
-// Reformat time text to browser locale.
-Array.from(document.getElementsByTagName('time')).forEach(timeTag => {
- const datetime = timeTag.getAttribute('datetime');
- const currentText = timeTag.textContent.trim();
- // Only format if current text contains precise time.
- if (currentText.match(/\d{2}:\d{2}/)) {
- const date = new Date(datetime);
- timeTag.textContent = dateTimeFormat.format(date);
- }
-});
-
// When clicking anywhere that's not an open
// stats-info-more-content details dropdown,
// close that open dropdown.
diff --git a/web/source/frontend_prerender/index.js b/web/source/frontend_prerender/index.js
new file mode 100644
index 000000000..294c1ddb1
--- /dev/null
+++ b/web/source/frontend_prerender/index.js
@@ -0,0 +1,263 @@
+/*
+ GoToSocial
+ Copyright (C) GoToSocial Authors admin@gotosocial.org
+ SPDX-License-Identifier: AGPL-3.0-or-later
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+/*
+ WHAT SHOULD GO IN THIS FILE?
+
+ This script is loaded just before the end of the HTML body, so
+ put stuff in here that should be run *before* the user sees the page.
+ So, stuff that shifts the layout or causes elements to jump around.
+*/
+
+import { decode } from "blurhash";
+
+const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
+
+// Generate a blurhash canvas for each image for
+// each blurhash container and put it in the summary.
+Array.from(document.getElementsByClassName('blurhash-container')).forEach(blurhashContainer => {
+ const hash = blurhashContainer.dataset.blurhashHash;
+ const thumbHeight = blurhashContainer.dataset.blurhashHeight;
+ const thumbWidth = blurhashContainer.dataset.blurhashWidth;
+ const thumbAspect = blurhashContainer.dataset.blurhashAspect;
+
+ /*
+ It's very expensive to draw big canvases
+ with blurhashes, so use tiny ones, keeping
+ aspect ratio of the original thumbnail.
+ */
+ var useWidth = 32;
+ var useHeight = 32;
+ switch (true) {
+ case thumbWidth > thumbHeight:
+ useHeight = Math.round(useWidth / thumbAspect);
+ break;
+ case thumbHeight > thumbWidth:
+ useWidth = Math.round(useHeight * thumbAspect);
+ break;
+ }
+
+ const pixels = decode(
+ hash,
+ useWidth,
+ useHeight,
+ );
+
+ // Create canvas of appropriate size.
+ const canvas = document.createElement("canvas");
+ canvas.width = useWidth;
+ canvas.height = useHeight;
+
+ // Draw the image data into the canvas.
+ const ctx = canvas.getContext("2d");
+ const imageData = ctx.createImageData(
+ useWidth,
+ useHeight,
+ );
+ imageData.data.set(pixels);
+ ctx.putImageData(imageData, 0, 0);
+
+ // Put the canvas inside the container.
+ blurhashContainer.appendChild(canvas);
+});
+
+// Add a smooth transition from blurhash
+// to image for each sensitive image.
+Array.from(document.getElementsByTagName('img')).forEach(img => {
+ if (!img.dataset.blurhashHash) {
+ // Has no blurhash,
+ // can't transition smoothly.
+ return;
+ }
+
+ if (img.dataset.sensitive !== "true") {
+ // Not sensitive, smooth
+ // transition doesn't matter.
+ return;
+ }
+
+ if (img.complete) {
+ // Image already loaded,
+ // don't stub it with anything.
+ return;
+ }
+
+ const parentSlide = img.closest(".photoswipe-slide");
+ if (!parentSlide) {
+ // Parent slide was nil,
+ // can't do anything.
+ return;
+ }
+
+ const blurhashContainer = document.querySelector("div[data-blurhash-hash=\"" + img.dataset.blurhashHash + "\"]");
+ if (!blurhashContainer) {
+ // Blurhash div was nil,
+ // can't do anything.
+ return;
+ }
+
+ const canvas = blurhashContainer.children[0];
+ if (!canvas) {
+ // Canvas was nil,
+ // can't do anything.
+ return;
+ }
+
+ // "Replace" the hidden img with a canvas
+ // that will show initially when it's clicked.
+ const clone = canvas.cloneNode(true);
+ clone.getContext("2d").drawImage(canvas, 0, 0);
+ parentSlide.prepend(clone);
+ img.className = img.className + " hidden";
+
+ // Add a listener so that when the spoiler
+ // is opened, loading of the image begins.
+ const parentSummary = img.closest(".media-spoiler");
+ parentSummary.addEventListener("toggle", (_) => {
+ if (parentSummary.hasAttribute("open") && !img.complete) {
+ img.loading = "eager";
+ }
+ });
+
+ // Add a callback that triggers
+ // when image loading is complete.
+ img.addEventListener("load", () => {
+ // Show the image now that it's loaded.
+ img.className = img.className.replace(" hidden", "");
+
+ // Reset the lazy loading tag to its initial
+ // value. This doesn't matter too much since
+ // it's already loaded but it feels neater.
+ img.loading = "lazy";
+
+ // Remove the temporary blurhash
+ // canvas so only the image shows.
+ const canvas = parentSlide.getElementsByTagName("canvas")[0];
+ if (canvas) {
+ canvas.remove();
+ }
+ });
+});
+
+// Change the spoiler / content warning boxes from generic
+// "toggle visibility" to show/hide depending on state,
+// and add keyboard functionality to spoiler buttons.
+function dynamicSpoiler(className, updateFunc) {
+ Array.from(document.getElementsByClassName(className)).forEach((spoiler) => {
+ const update = updateFunc(spoiler);
+ if (update) {
+ update();
+ spoiler.addEventListener("toggle", update);
+ }
+ });
+}
+dynamicSpoiler("text-spoiler", (details) => {
+ const summary = details.children[0];
+ const button = details.querySelector(".button");
+
+ // Use button *instead of summary*
+ // to toggle post visibility.
+ summary.tabIndex = "-1";
+ button.tabIndex = "0";
+ button.setAttribute("aria-role", "button");
+ button.onclick = (e) => {
+ e.preventDefault();
+ return details.hasAttribute("open")
+ ? details.removeAttribute("open")
+ : details.setAttribute("open", "");
+ };
+
+ // Let enter also trigger the button
+ // (for those using keyboard to navigate).
+ button.addEventListener("keydown", (e) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ button.click();
+ }
+ });
+
+ // Change button text depending on
+ // whether spoiler is open or closed rn.
+ return () => {
+ button.textContent = details.open
+ ? "Show less"
+ : "Show more";
+ };
+});
+dynamicSpoiler("media-spoiler", (details) => {
+ const summary = details.children[0];
+ const button = details.querySelector(".eye.button");
+ const video = details.querySelector(".plyr-video");
+ const loopingAuto = !reduceMotion.matches && video != null && video.classList.contains("gifv");
+
+ // Use button *instead of summary*
+ // to toggle media visibility.
+ summary.tabIndex = "-1";
+ button.tabIndex = "0";
+ button.setAttribute("aria-role", "button");
+ button.onclick = (e) => {
+ e.preventDefault();
+ return details.hasAttribute("open")
+ ? details.removeAttribute("open")
+ : details.setAttribute("open", "");
+ };
+
+ // Let enter also trigger the button
+ // (for those using keyboard to navigate).
+ button.addEventListener("keydown", (e) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ button.click();
+ }
+ });
+
+ return () => {
+ if (details.open) {
+ button.setAttribute("aria-label", "Hide media");
+ } else {
+ button.setAttribute("aria-label", "Show media");
+ if (video && !loopingAuto) {
+ video.pause();
+ }
+ }
+ };
+});
+
+// Reformat time text to browser locale.
+// Define + reuse one DateTimeFormat (cheaper).
+const dateTimeFormat = Intl.DateTimeFormat(
+ undefined,
+ {
+ year: 'numeric',
+ month: 'short',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ hour12: false
+ },
+);
+Array.from(document.getElementsByTagName('time')).forEach(timeTag => {
+ const datetime = timeTag.getAttribute('datetime');
+ const currentText = timeTag.textContent.trim();
+ // Only format if current text contains precise time.
+ if (currentText.match(/\d{2}:\d{2}/)) {
+ const date = new Date(datetime);
+ timeTag.textContent = dateTimeFormat.format(date);
+ }
+});
diff --git a/web/source/index.js b/web/source/index.js
index c47e9c5bb..d66afe757 100644
--- a/web/source/index.js
+++ b/web/source/index.js
@@ -64,9 +64,9 @@ skulk({
}]
],
},
- blurhash: {
- entryFile: "blurhash",
- outputFile: "blurhash.js",
+ frontend_prerender: {
+ entryFile: "frontend_prerender",
+ outputFile: "frontend_prerender.js",
preset: ["js"],
prodCfg: prodCfg,
transform: [
--
cgit v1.2.3