diff --git a/dist/index.js b/dist/index.js index f9899ab6..d19461ff 100644 --- a/dist/index.js +++ b/dist/index.js @@ -325,7 +325,7 @@ const stringEntryObjectValue = stringEntryObject[referenceKey]; if (referenceValue == void 0) return false; if (referenceValue[0] == "-") { - const strippedReferenceValue = referenceValue.substring(1); + const strippedReferenceValue = referenceValue.toString().substring(1); if (strippedReferenceValue == "" && stringEntryObjectValue != void 0 && stringEntryObjectValue != "") { return false; } @@ -352,7 +352,7 @@ const stringEntryObjectValue = stringEntryObject[key]; if (stringEntryObjectValue == void 0 || stringEntryObjectValue == "") continue; - values.add(stringEntryObjectValue); + values.add(stringEntryObjectValue.toString()); } return [...values.values()]; } @@ -361,7 +361,7 @@ const stringsInObject = getStringsOfObject(object); const wordsInObject = []; for (const string of stringsInObject) { - const lowercaseWordsInString = string.toLocaleLowerCase().split(" ").filter((word) => word != ""); + const lowercaseWordsInString = string.toString().toLowerCase().split(" ").filter((word) => word != ""); wordsInObject.push(...lowercaseWordsInString); } const lowercaseWordsInQuery = query.toLowerCase().split(" ").filter((word) => word != ""); @@ -418,6 +418,12 @@ this.itemToString = itemToString; } }; + function getLocalStorageItemAndClear(key) { + const value = localStorage.getItem(key); + localStorage.removeItem(key); + console.log(localStorage.getItem(key)); + return value; + } function stringify(data) { return JSON.stringify(data, null, 4); } @@ -441,6 +447,18 @@ if (doesMatchReference == false) return null; return parsed; } + function parseOrFallback(inputString) { + try { + return JSON.parse(inputString); + } catch { + return inputString; + } + } + function parseArray(inputString) { + const parsed = parseOrFallback(inputString); + if (Array.isArray(parsed) == false) return []; + return parsed; + } function localeCompare(a, b) { return a.localeCompare(b); } @@ -2446,6 +2464,9 @@ // src/View/translations.ts var englishTranslations = { + updater: { + migrated: "Migrated" + }, general: { deleteItemButtonAudioLabel: "delete item", searchButtonAudioLabel: "search", @@ -4634,6 +4655,203 @@ }; }; + // src/Upgrader/v1.ts + var v1Upgrader = class { + // init + constructor(settingsModel2, connectionModel2, chatListModel2) { + this.settingsModel = settingsModel2; + this.connectionModel = connectionModel2; + this.chatListModel = chatListModel2; + this.migrateSettings(); + this.migrateConnections(); + this.migrateChats(); + } + // general + migrateSettings = () => { + const name = getLocalStorageItemAndClear("sender-name"); + if (name != null) { + const parsedName = parseOrFallback(name); + this.settingsModel.setName(parsedName); + } + const firstDayOfWeek = getLocalStorageItemAndClear("first-day-of-week"); + if (firstDayOfWeek != null) { + const parsedFirstDayOfWeek = parseOrFallback(firstDayOfWeek); + this.settingsModel.setFirstDayOfWeek(parsedFirstDayOfWeek); + } + }; + migrateConnections = () => { + const previousAddressString = getLocalStorageItemAndClear("previous-addresses"); + if (previousAddressString == null) return; + const previousAddresses = parseArray(previousAddressString); + for (const address of previousAddresses) { + if (typeof address != "string") continue; + this.connectionModel.storeAddress(address); + } + }; + // chats + migrateChats = () => { + const chatIdString = getLocalStorageItemAndClear("chat-ids"); + if (chatIdString == null) return; + const chatIds = parseArray(chatIdString); + for (const chatId of chatIds) { + if (typeof chatId != "string") continue; + this.migrateChatById(chatId); + } + }; + migrateChatById = (id) => { + const primaryChannel = getLocalStorageItemAndClear( + storageKeys.primaryChannel(id) + ); + if (primaryChannel == null) return; + const parsedPriamryChannel = parseOrFallback(primaryChannel); + const chatModel = this.chatListModel.createChat(parsedPriamryChannel); + const secondaryChannelString = getLocalStorageItemAndClear( + storageKeys.secondaryChannels(id) + ); + if (secondaryChannelString != null) { + const potentialSecondaryChannels = parseArray( + secondaryChannelString + ); + const confirmedSecondaryChannels = []; + for (const secondaryChannel of potentialSecondaryChannels) { + if (typeof secondaryChannel != "string") continue; + confirmedSecondaryChannels.push(secondaryChannel); + } + chatModel.setSecondaryChannels(confirmedSecondaryChannels); + } + const encryptionKey = getLocalStorageItemAndClear( + storageKeys.encyptionKey(id) + ); + if (encryptionKey != null) { + const parsedEncryptionKey = parseOrFallback(encryptionKey); + chatModel.setEncryptionKey(parsedEncryptionKey); + } + const messagesString = getLocalStorageItemAndClear(storageKeys.messages(id)) ?? ""; + const messageOutboxString = getLocalStorageItemAndClear(storageKeys.outbox(id)) ?? ""; + const potentialMessages = parseArray(messagesString); + const potentialMessagesInOutbox = parseArray(messageOutboxString); + const addMessages = (potentialMessages2, status) => { + for (const potentialMessage of potentialMessages2) { + const isV1ChatMessage = checkMatchesObjectStructure( + potentialMessage, + V1ChatMessageReference + ); + if (isV1ChatMessage == false) continue; + const v1ChatMessage = potentialMessage; + const convertedChatMessage = { + dataVersion: "v2", + id: v4_default(), + channel: v1ChatMessage.channel, + sender: v1ChatMessage.sender, + body: v1ChatMessage.body, + dateSent: v1ChatMessage.isoDate, + status, + stringifiedFile: "" + }; + chatModel.addMessage(convertedChatMessage); + if (status == "outbox" /* Outbox */) { + this.connectionModel.sendMessageOrStore(convertedChatMessage); + } + } + }; + addMessages(potentialMessages, "received" /* Received */); + addMessages(potentialMessagesInOutbox, "outbox" /* Outbox */); + const objectsString = getLocalStorageItemAndClear(storageKeys.objects(id)) ?? ""; + const objectOutboxString = getLocalStorageItemAndClear(storageKeys.itemOutbox(id)) ?? ""; + const potentialObjects = parseArray(objectsString); + const potentialObjectsInOutbox = parseArray(objectOutboxString); + const addObjects = (potentialObjects2) => { + const board = chatModel.fileModel.boardsAndTasksModel.createBoard( + translations.updater.migrated + ); + chatModel.fileModel.boardsAndTasksModel.updateBoard(board); + for (const potentialObjectEntry of potentialObjects2) { + const potentialObject = potentialObjectEntry[1]; + const isV1MessageObject = checkIsV1MessageObject(potentialObject); + if (isV1MessageObject == false) continue; + const objectId = potentialObject.id; + const objectName = potentialObject.title; + const contentVersions = Object.values( + potentialObject.contentVersions + ); + for (const potentialVersion of contentVersions) { + const isV1MessageObjectContent = checkIsV1MessageObjectContent(potentialVersion); + if (isV1MessageObjectContent == false) continue; + const convertedTaskFileContent = { + dataVersion: "v2", + fileId: objectId, + fileContentId: FileModel2.generateFileContentId( + potentialVersion.isoDateVersionCreated + ), + creationDate: potentialVersion.isoDateVersionCreated, + type: "task", + name: objectName ?? "", + boardId: board.fileId ?? "", + description: potentialVersion.noteContent ?? "", + category: potentialVersion.categoryName ?? "", + status: potentialVersion.status ?? "", + priority: potentialVersion.priority ?? "", + date: potentialVersion.date ?? "", + time: potentialVersion.time ?? "" + }; + console.log(convertedTaskFileContent); + chatModel.fileModel.handleFileContent(convertedTaskFileContent); + } + } + }; + addObjects([...potentialObjects, ...potentialObjectsInOutbox]); + }; + }; + var storageKeys = { + viewType(id) { + return id + "view-type"; + }, + hasUnread(id) { + return id + "has-unread-messages"; + }, + primaryChannel(id) { + return id + "primary-channel"; + }, + secondaryChannels(id) { + return id + "secondary-channels"; + }, + encyptionKey(id) { + return id + "encryption-key"; + }, + messages(id) { + return id + "messages"; + }, + objects(id) { + return id + "items"; + }, + outbox(id) { + return id + "outbox"; + }, + itemOutbox(id) { + return id + "item-outbox"; + }, + composingMessage(id) { + return id + "composing-message"; + } + }; + var V1ChatMessageReference = { + channel: "", + sender: "", + body: "", + isoDate: "" + }; + function checkIsV1MessageObject(object) { + if (object.id == void 0) return false; + if (object.title == void 0) return false; + if (object.contentVersions == void 0) return false; + return true; + } + function checkIsV1MessageObjectContent(object) { + if (object.id == void 0) return false; + if (object.isoDateVersionCreated == void 0) return false; + return true; + } + // src/index.tsx var storageModel = new StorageModel(); var settingsModel = new SettingsModel(storageModel); @@ -4643,6 +4861,7 @@ settingsModel, connectionModel ); + new v1Upgrader(settingsModel, connectionModel, chatListModel); var coreVieWModel = new CoreViewModel(); var storageViewModel = new StorageViewModel(coreVieWModel, storageModel); var settingsViewModel = new SettingsViewModel(coreVieWModel, settingsModel); diff --git a/src/Model/Chat/chatModel.ts b/src/Model/Chat/chatModel.ts index 5ca178a2..1c93d068 100644 --- a/src/Model/Chat/chatModel.ts +++ b/src/Model/Chat/chatModel.ts @@ -316,6 +316,7 @@ export enum ChatMessageStatus { Outbox = "outbox", Sent = "sent", Received = "received", + Other = "other", } export interface ChatMessage extends ValidObject { diff --git a/src/Model/Files/fileModel.ts b/src/Model/Files/fileModel.ts index cf463b4a..feda16d4 100644 --- a/src/Model/Files/fileModel.ts +++ b/src/Model/Files/fileModel.ts @@ -154,7 +154,7 @@ export default class FileModel { } // utility - static generateFileContentId = (creationDate): string => { + static generateFileContentId = (creationDate: string): string => { return creationDate + v4(); }; diff --git a/src/Model/Global/connectionModel.ts b/src/Model/Global/connectionModel.ts index 1da1b689..bb81c486 100644 --- a/src/Model/Global/connectionModel.ts +++ b/src/Model/Global/connectionModel.ts @@ -1,7 +1,7 @@ // this file is responsible for managing UDN connections. import { ChatMessage, ChatMessageReference } from "../Chat/chatModel"; -import { Handler, HandlerManager, stringify } from "../Utility/utility"; +import { HandlerManager, stringify } from "../Utility/utility"; import StorageModel, { StorageModelSubPath, filePaths } from "./storageModel"; import UDNFrontend, { Message } from "udn-frontend"; diff --git a/src/Model/Utility/utility.ts b/src/Model/Utility/utility.ts index 85598cfc..2ee5cf90 100644 --- a/src/Model/Utility/utility.ts +++ b/src/Model/Utility/utility.ts @@ -1,12 +1,16 @@ import { checkIsValidObject, checkMatchesObjectStructure } from "./typeSafety"; +export interface Stringifiable { + toString(): string; +} + // date export function createTimestamp(): string { return new Date().toISOString(); } // filters etc -export type StringEntryObject = { [key: string]: string | undefined }; +export type StringEntryObject = { [key: string]: Stringifiable | undefined }; export function filterObjectsByStringEntries( reference: StringEntryObject, @@ -34,13 +38,15 @@ export function checkDoesObjectMatchReference( reference )) { const [referenceKey, referenceValue] = referenceEntry; - const stringEntryObjectValue: string | undefined = + const stringEntryObjectValue: Stringifiable | undefined = stringEntryObject[referenceKey]; if (referenceValue == undefined) return false; if (referenceValue[0] == "-") { - const strippedReferenceValue: string = referenceValue.substring(1); + const strippedReferenceValue: string = referenceValue + .toString() + .substring(1); // property may not exist if ( strippedReferenceValue == "" && @@ -83,11 +89,12 @@ export function collectObjectValuesForKey( for (const object of objects) { const stringEntryObject: StringEntryObject = converter(object); - const stringEntryObjectValue: string | undefined = stringEntryObject[key]; + const stringEntryObjectValue: Stringifiable | undefined = + stringEntryObject[key]; if (stringEntryObjectValue == undefined || stringEntryObjectValue == "") continue; - values.add(stringEntryObjectValue); + values.add(stringEntryObjectValue.toString()); } return [...values.values()]; @@ -123,7 +130,8 @@ export function checkDoesObjectMatchSearch( const wordsInObject: string[] = []; for (const string of stringsInObject) { const lowercaseWordsInString = string - .toLocaleLowerCase() + .toString() + .toLowerCase() .split(" ") .filter((word) => word != ""); wordsInObject.push(...lowercaseWordsInString); @@ -204,7 +212,15 @@ export class IndexManager { } } -// string +// storage +export function getLocalStorageItemAndClear(key: string): string | null { + const value: string | null = localStorage.getItem(key); + localStorage.removeItem(key); + console.log(localStorage.getItem(key)); + return value; +} + +// string & parsing export function stringify(data: any): string { return JSON.stringify(data, null, 4); } @@ -234,6 +250,20 @@ export function parseValidObject(string: string, reference: T): T | null { return parsed; } +export function parseOrFallback(inputString: string): any { + try { + return JSON.parse(inputString); + } catch { + return inputString; + } +} + +export function parseArray(inputString: string): any[] { + const parsed: any = parseOrFallback(inputString); + if (Array.isArray(parsed) == false) return []; + return parsed; +} + // sort export function localeCompare(a: string, b: string): number { return a.localeCompare(b); diff --git a/src/Upgrader/v1.ts b/src/Upgrader/v1.ts new file mode 100644 index 00000000..2e0694ca --- /dev/null +++ b/src/Upgrader/v1.ts @@ -0,0 +1,281 @@ +// this file is responsible for migrating data from v1 to v2. + +import { + BoardInfoFileContent, + TaskFileContent, +} from "../Model/Files/boardsAndTasksModel"; +import ChatModel, { + ChatMessage, + ChatMessageStatus, +} from "../Model/Chat/chatModel"; +import { getLocalStorageItemAndClear, parseArray, parseOrFallback } from "../Model/Utility/utility"; + +import ChatListModel from "../Model/Chat/chatListModel"; +import ConnectionModel from "../Model/Global/connectionModel"; +import FileModel from "../Model/Files/fileModel"; +import SettingsModel from "../Model/Global/settingsModel"; +import { checkMatchesObjectStructure } from "../Model/Utility/typeSafety"; +import { translations } from "../View/translations"; +import { v4 } from "uuid"; + +export default class v1Upgrader { + // general + migrateSettings = (): void => { + const name: string | null = getLocalStorageItemAndClear("sender-name"); + if (name != null) { + const parsedName: string = parseOrFallback(name); + this.settingsModel.setName(parsedName); + } + + const firstDayOfWeek: string | null = + getLocalStorageItemAndClear("first-day-of-week"); + if (firstDayOfWeek != null) { + const parsedFirstDayOfWeek: string = parseOrFallback(firstDayOfWeek); + this.settingsModel.setFirstDayOfWeek(parsedFirstDayOfWeek); + } + }; + + migrateConnections = (): void => { + const previousAddressString: string | null = + getLocalStorageItemAndClear("previous-addresses"); + if (previousAddressString == null) return; + + const previousAddresses: any[] = parseArray(previousAddressString); + for (const address of previousAddresses) { + if (typeof address != "string") continue; + this.connectionModel.storeAddress(address); + } + }; + + // chats + migrateChats = (): void => { + const chatIdString: string | null = getLocalStorageItemAndClear("chat-ids"); + if (chatIdString == null) return; + + const chatIds: any[] = parseArray(chatIdString); + for (const chatId of chatIds) { + if (typeof chatId != "string") continue; + this.migrateChatById(chatId); + } + }; + + migrateChatById = (id: string): void => { + // get primary channel + const primaryChannel: string | null = getLocalStorageItemAndClear( + storageKeys.primaryChannel(id) + ); + if (primaryChannel == null) return; + const parsedPriamryChannel: string = parseOrFallback(primaryChannel); + + // create chat + const chatModel: ChatModel = + this.chatListModel.createChat(parsedPriamryChannel); + + // secondary channels + const secondaryChannelString: string | null = getLocalStorageItemAndClear( + storageKeys.secondaryChannels(id) + ); + if (secondaryChannelString != null) { + const potentialSecondaryChannels: any[] = parseArray( + secondaryChannelString + ); + + const confirmedSecondaryChannels: string[] = []; + for (const secondaryChannel of potentialSecondaryChannels) { + if (typeof secondaryChannel != "string") continue; + confirmedSecondaryChannels.push(secondaryChannel); + } + + chatModel.setSecondaryChannels(confirmedSecondaryChannels); + } + + // encryption key + const encryptionKey: string | null = getLocalStorageItemAndClear( + storageKeys.encyptionKey(id) + ); + if (encryptionKey != null) { + const parsedEncryptionKey: string = parseOrFallback(encryptionKey); + chatModel.setEncryptionKey(parsedEncryptionKey); + } + + // messages + const messagesString: string = + getLocalStorageItemAndClear(storageKeys.messages(id)) ?? ""; + const messageOutboxString: string = + getLocalStorageItemAndClear(storageKeys.outbox(id)) ?? ""; + const potentialMessages: any[] = parseArray(messagesString); + const potentialMessagesInOutbox: any[] = parseArray(messageOutboxString); + + const addMessages = ( + potentialMessages: any[], + status: ChatMessageStatus + ) => { + for (const potentialMessage of potentialMessages) { + const isV1ChatMessage: boolean = checkMatchesObjectStructure( + potentialMessage, + V1ChatMessageReference + ); + if (isV1ChatMessage == false) continue; + const v1ChatMessage: typeof V1ChatMessageReference = potentialMessage; + + const convertedChatMessage: ChatMessage = { + dataVersion: "v2", + + id: v4(), + + channel: v1ChatMessage.channel, + sender: v1ChatMessage.sender, + body: v1ChatMessage.body, + dateSent: v1ChatMessage.isoDate, + + status, + stringifiedFile: "", + }; + chatModel.addMessage(convertedChatMessage); + if (status == ChatMessageStatus.Outbox) { + this.connectionModel.sendMessageOrStore(convertedChatMessage); + } + } + }; + + addMessages(potentialMessages, ChatMessageStatus.Received); + addMessages(potentialMessagesInOutbox, ChatMessageStatus.Outbox); + + // objects + const objectsString: string = + getLocalStorageItemAndClear(storageKeys.objects(id)) ?? ""; + const objectOutboxString: string = + getLocalStorageItemAndClear(storageKeys.itemOutbox(id)) ?? ""; + const potentialObjects: any[] = parseArray(objectsString); + const potentialObjectsInOutbox: any[] = parseArray(objectOutboxString); + + const addObjects = (potentialObjects: any[]): void => { + const board: BoardInfoFileContent = + chatModel.fileModel.boardsAndTasksModel.createBoard( + translations.updater.migrated + ); + chatModel.fileModel.boardsAndTasksModel.updateBoard(board); + + for (const potentialObjectEntry of potentialObjects) { + const potentialObject = potentialObjectEntry[1]; + const isV1MessageObject: boolean = + checkIsV1MessageObject(potentialObject); + if (isV1MessageObject == false) continue; + + const objectId: string = potentialObject.id; + const objectName: string = potentialObject.title; + + const contentVersions: any[] = Object.values( + potentialObject.contentVersions + ); + for (const potentialVersion of contentVersions) { + const isV1MessageObjectContent: boolean = + checkIsV1MessageObjectContent(potentialVersion); + if (isV1MessageObjectContent == false) continue; + + const convertedTaskFileContent: TaskFileContent = { + dataVersion: "v2", + + fileId: objectId, + fileContentId: FileModel.generateFileContentId( + potentialVersion.isoDateVersionCreated + ), + creationDate: potentialVersion.isoDateVersionCreated, + type: "task", + + name: objectName ?? "", + boardId: board.fileId ?? "", + + description: potentialVersion.noteContent ?? "", + + category: potentialVersion.categoryName ?? "", + status: potentialVersion.status ?? "", + priority: potentialVersion.priority ?? "", + + date: potentialVersion.date ?? "", + time: potentialVersion.time ?? "", + }; + console.log(convertedTaskFileContent); + chatModel.fileModel.handleFileContent(convertedTaskFileContent); + } + } + }; + + addObjects([...potentialObjects, ...potentialObjectsInOutbox]); + }; + + // init + constructor( + public settingsModel: SettingsModel, + public connectionModel: ConnectionModel, + public chatListModel: ChatListModel + ) { + this.migrateSettings(); + this.migrateConnections(); + this.migrateChats(); + } +} + +const storageKeys = { + viewType(id: string): string { + return id + "view-type"; + }, + + hasUnread(id: string): string { + return id + "has-unread-messages"; + }, + + primaryChannel(id: string): string { + return id + "primary-channel"; + }, + + secondaryChannels(id: string): string { + return id + "secondary-channels"; + }, + + encyptionKey(id: string): string { + return id + "encryption-key"; + }, + + messages(id: string): string { + return id + "messages"; + }, + + objects(id: string): string { + return id + "items"; + }, + + outbox(id: string): string { + return id + "outbox"; + }, + + itemOutbox(id: string): string { + return id + "item-outbox"; + }, + + composingMessage(id: string): string { + return id + "composing-message"; + }, +}; + +const V1ChatMessageReference = { + channel: "", + sender: "", + body: "", + isoDate: "", +}; + +function checkIsV1MessageObject(object: any): boolean { + if (object.id == undefined) return false; + if (object.title == undefined) return false; + if (object.contentVersions == undefined) return false; + + return true; +} + +function checkIsV1MessageObjectContent(object: any): boolean { + if (object.id == undefined) return false; + if (object.isoDateVersionCreated == undefined) return false; + + return true; +} \ No newline at end of file diff --git a/src/View/translations.ts b/src/View/translations.ts index 23e0e815..4b0aaa43 100644 --- a/src/View/translations.ts +++ b/src/View/translations.ts @@ -1,4 +1,8 @@ const englishTranslations = { + updater: { + migrated: "Migrated", + }, + general: { deleteItemButtonAudioLabel: "delete item", searchButtonAudioLabel: "search", diff --git a/src/index.tsx b/src/index.tsx index 91c6b631..18b724e2 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -17,6 +17,7 @@ import SettingsViewModel from "./ViewModel/Global/settingsViewModel"; import { StorageModal } from "./View/Modals/storageModal"; import StorageModel from "./Model/Global/storageModel"; import StorageViewModel from "./ViewModel/Global/storageViewModel"; +import v1Upgrader from "./Upgrader/v1"; // models const storageModel = new StorageModel(); @@ -28,6 +29,9 @@ const chatListModel = new ChatListModel( connectionModel ); +// upgrade +new v1Upgrader(settingsModel, connectionModel, chatListModel); + // viewModels const coreVieWModel = new CoreViewModel();