Files
decePubClient/wwwroot/main.js
T
thepraandClaude Opus 5.5 697b2c8897
Build / Build (push) Successful in 33s
Animations, GIFs and the globe now move for every reader
decePub no longer looks at the system's "reduce motion" setting, so everything moves the same way whatever it says:
- a GIF video in a post plays by itself, muted and looping, instead of waiting for a tap; it now also starts on a page
  just opened, before the first tap, where the browser used to hold it still;
- animated custom emoji move in the emoji picker instead of showing a still picture;
- the globe turns by itself while no located post is in view, flies to the posts, and its pulses, arcs and Sun move;
- going back to the top of a feed scrolls smoothly;
- tooltips, dropdowns, menus, folding sections, toasts, loading spinners, placeholders, progress bars, radio buttons
  and ratings keep their animations: the build now takes daisyUI's reduce-motion rules out of the stylesheet
  (tools/css/motion.py, run after every Tailwind build, in watch mode too).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W4YtEKqxKjKyHkFzuRVMNA
2026-11-09 20:48:47 +01:00

199 lines
6.6 KiB
JavaScript

window.cascadingStateInstanceReference = (dotNetReference) => {
try {
window.cascadingStateInstance = dotNetReference
} catch (e) {
console.error(e)
}
}
window.logFromJs = (message, where) => {
try {
if (!window.cascadingStateInstance ||
typeof window.cascadingStateInstance.invokeMethodAsync != "function")
return
window.cascadingStateInstance.invokeMethodAsync('LogFromJs', message, where)
} catch (e) {
console.error(e)
}
}
// The client once kept its error log in IndexedDB (DnetIndexedDb's database "data"). Nothing uses IndexedDB now, so a
// browser that still holds that database drops it.
try {
window.indexedDB?.deleteDatabase("data")
} catch (e) {
console.error(e)
}
window.getLanguage = () => {
try {
return navigator.language.substr(0, 2)
} catch (e) {
console.error(e)
window.logFromJs(e.message, "getLanguage")
}
}
window.isOnline = async () => {
try {
const isOnline = navigator.onLine
if (navigator.connection !== undefined &&
navigator.connection.effectiveType !== undefined &&
navigator.connection.type !== undefined) {
if (!isOnline)
return isOnline
return navigator.connection.type === "cellular" ?
navigator.connection.effectiveType.indexOf("2g") === -1 :
true
} else
return isOnline
} catch (e) {
console.error(e)
window.logFromJs(e.message, "isOnline")
}
}
// The installed app's badge: the current persona's unread notifications (Index.razor). Browsers without the Badging API
// (Firefox, or a tab that isn't installed) show none, and say nothing about it.
window.setAppBadge = (counter) => {
try {
if (typeof navigator.setAppBadge != "function")
return
navigator.setAppBadge(counter)?.catch?.(e => console.debug(e))
} catch (e) {
console.error(e)
window.logFromJs(e.message, "setAppBadge")
}
}
window.clearAppBadge = () => {
try {
if (typeof navigator.clearAppBadge != "function")
return
navigator.clearAppBadge()?.catch?.(e => console.debug(e))
} catch (e) {
console.error(e)
window.logFromJs(e.message, "clearAppBadge")
}
}
// A click on a Web Push notification while decePub is open: the persona's worker (push-worker.js) focuses this tab and
// says which of its registrations showed it, and the app opens that persona's notifications (CascadingState.PushOpened).
// Only this origin's workers can post here.
try {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.addEventListener("message", event => {
try {
if (event.data?.type !== "decepub-push-open" || typeof event.data.slot != "string")
return
window.cascadingStateInstance?.invokeMethodAsync("PushOpened", event.data.slot)
} catch (e) {
console.error(e)
window.logFromJs(e.message, "pushOpened")
}
})
navigator.serviceWorker.startMessages()
}
} catch (e) {
console.error(e)
}
// On a touch screen a tap on a post (not on its links, controls or menus, and not ending a text selection) opens its
// thread, through the card's own thread link (Content.razor, data-thread-link), which Blazor's router takes.
document.addEventListener("click", event => {
try {
if (window.matchMedia("(hover: hover)").matches || !(event.target instanceof Element))
return
const card = event.target.closest("[data-thread]")
if (!card || event.target.closest("a, button, input, textarea, select, label, summary, video, audio, [role=button], .dropdown, .modal, [data-sheet]"))
return
if (!window.getSelection()?.isCollapsed)
return
card.querySelector("a[data-thread-link]")?.click()
} catch (e) {
console.error(e)
window.logFromJs(e.message, "openThread")
}
})
document.addEventListener("loadstart", event => {
if (event.target instanceof HTMLVideoElement && event.target.hasAttribute("muted"))
event.target.muted = true
}, { capture: true })
// The Back button (a popstate) as opposed to the app's own navigation: Blazor's location-changing handlers can't tell
// them apart, and MainLayout lets Back close an open overlay but lets the app's navigation through. Registered for the
// capture phase, so it runs before Blazor's own listener.
let backAt = 0
window.addEventListener("popstate", () => backAt = Date.now(), { capture: true })
window.tookBack = () => {
const back = Date.now() - backAt < 1000
backAt = 0
return back
}
window.canHover = () => {
try {
return window.matchMedia("(hover: hover)").matches
} catch (e) {
console.error(e)
window.logFromJs(e.message, "canHover")
return true
}
}
// the selected tab of a sideways-scrolling strip, brought into its middle
window.revealActive = strip => {
try {
strip?.querySelector("[aria-selected=true]")?.scrollIntoView({ inline: "center", block: "nearest" })
} catch (e) {
console.error(e)
window.logFromJs(e.message, "revealActive")
}
}
// text put on the clipboard (the recovery codes of two-factor sign-in); false when the browser refused it
window.copyText = async text => {
try {
await navigator.clipboard.writeText(text)
return true
} catch (e) {
console.error(e)
window.logFromJs(e.message, "copyText")
return false
}
}
// where a text field's selection starts and ends (the composer writes an emoji there); null when it can't say
window.caretOf = field => {
try {
return typeof field?.selectionStart == "number" ? [field.selectionStart, field.selectionEnd] : null
} catch (e) {
console.error(e)
window.logFromJs(e.message, "caretOf")
return null
}
}
// the field takes the focus with its caret at that offset (after the emoji just written)
window.placeCaret = (field, at) => {
try {
field?.focus()
field?.setSelectionRange(at, at)
} catch (e) {
console.error(e)
window.logFromJs(e.message, "placeCaret")
}
}
// a phone: narrower than Tailwind's md, or held sideways (as short as the short: variant)
window.isMobileMedia = () => {
try {
return window.matchMedia("(max-width: 767.98px), (max-height: 30rem)").matches
} catch (e) {
console.error(e)
window.logFromJs(e.message, "isMobileMedia")
}
}
// This device's position for located posts and the Nearby feed, asked only when the reader chooses one of them. It
// goes to PrivaPub only, which keeps located posts on the server and rounds what it stores.
window.feedPosition = () => new Promise(resolve => {
try {
if (!navigator.geolocation)
return resolve({ error: "unsupported" })
navigator.geolocation.getCurrentPosition(
position => resolve({ latitude: position.coords.latitude, longitude: position.coords.longitude, accuracy: position.coords.accuracy }),
error => resolve({ error: error.message || String(error.code) }),
{ enableHighAccuracy: false, timeout: 15000, maximumAge: 600000 })
} catch (e) {
console.error(e)
window.logFromJs(e.message, "feedPosition")
resolve({ error: e.message })
}
})