From ff27112733d135a7366f8e517b33e36db00fa721 Mon Sep 17 00:00:00 2001 From: marlon-erler Date: Sat, 20 Jul 2024 02:56:44 +0200 Subject: [PATCH] add history of previous connections, remodel storageModel to use backslash as path separator --- dist/index.js | 292 +++++++++++++++++--------- src/Model/chatModel.ts | 33 +-- src/Model/connectionModel.ts | 42 +++- src/Model/storageModel.ts | 50 ++--- src/View/Components/option.tsx | 6 + src/View/Modals/connectionModal.tsx | 6 +- src/View/homePage.tsx | 12 +- src/View/translations.ts | 4 +- src/ViewModel/connectionViewModel.tsx | 17 +- src/index.tsx | 3 +- 10 files changed, 309 insertions(+), 156 deletions(-) diff --git a/dist/index.js b/dist/index.js index 23d4c5b9..b326b2c0 100644 --- a/dist/index.js +++ b/dist/index.js @@ -32,6 +32,50 @@ return JSON.stringify(this._value); } }; + var ListState = class extends State { + additionHandlers = /* @__PURE__ */ new Set(); + removalHandlers = /* @__PURE__ */ new Map(); + // init + constructor(initialItems) { + super(new Set(initialItems)); + } + // list + add(...items) { + items.forEach((item) => { + this.value.add(item); + this.additionHandlers.forEach((handler) => handler(item)); + }); + this.callSubscriptions(); + } + remove(...items) { + items.forEach((item) => { + this.value.delete(item); + if (!this.removalHandlers.has(item)) return; + this.removalHandlers.get(item).forEach((handler) => handler(item)); + this.removalHandlers.delete(item); + }); + this.callSubscriptions(); + } + clear() { + this.remove(...this.value.values()); + } + // handlers + handleAddition(handler) { + this.additionHandlers.add(handler); + [...this.value.values()].forEach(handler); + } + handleRemoval(item, handler) { + if (!this.removalHandlers.has(item)) + this.removalHandlers.set(item, /* @__PURE__ */ new Set()); + this.removalHandlers.get(item).add(handler); + } + // stringification + toString() { + const array = [...this.value.values()]; + const json = JSON.stringify(array); + return json; + } + }; function createProxyState(statesToSubscibe, fn) { const proxyState = new State(fn()); statesToSubscibe.forEach( @@ -192,8 +236,8 @@ addChatPlaceholder: "Add chat", addChatButton: "Add chat" }, - settings: { - settingsHeadline: "Settings" + connectionModal: { + connectionModalHeadline: "Manage Connections" /// } }; @@ -211,10 +255,108 @@ class: "modal", "toggle:open": connectionViewModel2.isShowingConnectionModal }, - /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("main", null), /* @__PURE__ */ createElement("button", { "on:click": connectionViewModel2.hideConnectionModal }, translations.general.closeButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "close"))) + /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("main", null, /* @__PURE__ */ createElement("h2", null, translations.connectionModal.connectionModalHeadline)), /* @__PURE__ */ createElement("button", { "on:click": connectionViewModel2.hideConnectionModal }, translations.general.closeButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "close"))) ); } + // src/Model/Utility/utility.ts + function stringify(data) { + return JSON.stringify(data); + } + function parse(string) { + return JSON.parse(string); + } + + // src/Model/Utility/typeSafety.ts + var DATA_VERSION = "v2"; + + // src/Model/storageModel.ts + var PATH_COMPONENT_SEPARATOR = "\\"; + var StorageModel = class _StorageModel { + storageEntryTree = {}; + // basic + store = (pathComponents, value) => { + const key = _StorageModel.pathComponentsToKey(...pathComponents); + localStorage.setItem(key, value); + this.updateTree(...pathComponents); + }; + restore = (pathComponents) => { + const key = _StorageModel.pathComponentsToKey(...pathComponents); + return localStorage.getItem(key); + }; + remove = (pathComponents) => { + const key = _StorageModel.pathComponentsToKey(...pathComponents); + localStorage.removeItem(key); + }; + list = (pathComponents) => { + 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 = (pathComponents, value) => { + const valueString = stringify(value); + this.store(pathComponents, valueString); + }; + restoreStringifiable = (pathComponents) => { + const valueString = this.restore(pathComponents); + 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(PATH_COMPONENT_SEPARATOR); + }; + static keyToPathComponents = (key) => { + return key.split(PATH_COMPONENT_SEPARATOR); + }; + 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"] + }; + // node_modules/udn-frontend/index.ts var UDNFrontend = class { ws; @@ -327,18 +469,10 @@ } }; - // 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; + storageModel; // data get isConnected() { return this.udn.ws != void 0 && this.udn.ws.readyState == 1; @@ -355,15 +489,37 @@ }; handleConnectionChange = () => { console.log("connection status:", this.isConnected, this.address); + if (this.isConnected == false) return; + if (this.address) { + this.storeAddress(this.address); + } }; // messaging sendChatMessage = (chatMessage) => { const stringifiedBody = stringify(chatMessage); return this.udn.sendMessage(chatMessage.channel, stringifiedBody); }; + // storage + getAddressPath = (address) => { + const dirPath = storageKeys.previousAddresses; + return [...dirPath, address]; + }; + storeAddress = (address) => { + const addressPath = this.getAddressPath(address); + this.storageModel.store(addressPath, ""); + }; + removeAddress = (address) => { + const addressPath = this.getAddressPath(address); + this.storageModel.remove(addressPath); + }; + getAddresses = () => { + const dirPath = storageKeys.previousAddresses; + return this.storageModel.list(dirPath); + }; // setup constructor(configuration) { this.udn = new UDNFrontend(); + this.storageModel = configuration.storageModel; this.udn.onmessage = (data) => { configuration.messageHandler(data); }; @@ -385,6 +541,7 @@ serverAddressInput = new State(""); isConnected = new State(false); isShowingConnectionModal = new State(false); + previousAddresses = new ListState(); // toggles cannotConnect = createProxyState( [this.serverAddressInput, this.isConnected], @@ -397,9 +554,12 @@ // handlers connectionChangeHandler = () => { this.isConnected.value = this.connectionModel.isConnected; + if (this.connectionModel.isConnected == false) return; if (this.connectionModel.address) { this.serverAddressInput.value = this.connectionModel.address; } + this.previousAddresses.clear(); + this.previousAddresses.add(...this.connectionModel.getAddresses()); }; messageHandler = (data) => { }; @@ -418,8 +578,9 @@ this.isShowingConnectionModal.value = false; }; // init - constructor() { + constructor(storageModel2) { const connectionModel = new ConnectionModel({ + storageModel: storageModel2, connectionChangeHandler: this.connectionChangeHandler, messageHandler: this.messageHandler }); @@ -431,16 +592,30 @@ function Option(text, value, selectedOnCreate) { return /* @__PURE__ */ createElement("option", { value, "toggle:selected": selectedOnCreate }, text); } + var StringToOption = (string) => { + return Option(string, string, false); + }; // 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", { + list: "previous-connection-list", placeholder: translations.homePage.serverAddressPlaceholder, "bind:value": connectionViewModel2.serverAddressInput, "on:enter": connectionViewModel2.connect } + ), /* @__PURE__ */ createElement( + "datalist", + { + hidden: true, + id: "previous-connection-list", + "children:append": [ + connectionViewModel2.previousAddresses, + StringToOption + ] + } ))), /* @__PURE__ */ createElement("div", { class: "flex-row" }, /* @__PURE__ */ createElement( "button", { @@ -510,94 +685,6 @@ 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; @@ -664,8 +751,9 @@ // src/index.tsx var storageModel = new StorageModel(); + console.log(JSON.stringify(storageModel.storageEntryTree, null, 4)); var settingsViewModel = new SettingsViewModel(storageModel); - var connectionViewModel = new ConnectionViewModel(); + var connectionViewModel = new ConnectionViewModel(storageModel); document.querySelector("main").append( HomePage(settingsViewModel, connectionViewModel), ConnectionModal(connectionViewModel) diff --git a/src/Model/chatModel.ts b/src/Model/chatModel.ts index 8d738cd2..0ce48473 100644 --- a/src/Model/chatModel.ts +++ b/src/Model/chatModel.ts @@ -17,28 +17,28 @@ export class ChatModel { outbox = new Set(); // paths - get infoPath(): string { + get infoPath(): string[] { return storageKeys.chatInfo(this.id); } - get messageDirPath(): string { + get messageDirPath(): string[] { return storageKeys.chatMessages(this.id); } - get objectDirPath(): string { + get objectDirPath(): string[] { return storageKeys.chatObjects(this.id); } - get outboxDirPath(): string { + get outboxDirPath(): string[] { return storageKeys.chatOutbox(this.id); } - getMessagePath(id: string): string { - return StorageModel.join(this.messageDirPath, id); + getMessagePath(id: string): string[] { + return [...this.messageDirPath, id]; } - getObjectPath(id: string): string { - return StorageModel.join(this.objectDirPath, id); + getObjectPath(id: string): string[] { + return [...this.objectDirPath, id]; } // set @@ -61,13 +61,13 @@ export class ChatModel { if (!this.messages.has(message)) { this.messages.add(message); } - const messagePath: string = this.getMessagePath(message.id); + const messagePath: string[] = this.getMessagePath(message.id); this.storageModel.storeStringifiable(messagePath, message); } addObject(object: ChatObject): void { this.objects.set(object.id, object); - const objectPath: string = this.getObjectPath(object.id); + const objectPath: string[] = this.getObjectPath(object.id); this.storageModel.storeStringifiable(objectPath, object); } @@ -82,7 +82,7 @@ export class ChatModel { if (!Array.isArray(messageIds)) return; for (const messageId of messageIds) { - const messagePath: string = this.getMessagePath(messageId); + const messagePath: string[] = this.getMessagePath(messageId); const message: any = this.storageModel.restoreStringifiable(messagePath); if (checkIsValidObject(message) == false) return; @@ -95,8 +95,8 @@ export class ChatModel { if (!Array.isArray(objectIds)) return; for (const objectId of objectIds) { - const objectPath: string = this.getObjectPath(objectId); - const object: any = this.storageModel.restoreStringifiable(objectPath); + const objectPathComponents: string[] = this.getObjectPath(objectId); + const object: any = this.storageModel.restoreStringifiable(objectPathComponents); if (checkIsValidObject(object) == false) return; if (object.id != objectId) return; @@ -116,12 +116,15 @@ export class ChatModel { } // creation methods -export function createChat(storageModel: StorageModel, primaryChannel: string): ChatModel { +export function createChat( + storageModel: StorageModel, + primaryChannel: string +): ChatModel { const id: string = v4(); const chatModel = new ChatModel(storageModel, id); chatModel.setPrimaryChannel(primaryChannel); - + return chatModel; } diff --git a/src/Model/connectionModel.ts b/src/Model/connectionModel.ts index dc3f5643..a11b7d29 100644 --- a/src/Model/connectionModel.ts +++ b/src/Model/connectionModel.ts @@ -1,13 +1,14 @@ // this file is responsible for managing UDN connections. +import StorageModel, { storageKeys } from "./storageModel"; import UDNFrontend, { Message } from "udn-frontend"; import { ChatMessage } from "./chatModel"; import { stringify } from "./Utility/utility"; export default class ConnectionModel { - // basic udn: UDNFrontend; + storageModel: StorageModel; // data get isConnected(): boolean { @@ -29,6 +30,11 @@ export default class ConnectionModel { handleConnectionChange = (): void => { console.log("connection status:", this.isConnected, this.address); + if (this.isConnected == false) return; + + if (this.address) { + this.storeAddress(this.address); + } }; // messaging @@ -37,28 +43,52 @@ export default class ConnectionModel { return this.udn.sendMessage(chatMessage.channel, stringifiedBody); }; + // storage + private getAddressPath = (address: string): string[] => { + const dirPath = storageKeys.previousAddresses; + return [...dirPath, address]; + }; + + storeAddress = (address: string): void => { + const addressPath = this.getAddressPath(address); + this.storageModel.store(addressPath, ""); + }; + + removeAddress = (address: string): void => { + const addressPath = this.getAddressPath(address); + this.storageModel.remove(addressPath); + }; + + getAddresses = (): string[] => { + const dirPath = storageKeys.previousAddresses; + return this.storageModel.list(dirPath); + }; + // setup constructor(configuration: ConnectionModelConfiguration) { // create frontend this.udn = new UDNFrontend(); + this.storageModel = configuration.storageModel; // setup handlers this.udn.onmessage = (data: Message) => { - configuration.messageHandler(data); + configuration.messageHandler(data); }; this.udn.onconnect = () => { - this.handleConnectionChange(); - configuration.connectionChangeHandler(); + this.handleConnectionChange(); + configuration.connectionChangeHandler(); }; this.udn.ondisconnect = () => { - this.handleConnectionChange(); - configuration.connectionChangeHandler(); + this.handleConnectionChange(); + configuration.connectionChangeHandler(); }; } } // types export interface ConnectionModelConfiguration { + storageModel: StorageModel; + connectionChangeHandler: () => void; messageHandler: (data: Message) => void; } diff --git a/src/Model/storageModel.ts b/src/Model/storageModel.ts index 4526a6b3..b1da33fb 100644 --- a/src/Model/storageModel.ts +++ b/src/Model/storageModel.ts @@ -4,27 +4,29 @@ import { parse, stringify } from "./Utility/utility"; import { DATA_VERSION } from "./Utility/typeSafety"; +const PATH_COMPONENT_SEPARATOR = "\\"; + export default class StorageModel { storageEntryTree: StorageEntry = {}; // basic - store = (key: string, value: string): void => { + store = (pathComponents: string[], value: string): void => { + const key: string = StorageModel.pathComponentsToKey(...pathComponents); localStorage.setItem(key, value); - const pathComponents: string[] = StorageModel.keyToPathComponents(key); this.updateTree(...pathComponents); }; - restore = (key: string): string | null => { + restore = (pathComponents: string[]): string | null => { + const key: string = StorageModel.pathComponentsToKey(...pathComponents); return localStorage.getItem(key); }; - remove = (key: string): void => { + remove = (pathComponents: string[]): void => { + const key: string = StorageModel.pathComponentsToKey(...pathComponents); localStorage.removeItem(key); }; - list = (key: string): string[] => { - const pathComponents: string[] = StorageModel.keyToPathComponents(key); - + list = (pathComponents: string[]): string[] => { let currentParent: StorageEntry = this.storageEntryTree; for (const component of pathComponents) { const nextParent: StorageEntry | undefined = currentParent[component]; @@ -36,13 +38,13 @@ export default class StorageModel { }; // stringifiable - storeStringifiable = (key: string, value: any): void => { + storeStringifiable = (pathComponents: string[], value: any): void => { const valueString: string = stringify(value); - this.store(key, valueString); + this.store(pathComponents, valueString); }; - restoreStringifiable = (key: string): any | null => { - const valueString: string | null = this.restore(key); + restoreStringifiable = (pathComponents: string[]): any | null => { + const valueString: string | null = this.restore(pathComponents); if (!valueString) return null; return parse(valueString); @@ -69,11 +71,11 @@ export default class StorageModel { }; static pathComponentsToKey = (...pathComponents: string[]): string => { - return pathComponents.join("/"); + return pathComponents.join(PATH_COMPONENT_SEPARATOR); }; static keyToPathComponents = (key: string): string[] => { - return key.split("/"); + return key.split(PATH_COMPONENT_SEPARATOR); }; static join = (...items: string[]): string => { @@ -92,21 +94,21 @@ export type StorageEntry = { [key: string]: StorageEntry }; // keys export const storageKeys = { // connection - socketAddress: `${DATA_VERSION}/connection/socket-address`, + socketAddress: [DATA_VERSION, "connection", "socket-address"], // settings - username: `${DATA_VERSION}/settings/user-name`, - firstDayOfWeek: `${DATA_VERSION}/settings/first-day-of-week`, + 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`, + 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: string) => `${DATA_VERSION}/chat/${id}/info`, - chatMessages: (id: string) => `${DATA_VERSION}/chat/${id}/messages`, - chatObjects: (id: string) => `${DATA_VERSION}/chat/${id}/objects`, - chatOutbox: (id: string) => `${DATA_VERSION}/chat/${id}/outbox`, + chatInfo: (id: string) => [DATA_VERSION, "chat", id, "info"], + chatMessages: (id: string) => [DATA_VERSION, "chat", id, "messages"], + chatObjects: (id: string) => [DATA_VERSION, "chat", id, "objects"], + chatOutbox: (id: string) => [DATA_VERSION, "chat", id, "outbox"], }; diff --git a/src/View/Components/option.tsx b/src/View/Components/option.tsx index aeeb863c..4a98ad00 100644 --- a/src/View/Components/option.tsx +++ b/src/View/Components/option.tsx @@ -7,3 +7,9 @@ export function Option(text: string, value: string, selectedOnCreate: boolean) { ); } + +export const StringToOption: React.StateItemConverter = ( + string: string +) => { + return Option(string, string, false); +}; diff --git a/src/View/Modals/connectionModal.tsx b/src/View/Modals/connectionModal.tsx index b4f63237..5b746cdd 100644 --- a/src/View/Modals/connectionModal.tsx +++ b/src/View/Modals/connectionModal.tsx @@ -10,7 +10,11 @@ export function ConnectionModal(connectionViewModel: ConnectionViewModel) { toggle:open={connectionViewModel.isShowingConnectionModal} >
-
+
+

{translations.connectionModal.connectionModalHeadline}

+ + +