diff --git a/internal/web/profile.go b/internal/web/profile.go
index e8483921d..dd3ed8e64 100644
--- a/internal/web/profile.go
+++ b/internal/web/profile.go
@@ -249,7 +249,7 @@ func (m *Module) profileMicroblog(c *gin.Context, p *profile) {
},
{
Bottom: true,
- Src: jsBlurhash,
+ Src: jsFrontendPrerender,
},
},
Extra: map[string]any{
@@ -323,7 +323,7 @@ func (m *Module) profileGallery(c *gin.Context, p *profile) {
},
{
Bottom: true,
- Src: jsBlurhash,
+ Src: jsFrontendPrerender,
},
},
Extra: map[string]any{
diff --git a/internal/web/thread.go b/internal/web/thread.go
index 0b296a75b..d46e52ac7 100644
--- a/internal/web/thread.go
+++ b/internal/web/thread.go
@@ -154,7 +154,7 @@ func (m *Module) threadGETHandler(c *gin.Context) {
},
{
Bottom: true,
- Src: jsBlurhash,
+ Src: jsFrontendPrerender,
},
},
Extra: map[string]any{
diff --git a/internal/web/web.go b/internal/web/web.go
index 15814942a..5bccca06d 100644
--- a/internal/web/web.go
+++ b/internal/web/web.go
@@ -67,9 +67,9 @@ const (
cssSettings = distPathPrefix + "/settings-style.css"
cssTag = distPathPrefix + "/tag.css"
- jsFrontend = distPathPrefix + "/frontend.js" // Progressive enhancement frontend JS.
- jsBlurhash = distPathPrefix + "/blurhash.js" // Blurhash rendering JS.
- jsSettings = distPathPrefix + "/settings.js" // Settings panel React application.
+ jsFrontend = distPathPrefix + "/frontend.js" // Progressive enhancement frontend JS.
+ jsFrontendPrerender = distPathPrefix + "/frontend_prerender.js" // Frontend JS that should run before page renders.
+ jsSettings = distPathPrefix + "/settings.js" // Settings panel React application.
)
type Module struct {
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/blurhash/index.js b/web/source/frontend_prerender/index.js
similarity index 56%
rename from web/source/blurhash/index.js
rename to web/source/frontend_prerender/index.js
index c964f69c4..294c1ddb1 100644
--- a/web/source/blurhash/index.js
+++ b/web/source/frontend_prerender/index.js
@@ -17,8 +17,18 @@
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 => {
@@ -144,3 +154,110 @@ Array.from(document.getElementsByTagName('img')).forEach(img => {
}
});
});
+
+// 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: [