diff --git a/bun.lockb b/bun.lockb index ae69f19f..9f4cce5b 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 5c7cb91a..97dfebb5 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,10 @@ { "dependencies": { + "@types/uuid": "^10.0.0", "bloatless-react": "^1.3.8", "esbuild": "^0.23.0", - "udn-frontend": "^1.0.7-beta1" + "udn-frontend": "^1.0.7-beta1", + "uuid": "^10.0.0" }, "scripts": { "build": "esbuild src/index.tsx --bundle --outdir=dist" diff --git a/src/Model/chatModel.ts b/src/Model/chatModel.ts index b7df4611..68fdf51a 100644 --- a/src/Model/chatModel.ts +++ b/src/Model/chatModel.ts @@ -1,74 +1,177 @@ // this file is responsible for managing chats and chat messages. objects are delegated to objectModel.ts. import StorageModel, { storageKeys } from "./storageModel"; +import { createTimestamp, stringify } from "../utility"; -import { createTimestamp } from "../utility"; +import { checkIsValidObject } from "../typeSafety"; +import { v4 } from "uuid"; export class ChatModel { - info: ChatInfo; + // data + id: string; + storageModel: StorageModel; + info: ChatInfo; - messages: ChatMessage[]; - objects: ChatObject[]; - outbox: ChatMessage[]; + messages = new Set(); + objects = new Map(); + outbox = new Set(); - constructor(storageModel: StorageModel, chatId: string) { - const infoPath = storageKeys.chatInfo(chatId) - const info = storageModel.restoreStringifiable(infoPath); + // paths + get infoPath(): string { + return storageKeys.chatInfo(this.id); + } + + get messageDirPath(): string { + return storageKeys.chatMessages(this.id); + } + + get objectDirPath(): string { + return storageKeys.chatObjects(this.id); + } + + get outboxDirPath(): string { + return storageKeys.chatOutbox(this.id); + } + + getMessagePath(id: string): string { + return StorageModel.join(this.messageDirPath, id); + } + + getObjectPath(id: string): string { + return StorageModel.join(this.objectDirPath, id); + } + + // store & add + storeInfo(): void { + this.storageModel.storeStringifiable(this.infoPath, this.info); + } + + addMessage(message: ChatMessage): void { + if (!this.messages.has(message)) { + this.messages.add(message); } + 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); + this.storageModel.storeStringifiable(objectPath, object); + } + + // restore + restoreInfo(): void { + const info: any = this.storageModel.restoreStringifiable(this.infoPath); + this.info == info ?? generateChatInfo("0"); + } + + restoreMessages(): void { + const messageIds: string[] = this.storageModel.list(this.messageDirPath); + if (!Array.isArray(messageIds)) return; + + for (const messageId of messageIds) { + const messagePath: string = this.getMessagePath(messageId); + const message: any = this.storageModel.restoreStringifiable(messagePath); + if (checkIsValidObject(message) == false) return; + + this.messages.add(message); + } + } + + restoreObjects(): void { + const objectIds: string[] = this.storageModel.list(this.objectDirPath); + if (!Array.isArray(objectIds)) return; + + for (const objectId of objectIds) { + const objectPath: string = this.getObjectPath(objectId); + const object: any = this.storageModel.restoreStringifiable(objectPath); + if (checkIsValidObject(object) == false) return; + if (object.id != objectId) return; + + this.objects.set(objectId, object); + } + } + + // init + constructor(storageModel: StorageModel, chatId: string) { + this.id = chatId; + this.storageModel = storageModel; + + this.restoreInfo(); + this.restoreMessages(); + this.restoreObjects(); + } } // creation methods -export function createChatMessage(channel: string, sender: string, body: string): ChatMessage { - return { - dataVersion: "v2", +export function generateChatInfo(primaryChannel: string): ChatInfo { + return { + primaryChannel, + secondaryChannels: [], + encryptionKey: "", + hasUnreadMessages: false, + }; +} - channel, - sender, - body, - dateSent: createTimestamp(), - } +export function createChatMessage( + channel: string, + sender: string, + body: string +): ChatMessage { + return { + dataVersion: "v2", + + id: v4(), + + channel, + sender, + body, + dateSent: createTimestamp(), + }; } // types export interface ChatInfo { - primaryChannel: string; - secondaryChannels: string[]; - encryptionKey: string; - - hasUnreadMessages: boolean; + primaryChannel: string; + secondaryChannels: string[]; + encryptionKey: string; + + hasUnreadMessages: boolean; } export interface ChatMessage { - dataVersion: "v2"; - - channel: string; - sender: string; - body: string; - dateSent: string; - - stringifiedObject?: string; - } - - export interface ChatObject { - dataVersion: "v2"; - - id: string; - title: string; - - contentVersions: { [key: string]: MessageObjectContent }; - } - - export interface MessageObjectContent { - dataVersion: "v2"; - - id: string; - creationDate: string; - - noteText?: string; - priority?: number; - category?: string; - status?: string; - date?: string; - time?: string; - } - \ No newline at end of file + dataVersion: "v2"; + + id: string; + + channel: string; + sender: string; + body: string; + dateSent: string; + + stringifiedObject?: string; +} + +export interface ChatObject { + dataVersion: "v2"; + + id: string; + title: string; + + contentVersions: { [key: string]: ChatObjectContent }; +} + +export interface ChatObjectContent { + dataVersion: "v2"; + + id: string; + creationDate: string; + + noteText?: string; + priority?: number; + category?: string; + status?: string; + date?: string; + time?: string; +} diff --git a/src/Model/connectionModel.ts b/src/Model/connectionModel.ts index a2bd14ef..26aa19cd 100644 --- a/src/Model/connectionModel.ts +++ b/src/Model/connectionModel.ts @@ -33,7 +33,7 @@ export default class ConnectionModel { // messaging sendChatMessage = (chatMessage: ChatMessage): boolean => { - const stringifiedBody = stringify(chatMessage); + const stringifiedBody: string = stringify(chatMessage); return this.udn.sendMessage(chatMessage.channel, stringifiedBody); }; diff --git a/src/Model/storageModel.ts b/src/Model/storageModel.ts index 17594151..1dd9ad60 100644 --- a/src/Model/storageModel.ts +++ b/src/Model/storageModel.ts @@ -2,13 +2,15 @@ import { parse, stringify } from "../utility"; +import { DATA_VERSION } from "../typeSafety"; + export default class StorageModel { storageEntryTree: StorageEntry = {}; // basic store = (key: string, value: string): void => { localStorage.setItem(key, value); - const pathComponents = this.keyToPathComponents(key); + const pathComponents: string[] = StorageModel.keyToPathComponents(key); this.updateTree(...pathComponents); }; @@ -21,9 +23,9 @@ export default class StorageModel { }; list = (key: string): string[] => { - const pathComponents = this.keyToPathComponents(key); - - let currentParent = this.storageEntryTree; + const pathComponents: string[] = StorageModel.keyToPathComponents(key); + + let currentParent: StorageEntry = this.storageEntryTree; for (const component of pathComponents) { currentParent = currentParent[component]; } @@ -32,13 +34,13 @@ export default class StorageModel { }; // stringifiable - storeStringifiable = (key: string, value: any[]): void => { - const valueString = stringify(value); + storeStringifiable = (key: string, value: any): void => { + const valueString: string = stringify(value); this.store(key, valueString); }; restoreStringifiable = (key: string): any | null => { - const valueString = this.restore(key); + const valueString: string | null = this.restore(key); if (!valueString) return null; return parse(valueString); @@ -47,14 +49,14 @@ export default class StorageModel { // init constructor() { for (const key of Object.keys(localStorage)) { - const components = this.keyToPathComponents(key); + const components: string[] = StorageModel.keyToPathComponents(key); this.updateTree(...components); } } // utility updateTree = (...pathComponents: string[]) => { - let currentParent = this.storageEntryTree; + let currentParent: StorageEntry = this.storageEntryTree; for (const pathPart of pathComponents) { if (!currentParent[pathPart]) { currentParent[pathPart] = {}; @@ -64,13 +66,22 @@ export default class StorageModel { } }; - pathComponentsToKey = (...pathComponents: string[]): string => { + static pathComponentsToKey = (...pathComponents: string[]): string => { return pathComponents.join("/"); }; - keyToPathComponents = (key: string): string[] => { + static keyToPathComponents = (key: string): string[] => { return key.split("/"); }; + + static join = (...items: string[]): string => { + let allComponents: string[] = []; + for (const item of items) { + const parts = this.keyToPathComponents(item); + allComponents.push(...parts); + } + return StorageModel.pathComponentsToKey(...allComponents); + }; } // types @@ -79,21 +90,21 @@ export type StorageEntry = { [key: string]: StorageEntry }; // keys export const storageKeys = { // connection - socketAddress: "v2/connection/socket-address", + socketAddress: `${DATA_VERSION}/connection/socket-address`, // settings - userName: "v2/settings/user-name", - firstDayOfWeek: "v2/settings/first-day-of-week", + userName: `${DATA_VERSION}/settings/user-name`, + firstDayOfWeek: `${DATA_VERSION}/settings/first-day-of-week`, // history - previousAddresses: "v2/history/previous-addresses", - previousObjectCategories: "v2/history/object-categories", - previousObjectStatuses: "v2/history/object-statuses", - previousObjectFilters: "v2/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) => `v2/chat/${id}/info`, - chatMessages: (id: string) => `v2/chat/${id}/messages`, - chatObjects: (id: string) => `v2/chat/${id}/objects`, - chatOutbox: (id: string) => `v2/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/Upgrade/v1.ts b/src/Upgrade/v1.ts index 31d1d548..b4408522 100644 --- a/src/Upgrade/v1.ts +++ b/src/Upgrade/v1.ts @@ -1,8 +1,10 @@ import { ChatMessage, ChatObject, - MessageObjectContent, -} from "../Model/chatTypes"; + ChatObjectContent, +} from "../Model/chatModel"; + +import { v4 } from "uuid"; // ChatMessage export interface ChatMessageV1 { @@ -21,6 +23,8 @@ export function upgradeMessageFromV1( return { dataVersion: "v2", + id: v4(), + channel, sender, body, @@ -41,7 +45,7 @@ export function upgradeMessageObjectFromV1( ): ChatObject { const { id, title, contentVersions } = messageObjectV1; - const upgradedContentVersions: { [key: string]: MessageObjectContent } = {}; + const upgradedContentVersions: { [key: string]: ChatObjectContent } = {}; for (const entry of Object.entries(contentVersions)) { const [id, object] = entry; const upgraded = upgradeMessageObjectContentFromV1(object); @@ -73,7 +77,7 @@ export interface MessageObjectContentV1 { export function upgradeMessageObjectContentFromV1( messageObjectContentV1: MessageObjectContentV1 -): MessageObjectContent { +): ChatObjectContent { const { isoDateVersionCreated, id, diff --git a/src/typeSafety.ts b/src/typeSafety.ts new file mode 100644 index 00000000..c9becd2f --- /dev/null +++ b/src/typeSafety.ts @@ -0,0 +1,5 @@ +export const DATA_VERSION = "v2" + +export function checkIsValidObject(object: any): boolean { + return object.dataVersion == DATA_VERSION; +}