change mailbox behaviour to use reception confirmations

This commit is contained in:
marlon-erler
2026-03-31 22:02:13 +00:00
parent 959dc4d288
commit ec020e6358
21 changed files with 2772 additions and 2476 deletions
+27 -27
View File
@@ -1,34 +1,34 @@
<html lang="en">
<head>
<title>UDN Server</title>
<head>
<title>UDN Server</title>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=no"
/>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=no"
/>
<meta
name="theme-color"
content="hsla(0, 0%, 100%, 100%)"
media="(prefers-color-scheme: light)"
/>
<meta
name="theme-color"
content="black"
media="(prefers-color-scheme: dark)"
/>
<meta
name="theme-color"
content="hsla(0, 0%, 100%, 100%)"
media="(prefers-color-scheme: light)"
/>
<meta
name="theme-color"
content="black"
media="(prefers-color-scheme: dark)"
/>
<link rel="stylesheet" href="/styles.css" />
<link rel="stylesheet" href="/index.css" />
<link rel="manifest" href="/manifest.json" />
<link rel="icon" href="/icon.png" />
<script src="/support.js" defer></script>
<link rel="stylesheet" href="/styles.css" />
<link rel="stylesheet" href="/index.css" />
<link rel="manifest" href="/manifest.json" />
<link rel="icon" href="/icon.png" />
<script src="/support.js" defer></script>
<script src="/index.js" defer></script>
</head>
<script src="/index.js" defer></script>
</head>
<body>
<main></main>
</body>
<body>
<main></main>
</body>
</html>
+797 -547
View File
File diff suppressed because it is too large Load Diff
+8 -8
View File
@@ -1,10 +1,10 @@
{
"name": "UDN Server",
"display": "standalone",
"icons": [
{
"src": "icon.png",
"sizes": "any"
}
]
"name": "UDN Server",
"display": "standalone",
"icons": [
{
"src": "icon.png",
"sizes": "any"
}
]
}
+401 -396
View File
File diff suppressed because it is too large Load Diff
+22 -19
View File
@@ -1,31 +1,34 @@
// TABS
document.querySelector("main").addEventListener("scroll", () => {
const links = document.querySelectorAll(".tab-link");
links.forEach((element) => element.removeAttribute("active"));
const links = document.querySelectorAll(".tab-link");
links.forEach((element) => element.removeAttribute("active"));
const pageWidth = document.querySelector("main > article").offsetWidth;
const scroll = document.querySelector("main").scrollLeft;
const currentPage = Math.round(scroll / pageWidth);
const pageWidth = document.querySelector("main > article").offsetWidth;
const scroll = document.querySelector("main").scrollLeft;
const currentPage = Math.round(scroll / pageWidth);
if (links[currentPage]) links[currentPage].setAttribute("active", "");
if (links[currentPage]) links[currentPage].setAttribute("active", "");
});
// SERVICE WORKER
async function registerServiceWorker() {
if ("serviceWorker" in navigator) {
try {
const registration = await navigator.serviceWorker.register("/sw.js", {
scope: "/",
});
if (registration.installing) {
setTimeout(() => window.location.reload(), 500);
} else if (registration.active) {
console.log("Service worker active");
}
} catch (error) {
console.error(`Could not install service worker: ${error}`);
if ("serviceWorker" in navigator) {
try {
const registration = await navigator.serviceWorker.register(
"/sw.js",
{
scope: "/",
},
);
if (registration.installing) {
setTimeout(() => window.location.reload(), 500);
} else if (registration.active) {
console.log("Service worker active");
}
} catch (error) {
console.error(`Could not install service worker: ${error}`);
}
}
}
}
registerServiceWorker();
+18 -18
View File
@@ -1,38 +1,38 @@
const CACHE_VERSION = "v1";
async function handleRequest(event) {
const responsePromise = getResponse(event);
event.respondWith(responsePromise);
const responsePromise = getResponse(event);
event.respondWith(responsePromise);
}
async function getResponse(event) {
try {
return await fetchAndCache(event);
} catch {
return await getFromCache(event);
}
try {
return await fetchAndCache(event);
} catch {
return await getFromCache(event);
}
}
async function fetchAndCache(event) {
const request = event.request;
const request = event.request;
const response = await fetch(request);
const response = await fetch(request);
const cache = await caches.open(CACHE_VERSION);
await cache.put(request, response.clone());
const cache = await caches.open(CACHE_VERSION);
await cache.put(request, response.clone());
return response;
return response;
}
async function getFromCache(event) {
const request = event.request;
const request = event.request;
const cache = await caches.open(CACHE_VERSION);
const response = await cache.match(request);
const cache = await caches.open(CACHE_VERSION);
const response = await cache.match(request);
return response;
return response;
}
self.addEventListener("fetch", (event) => {
handleRequest(event);
});
handleRequest(event);
});
+8 -8
View File
@@ -1,10 +1,10 @@
{
"dependencies": {
"bloatless-react": "^1.2.6",
"esbuild": "latest",
"udn-frontend": "^1.0.6"
},
"scripts": {
"build": "esbuild src/index.tsx --bundle --outdir=dist"
}
"dependencies": {
"bloatless-react": "^1.2.6",
"esbuild": "latest",
"udn-frontend": "^1.0.6"
},
"scripts": {
"build": "esbuild src/index.tsx --bundle --outdir=dist"
}
}
+31 -31
View File
@@ -7,41 +7,41 @@ import { isDisconnected } from "./model";
import { translations } from "./translations";
document.body.prepend(
<menu>
<a class="tab-link" href="#info-screen" active>
<span class="icon">info</span>
{translations.info}
</a>
<a class="tab-link" href="#main-screen">
<span class="icon">settings</span>
{translations.settings}
</a>
<a class="tab-link" href="#message-screen">
<span class="icon">chat</span>
{translations.messages}
</a>
</menu>
<menu>
<a class="tab-link" href="#info-screen" active>
<span class="icon">info</span>
{translations.info}
</a>
<a class="tab-link" href="#main-screen">
<span class="icon">settings</span>
{translations.settings}
</a>
<a class="tab-link" href="#message-screen">
<span class="icon">chat</span>
{translations.messages}
</a>
</menu>,
);
document
.querySelector("main")!
.append(InfoScreen(), MainScreen(), MessageScreen());
.querySelector("main")!
.append(InfoScreen(), MainScreen(), MessageScreen());
document.body.append(
<div class="modal" toggle:open={isDisconnected}>
<div>
<main>
<div
class="flex-column align-center justify-center width-100 height-100"
style="gap: 1rem"
>
<span class="icon error" style="font-size: 3rem">
signal_disconnected
</span>
<h1 class="error">{translations.disconnected}</h1>
<p class="secondary">{translations.reconnecting}</p>
<div class="modal" toggle:open={isDisconnected}>
<div>
<main>
<div
class="flex-column align-center justify-center width-100 height-100"
style="gap: 1rem"
>
<span class="icon error" style="font-size: 3rem">
signal_disconnected
</span>
<h1 class="error">{translations.disconnected}</h1>
<p class="secondary">{translations.reconnecting}</p>
</div>
</main>
</div>
</main>
</div>
</div>
</div>,
);
+18 -15
View File
@@ -5,19 +5,22 @@ import { statHTMLString, updateStats } from "./model";
import { translations } from "./translations";
export function InfoScreen() {
return (
<article id="info-screen">
<header>
{translations.serverInfo}
<span>
<button on:click={updateStats}>
<span class="icon">refresh</span>
</button>
</span>
</header>
<div>
<div class="flex-column gap" subscribe:innerHTML={statHTMLString}></div>
</div>
</article>
);
return (
<article id="info-screen">
<header>
{translations.serverInfo}
<span>
<button on:click={updateStats}>
<span class="icon">refresh</span>
</button>
</span>
</header>
<div>
<div
class="flex-column gap"
subscribe:innerHTML={statHTMLString}
></div>
</div>
</article>
);
}
+109 -103
View File
@@ -1,122 +1,128 @@
import * as React from "bloatless-react";
import {
deleteMailbox,
isDisconnected,
isMailboxDisonnected,
isMessageEmpty,
lastReceivedMessage,
newMessageBody,
newMessageChannel,
requestMailbox,
sendMessage,
subscribe,
subscriptionChannel,
unsubscribe,
deleteMailbox,
isDisconnected,
isMailboxDisonnected,
isMessageEmpty,
lastReceivedMessage,
newMessageBody,
newMessageChannel,
requestMailbox,
sendMessage,
subscribe,
subscriptionChannel,
unsubscribe,
} from "./model";
import { translations } from "./translations";
export function MainScreen() {
// UI
return (
<article id="main-screen">
<header>{translations.settings}</header>
<div>
<div class="tile width-input">
<div>
<b>{translations.messageLastReceived}</b>
<p class="secondary" subscribe:innerText={lastReceivedMessage}></p>
</div>
</div>
// UI
return (
<article id="main-screen">
<header>{translations.settings}</header>
<div>
<div class="tile width-input">
<div>
<b>{translations.messageLastReceived}</b>
<p
class="secondary"
subscribe:innerText={lastReceivedMessage}
></p>
</div>
</div>
<hr></hr>
<hr></hr>
<div class="tile width-input">
<div>
<b>{translations.mailbox}</b>
<p class="secondary" toggle:hidden={isMailboxDisonnected}>
{translations.mailboxConnected}
</p>
</div>
</div>
<div class="tile width-input">
<div>
<b>{translations.mailbox}</b>
<p
class="secondary"
toggle:hidden={isMailboxDisonnected}
>
{translations.mailboxConnected}
</p>
</div>
</div>
<div class="flex-row width-input justify-end">
<button
class="danger width-50"
on:click={deleteMailbox}
toggle:disabled={isMailboxDisonnected}
>
{translations.deleteMailbox}
</button>
<button
class="primary width-50"
on:click={requestMailbox}
toggle:disabled={isDisconnected}
>
{translations.requestMailbox}
<span class="icon">inbox</span>
</button>
</div>
<div class="flex-row width-input justify-end">
<button
class="danger width-50"
on:click={deleteMailbox}
toggle:disabled={isMailboxDisonnected}
>
{translations.deleteMailbox}
</button>
<button
class="primary width-50"
on:click={requestMailbox}
toggle:disabled={isDisconnected}
>
{translations.requestMailbox}
<span class="icon">inbox</span>
</button>
</div>
<hr></hr>
<hr></hr>
<label class="tile">
<div>
<span>{translations.subscribeToChannel}</span>
<input
placeholder={translations.channel_placeholder}
bind:value={subscriptionChannel}
on:enter={subscribe}
></input>
</div>
</label>
<label class="tile">
<div>
<span>{translations.subscribeToChannel}</span>
<input
placeholder={translations.channel_placeholder}
bind:value={subscriptionChannel}
on:enter={subscribe}
></input>
</div>
</label>
<div class="flex-row width-input justify-end">
<button class="danger width-50" on:click={unsubscribe}>
{translations.unsubscribe}
</button>
<button class="primary width-50" on:click={subscribe}>
{translations.subscribe}
<span class="icon">arrow_forward</span>
</button>
</div>
<div class="flex-row width-input justify-end">
<button class="danger width-50" on:click={unsubscribe}>
{translations.unsubscribe}
</button>
<button class="primary width-50" on:click={subscribe}>
{translations.subscribe}
<span class="icon">arrow_forward</span>
</button>
</div>
<hr></hr>
<hr></hr>
<label class="tile">
<div>
<span>{translations.channel}</span>
<input
placeholder={translations.channel_placeholder}
bind:value={newMessageChannel}
on:enter={sendMessage}
></input>
</div>
</label>
<label class="tile">
<div>
<span>{translations.channel}</span>
<input
placeholder={translations.channel_placeholder}
bind:value={newMessageChannel}
on:enter={sendMessage}
></input>
</div>
</label>
<label class="tile">
<div>
<span>{translations.message}</span>
<input
placeholder={translations.message_placeholder}
bind:value={newMessageBody}
on:enter={sendMessage}
></input>
</div>
</label>
<label class="tile">
<div>
<span>{translations.message}</span>
<input
placeholder={translations.message_placeholder}
bind:value={newMessageBody}
on:enter={sendMessage}
></input>
</div>
</label>
<div class="flex-row width-input justify-end">
<button
class="primary width-50"
on:click={sendMessage}
toggle:disabled={isMessageEmpty}
>
{translations.send}
<span class="icon">send</span>
</button>
</div>
</div>
</article>
);
<div class="flex-row width-input justify-end">
<button
class="primary width-50"
on:click={sendMessage}
toggle:disabled={isMessageEmpty}
>
{translations.send}
<span class="icon">send</span>
</button>
</div>
</div>
</article>
);
}
+39 -39
View File
@@ -1,51 +1,51 @@
import * as React from "bloatless-react";
import {
Message,
isMessageEmpty,
messages,
newMessageBody,
sendMessage,
Message,
isMessageEmpty,
messages,
newMessageBody,
sendMessage,
} from "./model";
import { translations } from "./translations";
const convertMessageToElement: React.ListItemConverter<Message> = (message) => {
return (
<div class="tile" style="flex: 0">
<div>
<span class="secondary">{message.channel}</span>
<b>{message.body}</b>
</div>
</div>
);
return (
<div class="tile" style="flex: 0">
<div>
<span class="secondary">{message.channel}</span>
<b>{message.body}</b>
</div>
</div>
);
};
export function MessageScreen() {
return (
<article id="message-screen">
<header>{translations.messagesReceived}</header>
<div>
<div
class="flex-column gap"
subscribe:children={[messages, convertMessageToElement]}
></div>
</div>
<footer>
<input
style="max-width: unset"
placeholder={translations.message_placeholder_generic}
bind:value={newMessageBody}
on:enter={sendMessage}
></input>
<button
class="primary"
on:click={sendMessage}
toggle:disabled={isMessageEmpty}
>
<span class="icon">send</span>
</button>
</footer>
</article>
);
return (
<article id="message-screen">
<header>{translations.messagesReceived}</header>
<div>
<div
class="flex-column gap"
subscribe:children={[messages, convertMessageToElement]}
></div>
</div>
<footer>
<input
style="max-width: unset"
placeholder={translations.message_placeholder_generic}
bind:value={newMessageBody}
on:enter={sendMessage}
></input>
<button
class="primary"
on:click={sendMessage}
toggle:disabled={isMessageEmpty}
>
<span class="icon">send</span>
</button>
</footer>
</article>
);
}
+55 -50
View File
@@ -4,7 +4,10 @@ import UDNFrontend from "udn-frontend";
import { translations } from "./translations";
export class Message {
constructor(public channel: string, public body: string) {}
constructor(
public channel: string,
public body: string,
) {}
}
// STATE
@@ -18,28 +21,30 @@ export const isMailboxDisonnected = new React.State(true);
// messages
export const messages = new React.ListState<Message>();
export const lastReceivedMessage = new React.State(
translations.noMessagesReceived
translations.noMessagesReceived,
);
// stats
export const statHTMLString = new React.State<string>("");
export async function updateStats() {
const response = await fetch(
`${window.location.protocol}//${window.location.host}/stats`
);
const data: [string, string][] = await response.json();
let html = "";
data.forEach((item) => {
const tile = (
<div class="tile">
<span class="secondary flex width-100">{item[0].toString()}</span>
<b>{item[1].toString()}</b>
</div>
const response = await fetch(
`${window.location.protocol}//${window.location.host}/stats`,
);
html += tile.outerHTML;
});
statHTMLString.value = html;
const data: [string, string][] = await response.json();
let html = "";
data.forEach((item) => {
const tile = (
<div class="tile">
<span class="secondary flex width-100">
{item[0].toString()}
</span>
<b>{item[1].toString()}</b>
</div>
);
html += tile.outerHTML;
});
statHTMLString.value = html;
}
// channels
@@ -49,79 +54,79 @@ export const newMessageChannel = new React.State("");
export const newMessageBody = new React.State("");
export const isMessageEmpty = React.createProxyState(
[newMessageChannel, newMessageBody],
() => newMessageChannel.value == "" || newMessageBody.value == ""
[newMessageChannel, newMessageBody],
() => newMessageChannel.value == "" || newMessageBody.value == "",
);
// UDN Frontend
const UDN = new UDNFrontend();
UDN.onconnect = () => {
isDisconnected.value = false;
updateStats();
isDisconnected.value = false;
updateStats();
if (mailboxId.value != "") {
UDN.connectMailbox(mailboxId.value);
}
if (mailboxId.value != "") {
UDN.connectMailbox(mailboxId.value);
}
};
UDN.onmessage = (data) => {
lastReceivedMessage.value = JSON.stringify(data, null, 4);
lastReceivedMessage.value = JSON.stringify(data, null, 4);
const { messageChannel, messageBody } = data;
if (messageChannel && messageBody) {
const messageObject = new Message(messageChannel, messageBody);
if (messageBody) messages.add(messageObject);
}
const { messageChannel, messageBody } = data;
if (messageChannel && messageBody) {
const messageObject = new Message(messageChannel, messageBody);
if (messageBody) messages.add(messageObject);
}
};
UDN.onmailboxcreate = (id) => {
console.log(id);
mailboxId.value = id;
UDN.connectMailbox(id);
console.log(id);
mailboxId.value = id;
UDN.connectMailbox(id);
};
UDN.onmailboxconnect = () => {
isMailboxDisonnected.value = false;
isMailboxDisonnected.value = false;
};
UDN.onmailboxdelete = () => {
isMailboxDisonnected.value = true;
}
isMailboxDisonnected.value = true;
};
UDN.ondisconnect = () => {
isDisconnected.value = true;
isMailboxDisonnected.value = true;
setTimeout(connect, 1000);
isDisconnected.value = true;
isMailboxDisonnected.value = true;
setTimeout(connect, 1000);
};
function connect() {
UDN.connect(
`${window.location.protocol.replace("http", "ws")}//${window.location.host}`
);
UDN.connect(
`${window.location.protocol.replace("http", "ws")}//${window.location.host}`,
);
}
connect();
// METHODS
export function subscribe() {
UDN.subscribe(subscriptionChannel.value);
UDN.subscribe(subscriptionChannel.value);
}
export function unsubscribe() {
UDN.unsubscribe(subscriptionChannel.value);
UDN.unsubscribe(subscriptionChannel.value);
}
export function sendMessage() {
if (isMessageEmpty.value == true) return;
UDN.sendMessage(newMessageChannel.value, newMessageBody.value);
newMessageBody.value = "";
if (isMessageEmpty.value == true) return;
UDN.sendMessage(newMessageChannel.value, newMessageBody.value);
newMessageBody.value = "";
}
export function requestMailbox() {
UDN.requestMailbox();
UDN.requestMailbox();
}
export function deleteMailbox() {
console.log("test");
UDN.deleteMailbox(mailboxId.value);
console.log("test");
UDN.deleteMailbox(mailboxId.value);
}
+71 -71
View File
@@ -1,79 +1,79 @@
// STATIC
const staticTextEnglish = {
mailbox: "Mailbox",
requestMailbox: "Request",
deleteMailbox: "Delete",
mailboxConnected: "Mailbox active",
channel: "Channel",
channel_placeholder: "my-channel",
disconnected: "Server disconnected",
reconnecting: "Reconnecting...",
info: "Info",
message: "Message",
messages: "Messages",
message_placeholder: "Hello, world!",
message_placeholder_generic: "Type a message...",
noMessagesReceived: "No messages received",
messageLastReceived: "Last received message",
messagesReceived: "Received Messages",
send: "Send",
serverInfo: "Server Info",
settings: "Settings",
subscribeToChannel: "Subscribe to channel",
subscribe: "Subscribe",
unsubscribe: "Unubscribe",
mailbox: "Mailbox",
requestMailbox: "Request",
deleteMailbox: "Delete",
mailboxConnected: "Mailbox active",
channel: "Channel",
channel_placeholder: "my-channel",
disconnected: "Server disconnected",
reconnecting: "Reconnecting...",
info: "Info",
message: "Message",
messages: "Messages",
message_placeholder: "Hello, world!",
message_placeholder_generic: "Type a message...",
noMessagesReceived: "No messages received",
messageLastReceived: "Last received message",
messagesReceived: "Received Messages",
send: "Send",
serverInfo: "Server Info",
settings: "Settings",
subscribeToChannel: "Subscribe to channel",
subscribe: "Subscribe",
unsubscribe: "Unubscribe",
};
const allTranslations: { [key: string]: typeof staticTextEnglish } = {
en: staticTextEnglish,
es: {
mailbox: "Buzón",
requestMailbox: "Solicitar",
deleteMailbox: "Eliminar",
mailboxConnected: "Buzón activado",
channel: "Canal",
channel_placeholder: "mi-canal",
disconnected: "Sin coneción",
reconnecting: "Conectando...",
info: "Info",
message: "Mensaje",
messages: "Mensajes",
message_placeholder: "Hola!",
message_placeholder_generic: "Nuevo mensaje",
noMessagesReceived: "Sin mensajes",
messageLastReceived: "Último mensaje",
messagesReceived: "Todos los Mensajes",
send: "Enviar",
serverInfo: "Información",
settings: "Configuración",
subscribeToChannel: "Suscribirse a un canal",
subscribe: "Suscribirse",
unsubscribe: "Cancelar",
},
de: {
mailbox: "Briefkasten",
requestMailbox: "Beantragen",
deleteMailbox: "Löschen",
mailboxConnected: "Briefkasten aktiv",
channel: "Kanal",
channel_placeholder: "mein-kanal",
disconnected: "Verbindung getrennt",
reconnecting: "Verbindung wird hergestellt...",
info: "Daten",
message: "Nachricht",
messages: "Nachrichten",
message_placeholder: "Hallo!",
message_placeholder_generic: "Neue Nachricht",
noMessagesReceived: "Keine Nachrichten",
messageLastReceived: "Letzte Nachricht",
messagesReceived: "Empfangene Nachrichten",
send: "Senden",
serverInfo: "Serverdaten",
settings: "Einstellungen",
subscribeToChannel: "Kanal beitreten",
subscribe: "Beitreten",
unsubscribe: "Verlassen",
},
en: staticTextEnglish,
es: {
mailbox: "Buzón",
requestMailbox: "Solicitar",
deleteMailbox: "Eliminar",
mailboxConnected: "Buzón activado",
channel: "Canal",
channel_placeholder: "mi-canal",
disconnected: "Sin coneción",
reconnecting: "Conectando...",
info: "Info",
message: "Mensaje",
messages: "Mensajes",
message_placeholder: "Hola!",
message_placeholder_generic: "Nuevo mensaje",
noMessagesReceived: "Sin mensajes",
messageLastReceived: "Último mensaje",
messagesReceived: "Todos los Mensajes",
send: "Enviar",
serverInfo: "Información",
settings: "Configuración",
subscribeToChannel: "Suscribirse a un canal",
subscribe: "Suscribirse",
unsubscribe: "Cancelar",
},
de: {
mailbox: "Briefkasten",
requestMailbox: "Beantragen",
deleteMailbox: "Löschen",
mailboxConnected: "Briefkasten aktiv",
channel: "Kanal",
channel_placeholder: "mein-kanal",
disconnected: "Verbindung getrennt",
reconnecting: "Verbindung wird hergestellt...",
info: "Daten",
message: "Nachricht",
messages: "Nachrichten",
message_placeholder: "Hallo!",
message_placeholder_generic: "Neue Nachricht",
noMessagesReceived: "Keine Nachrichten",
messageLastReceived: "Letzte Nachricht",
messagesReceived: "Empfangene Nachrichten",
send: "Senden",
serverInfo: "Serverdaten",
settings: "Einstellungen",
subscribeToChannel: "Kanal beitreten",
subscribe: "Beitreten",
unsubscribe: "Verlassen",
},
};
const language = navigator.language.substring(0, 2);