One sign-in for both of PrivaPub's APIs: the root logs in on /clientapi, picks a persona, and each persona gets its own Mastodon token in exchange for the JWT (PersonaExchange). HttpService picks credentials and JSON by route (JWT and camelCase for /clientapi, the persona token and snake_case for /api, nothing for /oauth), retries a refused persona token once, and records every request for the nerd stats. The feed page is three columns: navigation with a persona switcher, the feed with a tab per kind (home, local, federated, nearby, communities and circles, direct, notifications, favourites, bookmarks, hashtags), and a 3D globe that looks at the spherical mean of the posts in view and draws a pulse and an arc to a hovered one. Posts sit at their own place, their event's venue, the reader (located posts) or their author's server as PrivaPub publishes it. Cards favourite, boost, bookmark, reply, mute, block and delete; the composer posts with a content warning, visibility, media and alt text, or as a located post. Older pages load as the end nears; newer ones are polled while visible. Nerd stats in tooltips: provenance, author and server, place, media, counts, paging, the globe's camera and frame rate. The mock (Faker, MessagesService, the Message models and store) and the OIDC template leftovers are gone. Strings in English and Italian. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
180 lines
4.5 KiB
JavaScript
180 lines
4.5 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)
|
|
}
|
|
}
|
|
window.isServiceWorkerAvailable = () => {
|
|
try {
|
|
return navigator.serviceWorker.valueOf().controller != null
|
|
} catch (e) {
|
|
console.error(e)
|
|
window.logFromJs(e.message, "isServiceWorkerAvailable")
|
|
}
|
|
}
|
|
window.getLanguage = () => {
|
|
try {
|
|
return navigator.language.substr(0, 2)
|
|
} catch (e) {
|
|
console.error(e)
|
|
window.logFromJs(e.message, "getLanguage")
|
|
}
|
|
}
|
|
window.setLanguage = (lang) => {
|
|
try {
|
|
navigator.language = lang
|
|
} catch (e) {
|
|
console.error(e)
|
|
window.logFromJs(e.message, "setLanguage")
|
|
}
|
|
}
|
|
window.isOnline = async () => {
|
|
try {
|
|
const isOnline = navigator.onLine
|
|
//const url = "https://contents.nuvola.xyz/api/assets/6d9f5e55-2034-456f-92e7-d9ec51066443"
|
|
//let isOnlineStatusCode = true
|
|
|
|
//try {
|
|
// const online = await fetch(url)
|
|
// isOnlineStatusCode = online.status >= 200 && online.status < 399
|
|
//} catch (_) {
|
|
// isOnlineStatusCode = false
|
|
//}
|
|
|
|
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 /*&& isOnlineStatusCode*/ :
|
|
true
|
|
} else
|
|
return isOnline //&& isOnlineStatusCode
|
|
} catch (e) {
|
|
console.error(e)
|
|
window.logFromJs(e.message, "isOnline")
|
|
}
|
|
}
|
|
window.clearCache = async () => {
|
|
try {
|
|
const cachesNames = await caches.keys()
|
|
for (let i = 0; i < cachesNames.length; i++)
|
|
await caches.delete(cachesNames[i])
|
|
} catch (e) {
|
|
console.error(e)
|
|
window.logFromJs(e.message, "clearCache")
|
|
}
|
|
}
|
|
window.clearLocalStorage = () => {
|
|
try {
|
|
localStorage.removeItem("PrivateCacheData")
|
|
localStorage.removeItem("AuthData")
|
|
} catch (e) {
|
|
console.error(e)
|
|
window.logFromJs(e.message, "clearLocalStorage")
|
|
}
|
|
}
|
|
window.reloadPage = () => {
|
|
try {
|
|
location.reload(true)
|
|
} catch (e) {
|
|
console.error(e)
|
|
window.logFromJs(e.message, "reloadPage")
|
|
}
|
|
}
|
|
window.getHeight = (classId) => {
|
|
try {
|
|
const elements = document.querySelectorAll(classId)
|
|
let px = 0
|
|
if (!elements) return 0
|
|
elements.forEach(v => px += v.offsetHeight)
|
|
return px
|
|
} catch (e) {
|
|
console.error(e)
|
|
window.logFromJs(e.message, "getHeight")
|
|
}
|
|
}
|
|
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")
|
|
}
|
|
}
|
|
window.isMobileMedia = () => {
|
|
try {
|
|
return ((window.innerWidth > 0) ? window.innerWidth : screen.width) <= 768
|
|
} catch (e) {
|
|
console.error(e)
|
|
window.logFromJs(e.message, "isMobileMedia")
|
|
}
|
|
}
|
|
window.canShareStuff = () => {
|
|
try {
|
|
return navigator.share !== undefined
|
|
} catch (e) {
|
|
console.error(e)
|
|
window.logFromJs(e.message, "canShareStuff")
|
|
}
|
|
}
|
|
window.shareTextUrl = (title, text, url) => {
|
|
try {
|
|
navigator.share({
|
|
title: title,
|
|
text: text,
|
|
url: url
|
|
})
|
|
} catch (e) {
|
|
console.error(e)
|
|
window.logFromJs(e.message, "shareTextUrl")
|
|
}
|
|
}
|
|
window.shareText = (title, text) => {
|
|
try {
|
|
navigator.share({
|
|
title: title,
|
|
text: text
|
|
})
|
|
} catch (e) {
|
|
console.error(e)
|
|
window.logFromJs(e.message, "shareText")
|
|
}
|
|
}// 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 })
|
|
}
|
|
})
|