(() => { // node_modules/bloatless-react/index.ts var State = class { _value; _bindings = /* @__PURE__ */ new Set(); // init constructor(initialValue) { this._value = initialValue; } // value get value() { return this._value; } set value(newValue) { if (this._value == newValue) return; this._value = newValue; this.callSubscriptions(); } // subscriptions callSubscriptions() { this._bindings.forEach((fn) => fn(this._value)); } subscribe(fn) { this._bindings.add(fn); fn(this._value); } subscribeSilent(fn) { this._bindings.add(fn); } // stringify toString() { return JSON.stringify(this._value); } }; function createProxyState(statesToSubscibe, fn) { const proxyState = new State(fn()); statesToSubscibe.forEach( (state) => state.subscribe(() => proxyState.value = fn()) ); return proxyState; } function createElement(tagName, attributes = {}, ...children) { const element = document.createElement(tagName); if (attributes != null) Object.entries(attributes).forEach((entry) => { const [attributename, value] = entry; const [directiveKey, directiveValue] = attributename.split(":"); switch (directiveKey) { case "on": { switch (directiveValue) { case "enter": { element.addEventListener("keydown", (e) => { if (e.key != "Enter") return; value(); }); break; } default: { element.addEventListener(directiveValue, value); } } break; } case "subscribe": { const state = value; state.subscribe( (newValue) => element[directiveValue] = newValue ); break; } case "bind": { const state = value; state.subscribe( (newValue) => element[directiveValue] = newValue ); element.addEventListener( "input", () => state.value = element[directiveValue] ); break; } case "toggle": { if (value.subscribe) { const state = value; state.subscribe( (newValue) => element.toggleAttribute(directiveValue, newValue) ); } else { element.toggleAttribute(directiveValue, value); } break; } case "set": { const state = value; state.subscribe( (newValue) => element.setAttribute(directiveValue, newValue) ); break; } case "children": { switch (directiveValue) { case "set": { const state = value; state.subscribe((newValue) => { element.innerHTML = ""; element.append(...[newValue].flat()); }); break; } case "append": case "appendandscroll": case "prepend": { element.style.scrollBehavior = "smooth"; try { const [listState, toElement] = value; listState.handleAddition((newItem) => { const child = toElement(newItem); listState.handleRemoval( newItem, () => child.remove() ); if (directiveValue == "append") { element.append(child); } else if (directiveValue == "prepend") { element.prepend(child); } }); } catch { throw `error: cannot process subscribe:children directive because StateItemConverter is not defined. Usage: "subscribe:children={[list, converter]}"; you can find a more detailed example in the documentation`; } } } } default: element.setAttribute(attributename, value); } }); children.filter((x) => x).forEach((child) => element.append(child)); return element; } // node_modules/udn-frontend/index.ts var UDNFrontend = class { ws; // HANDLERS connectionHandler = () => { }; disconnectionHandler = () => { }; messageHandler = (data) => { }; mailboxHandler = (mailboxId) => { }; mailboxConnectionHandler = (mailboxId) => { }; mailboxDeleteHandler = (mailboxId) => { }; // INIT set onconnect(handler) { this.connectionHandler = handler; } set ondisconnect(handler) { this.disconnectionHandler = handler; } set onmessage(handler) { this.messageHandler = handler; } set onmailboxcreate(handler) { this.mailboxHandler = handler; } set onmailboxconnect(handler) { this.mailboxConnectionHandler = handler; } set onmailboxdelete(handler) { this.mailboxDeleteHandler = handler; } // UTILITY METHODS send(messageObject) { if (this.ws == void 0) return false; const messageString = JSON.stringify(messageObject); this.ws.send(messageString); return true; } // PUBLIC METHODS // connection connect(address) { try { this.disconnect(); this.ws = new WebSocket(address); this.ws.addEventListener("open", this.connectionHandler); this.ws.addEventListener("close", this.disconnectionHandler); this.ws.addEventListener("message", (message) => { const dataString = message.data.toString(); const data = JSON.parse(dataString); if (data.assignedMailboxId) { return this.mailboxHandler(data.assignedMailboxId); } else if (data.connectedMailboxId) { return this.mailboxConnectionHandler(data.connectedMailboxId); } else if (data.deletedMailbox) { return this.mailboxDeleteHandler(data.deletedMailbox); } else { this.messageHandler(data); } }); } catch (error) { console.error(error); } } disconnect() { this.ws?.close(); } // message sendMessage(channel, body) { const messageObject = { messageChannel: channel, messageBody: body }; return this.send(messageObject); } // subscription subscribe(channel) { const messageObject = { subscribeChannel: channel }; return this.send(messageObject); } unsubscribe(channel) { const messageObject = { unsubscribeChannel: channel }; return this.send(messageObject); } // mailbox requestMailbox() { const messageObject = { requestingMailboxSetup: true }; return this.send(messageObject); } connectMailbox(mailboxId) { const messageObject = { requestedMailbox: mailboxId }; return this.send(messageObject); } deleteMailbox(mailboxId) { const messageObject = { deletingMailbox: mailboxId }; return this.send(messageObject); } }; // src/Model/Utility/utility.ts function stringify(data) { return JSON.stringify(data); } function parse(string) { return JSON.parse(string); } // src/Model/connectionModel.ts var ConnectionModel = class { // basic udn; // data get isConnected() { return this.udn.ws != void 0 && this.udn.ws.readyState == 1; } get address() { return this.udn.ws?.url; } // connection connect = (address) => { this.udn.connect(address); }; disconnect = () => { this.udn.disconnect(); }; handleConnectionChange = () => { console.log("connection status:", this.isConnected, this.address); }; // messaging sendChatMessage = (chatMessage) => { const stringifiedBody = stringify(chatMessage); return this.udn.sendMessage(chatMessage.channel, stringifiedBody); }; // setup constructor(configuration) { this.udn = new UDNFrontend(); this.udn.onmessage = (data) => { configuration.messageHandler(data); }; this.udn.onconnect = () => { this.handleConnectionChange(); configuration.connectionChangeHandler(); }; this.udn.ondisconnect = () => { this.handleConnectionChange(); configuration.connectionChangeHandler(); }; } }; // src/ViewModel/connectionViewModel.tsx var ConnectionViewModel = class { connectionModel; // state serverAddressInput = new State(""); isConnected = new State(false); // toggles cannotConnect = createProxyState( [this.serverAddressInput, this.isConnected], () => this.isConnected.value == true && this.serverAddressInput.value == this.connectionModel.address || this.serverAddressInput.value == "" ); cannotDisonnect = createProxyState( [this.isConnected], () => this.isConnected.value == false ); // handlers connectionChangeHandler = () => { this.isConnected.value = this.connectionModel.isConnected; if (this.connectionModel.address) { this.serverAddressInput.value = this.connectionModel.address; } }; messageHandler = (data) => { }; // methods connect = () => { this.connectionModel.connect(this.serverAddressInput.value); }; disconnect = () => { this.connectionModel.disconnect(); }; // init constructor() { const connectionModel = new ConnectionModel({ connectionChangeHandler: this.connectionChangeHandler, messageHandler: this.messageHandler }); this.connectionModel = connectionModel; } }; // src/View/Components/option.tsx function Option(text, value, selectedOnCreate) { return /* @__PURE__ */ createElement("option", { value, "toggle:selected": selectedOnCreate }, text); } // src/View/translations.ts var englishTranslations = { general: { closeButton: "close" }, regional: { weekdays: { full: [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ], abbreviated: [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" ] } }, homePage: { appName: "Comms", /// overviewHeadline: "Overview", serverAddress: "Server address", connectAudioLabel: "connect to server", disconnectAudioLabel: "disconnect from server", manageConnectionsAudioLabel: "manage connections", mailboxHeadline: "Server Mailbox", mailboxDisabled: "Mailbox disabled. You will miss out on messages sent while you're away", mailboxEnabled: "Mailbox enabled. If you disconnect, the server will keep your messages temporarily", outboxHeadline: "Outbox", outboxAllItemsSent: "All items sent", yourNameLabel: "Your name", setNameButton: "Set", firstDayOfWeekLabel: "First day of week", scrollToChatButton: "Chats", /// backToOverviewAudioLabel: "go back to overview", chatsHeadline: "Chats", addChatAudioLabel: "name of new chat", addChatPlaceholder: "Add chat", addChatButton: "Add chat" }, settings: { settingsHeadline: "Settings" /// } }; var allTranslations = { en: englishTranslations }; var language = navigator.language.substring(0, 2); var translations = allTranslations[language] || allTranslations.en; // src/View/homePage.tsx function HomePage(settingsViewModel2, connectionViewModel2) { const overviewSection = /* @__PURE__ */ createElement("div", { id: "overview-section" }, /* @__PURE__ */ createElement("h2", null, translations.homePage.overviewHeadline), /* @__PURE__ */ createElement("label", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "cell_tower"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.homePage.serverAddress), /* @__PURE__ */ createElement( "input", { "bind:value": connectionViewModel2.serverAddressInput, "on:enter": connectionViewModel2.connect } ))), /* @__PURE__ */ createElement("div", { class: "flex-row" }, /* @__PURE__ */ createElement( "button", { class: "danger flex justify-center", "aria-label": translations.homePage.disconnectAudioLabel, "on:click": connectionViewModel2.disconnect, "toggle:disabled": connectionViewModel2.cannotDisonnect }, /* @__PURE__ */ createElement("span", { class: "icon" }, "link_off") ), /* @__PURE__ */ createElement( "button", { class: "flex justify-center", "aria-label": translations.homePage.manageConnectionsAudioLabel }, /* @__PURE__ */ createElement("span", { class: "icon" }, "build") ), /* @__PURE__ */ createElement( "button", { class: "primary flex justify-center", "aria-label": translations.homePage.connectAudioLabel, "on:click": connectionViewModel2.connect, "toggle:disabled": connectionViewModel2.cannotConnect }, /* @__PURE__ */ createElement("span", { class: "icon" }, "link") )), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "inbox"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, translations.homePage.mailboxHeadline), /* @__PURE__ */ createElement("span", { class: "error" }, translations.homePage.mailboxDisabled))), /* @__PURE__ */ createElement("div", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "outbox"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, translations.homePage.outboxHeadline), /* @__PURE__ */ createElement("span", { class: "success" }, translations.homePage.outboxAllItemsSent))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("label", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "account_circle"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.homePage.yourNameLabel), /* @__PURE__ */ createElement( "input", { "bind:value": settingsViewModel2.nameInput, "on:enter": settingsViewModel2.setName } ))), /* @__PURE__ */ createElement("div", { class: "flex-row justify-end" }, /* @__PURE__ */ createElement( "button", { class: "width-50", "on:click": settingsViewModel2.setName, "toggle:disabled": settingsViewModel2.cannotSetName }, translations.homePage.setNameButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "check") )), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("label", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "calendar_month"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.homePage.firstDayOfWeekLabel), /* @__PURE__ */ createElement("select", { "bind:value": settingsViewModel2.firstDayOfWeekInput }, ...translations.regional.weekdays.full.map( (weekdayName, i) => Option( weekdayName, i.toString(), i == settingsViewModel2.firstDayOfWeekInput.value ) )), /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_drop_down"))), /* @__PURE__ */ createElement("div", { class: "mobile-only" }, /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "flex-row justify-end" }, /* @__PURE__ */ createElement("button", { class: "ghost width-50", "on:click": scrollToChat }, translations.homePage.scrollToChatButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_forward"))))); const chatSection = /* @__PURE__ */ createElement("div", { id: "chat-section" }, /* @__PURE__ */ createElement("h2", null, translations.homePage.chatsHeadline), /* @__PURE__ */ createElement("div", { class: "flex-row width-input" }, /* @__PURE__ */ createElement( "input", { placeholder: translations.homePage.addChatPlaceholder, "aria-label": translations.homePage.addChatAudioLabel } ), /* @__PURE__ */ createElement( "button", { class: "primary", "aria-label": translations.homePage.addChatButton }, /* @__PURE__ */ createElement("span", { class: "icon" }, "add") ))); function scrollToChat() { chatSection.scrollIntoView(); } return /* @__PURE__ */ createElement("article", { id: "home-page" }, /* @__PURE__ */ createElement("header", null, /* @__PURE__ */ createElement("span", null, translations.homePage.appName)), /* @__PURE__ */ createElement("div", null, overviewSection, chatSection)); } // src/Model/Utility/typeSafety.ts var DATA_VERSION = "v2"; // src/Model/storageModel.ts var StorageModel = class _StorageModel { storageEntryTree = {}; // basic store = (key, value) => { localStorage.setItem(key, value); const pathComponents = _StorageModel.keyToPathComponents(key); this.updateTree(...pathComponents); }; restore = (key) => { return localStorage.getItem(key); }; remove = (key) => { localStorage.removeItem(key); }; list = (key) => { const pathComponents = _StorageModel.keyToPathComponents(key); let currentParent = this.storageEntryTree; for (const component of pathComponents) { const nextParent = currentParent[component]; if (nextParent == void 0) return []; currentParent = nextParent; } return [...Object.keys(currentParent)]; }; // stringifiable storeStringifiable = (key, value) => { const valueString = stringify(value); this.store(key, valueString); }; restoreStringifiable = (key) => { const valueString = this.restore(key); if (!valueString) return null; return parse(valueString); }; // init constructor() { for (const key of Object.keys(localStorage)) { const components = _StorageModel.keyToPathComponents(key); this.updateTree(...components); } } // utility updateTree = (...pathComponents) => { let currentParent = this.storageEntryTree; for (const pathPart of pathComponents) { if (!currentParent[pathPart]) { currentParent[pathPart] = {}; } currentParent = currentParent[pathPart]; } }; static pathComponentsToKey = (...pathComponents) => { return pathComponents.join("/"); }; static keyToPathComponents = (key) => { return key.split("/"); }; static join = (...items) => { let allComponents = []; for (const item of items) { const parts = this.keyToPathComponents(item); allComponents.push(...parts); } return _StorageModel.pathComponentsToKey(...allComponents); }; }; var storageKeys = { // connection socketAddress: `${DATA_VERSION}/connection/socket-address`, // settings username: `${DATA_VERSION}/settings/user-name`, firstDayOfWeek: `${DATA_VERSION}/settings/first-day-of-week`, // history previousAddresses: `${DATA_VERSION}/history/previous-addresses`, previousObjectCategories: `${DATA_VERSION}/history/object-categories`, previousObjectStatuses: `${DATA_VERSION}/history/object-statuses`, previousObjectFilters: `${DATA_VERSION}/history/object-filters`, // chat etc chatInfo: (id) => `${DATA_VERSION}/chat/${id}/info`, chatMessages: (id) => `${DATA_VERSION}/chat/${id}/messages`, chatObjects: (id) => `${DATA_VERSION}/chat/${id}/objects`, chatOutbox: (id) => `${DATA_VERSION}/chat/${id}/outbox` }; // src/Model/settingsModel.ts var SettingsModel = class { storageModel; username; firstDayOfWeek; // set setName(newValue) { this.username = newValue; const path = storageKeys.username; this.storageModel.store(path, newValue); } setFirstDayOfWeek(newValue) { this.firstDayOfWeek = newValue; const path = storageKeys.firstDayOfWeek; this.storageModel.storeStringifiable(path, newValue); } // restore restoreUsername() { const path = storageKeys.username; const content = this.storageModel.restore(path); this.username = content ?? ""; } restoreFirstDayofWeek() { const path = storageKeys.firstDayOfWeek; const content = this.storageModel.restoreStringifiable(path); this.firstDayOfWeek = content ?? 0; } // init constructor(storageModel2) { this.storageModel = storageModel2; this.restoreUsername(); this.restoreFirstDayofWeek(); } }; // src/ViewModel/settingsViewModel.ts var SettingsViewModel = class { settingsModel; // state nameInput = new State(""); firstDayOfWeekInput = new State(0); // toggles cannotSetName = createProxyState( [this.nameInput], () => this.nameInput.value == "" || this.nameInput.value == this.settingsModel.username ); // set setName = () => { this.settingsModel.setName(this.nameInput.value); this.nameInput.callSubscriptions(); }; setFirstDayofWeek = () => { this.settingsModel.setFirstDayOfWeek(this.firstDayOfWeekInput.value); }; // init constructor(storageModel2) { const settingsModel = new SettingsModel(storageModel2); this.settingsModel = settingsModel; this.nameInput.value = settingsModel.username; this.firstDayOfWeekInput.value = settingsModel.firstDayOfWeek; this.firstDayOfWeekInput.subscribe(this.setFirstDayofWeek); } }; // src/index.tsx var storageModel = new StorageModel(); var settingsViewModel = new SettingsViewModel(storageModel); var connectionViewModel = new ConnectionViewModel(); document.querySelector("main").append(HomePage(settingsViewModel, connectionViewModel)); })();