Build / Build (push) Successful in 1m3s
Completion program R3, with PrivaPub's TOTP routes of the same release. - Settings, the login: TotpEditor turns two-factor sign-in on (the password, then the QR code, the secret and the otpauth:// link, then the app's first code; the ten recovery codes shown once with Copy), shows how many recovery codes are left (warned at three), and turns it off with the password and a code, asked first. - Turning it on, like a password change, ends every other session: AuthService.ContinueSession takes the new JWT and exchanges the persona's token again. - Sign-in: PrivaPub's 428 keeps the name and password and asks for the six digits (DOtp), or a recovery code instead; a wrong code says so; Back returns to the password. - window.copyText (main.js) through AppStatusService.CopyText. - 29 keys in both resx files; CLAUDE.md follows. Verified: dotnet build decePubClient.csproj -c Release (0 errors, no new warnings). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
159 lines
5.0 KiB
JavaScript
159 lines
5.0 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")
|
|
}
|
|
}
|
|
window.setAppBadge = (counter) => {
|
|
try {
|
|
navigator.setAppBadge(counter)
|
|
} catch (e) {
|
|
console.error(e)
|
|
window.logFromJs(e.message, "setAppBadge")
|
|
}
|
|
}
|
|
window.clearAppBadge = () => {
|
|
try {
|
|
navigator.clearAppBadge()
|
|
} catch (e) {
|
|
console.error(e)
|
|
window.logFromJs(e.message, "clearAppBadge")
|
|
}
|
|
}
|
|
// 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")
|
|
}
|
|
})
|
|
// 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")
|
|
}
|
|
}
|
|
// the reader asked for less motion: what would move by itself (a gifv) waits for a tap
|
|
window.prefersReducedMotion = () => {
|
|
try {
|
|
return window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
} catch (e) {
|
|
console.error(e)
|
|
window.logFromJs(e.message, "prefersReducedMotion")
|
|
return false
|
|
}
|
|
}
|
|
// 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
|
|
}
|
|
}
|
|
// 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 })
|
|
}
|
|
})
|