implement auto reconnect

This commit is contained in:
marlon-erler
2026-04-01 08:29:39 +00:00
parent c52d7b7685
commit 9d400a9929
82 changed files with 8340 additions and 7641 deletions
+99 -93
View File
@@ -1,5 +1,8 @@
import ChatModel, { ChatMessage } from "./chatModel";
import StorageModel, { StorageModelSubPath, filePaths } from "../Global/storageModel";
import StorageModel, {
StorageModelSubPath,
filePaths,
} from "../Global/storageModel";
import ConnectionModel from "../Global/connectionModel";
import { Message } from "udn-frontend";
@@ -7,104 +10,107 @@ import SettingsModel from "../Global/settingsModel";
import { v4 } from "uuid";
export default class ChatListModel {
storageModel: StorageModel;
settingsModel: SettingsModel;
connectionModel: ConnectionModel;
storageModel: StorageModel;
settingsModel: SettingsModel;
connectionModel: ConnectionModel;
// data
chatModels = new Set<ChatModel>();
// data
chatModels = new Set<ChatModel>();
// handlers
messageHandler = (data: Message): void => {
const channel: string | undefined = data.messageChannel;
const body: string | undefined = data.messageBody;
if (channel == undefined) return;
if (body == undefined) return;
// handlers
messageHandler = (data: Message): void => {
const channel: string | undefined = data.messageChannel;
const body: string | undefined = data.messageBody;
if (channel == undefined) return;
if (body == undefined) return;
this.routeMessageToCorrectChatModel(channel, (chatModel: ChatModel) =>
chatModel.handleMessage(body)
);
};
this.routeMessageToCorrectChatModel(channel, (chatModel: ChatModel) =>
chatModel.handleMessage(body),
);
};
messageSentHandler = (chatMessage: ChatMessage): void => {
const channel: string = chatMessage.channel;
messageSentHandler = (chatMessage: ChatMessage): void => {
const channel: string = chatMessage.channel;
this.routeMessageToCorrectChatModel(channel, (chatModel: ChatModel) =>
chatModel.handleMessageSent(chatMessage)
);
};
this.routeMessageToCorrectChatModel(channel, (chatModel: ChatModel) =>
chatModel.handleMessageSent(chatMessage),
);
};
// methods
routeMessageToCorrectChatModel = (
channel: string,
fn: (chatModel: ChatModel) => void
): void => {
const allChannels = channel.split("/");
// methods
routeMessageToCorrectChatModel = (
channel: string,
fn: (chatModel: ChatModel) => void,
): void => {
const allChannels = channel.split("/");
for (const chatModel of this.chatModels) {
for (const channel of allChannels) {
if (channel != chatModel.info.primaryChannel) continue;
fn(chatModel);
break;
}
for (const chatModel of this.chatModels) {
for (const channel of allChannels) {
if (channel != chatModel.info.primaryChannel) continue;
fn(chatModel);
break;
}
}
};
// storage
addChatModel = (chatModel: ChatModel) => {
this.chatModels.add(chatModel);
};
createChat = (primaryChannel: string): ChatModel => {
const id: string = v4();
const chatModel = new ChatModel(
this.storageModel,
this.connectionModel,
this.settingsModel,
this,
id,
);
chatModel.setPrimaryChannel(primaryChannel);
this.addChatModel(chatModel);
return chatModel;
};
untrackChat = (chat: ChatModel): void => {
this.chatModels.delete(chat);
};
// load
loadChats = (): void => {
const chatDir = StorageModel.getPath(
StorageModelSubPath.Chat,
filePaths.chat.base,
);
const chatIds = this.storageModel.list(chatDir);
for (const chatId of chatIds) {
const chatModel = new ChatModel(
this.storageModel,
this.connectionModel,
this.settingsModel,
this,
chatId,
);
this.addChatModel(chatModel);
}
};
// init
constructor(
storageModel: StorageModel,
settingsModel: SettingsModel,
connectionModel: ConnectionModel,
) {
this.storageModel = storageModel;
this.settingsModel = settingsModel;
this.connectionModel = connectionModel;
this.loadChats();
connectionModel.messageHandlerManager.addHandler(this.messageHandler);
connectionModel.messageSentHandlerManager.addHandler(
this.messageSentHandler,
);
}
};
// storage
addChatModel = (chatModel: ChatModel) => {
this.chatModels.add(chatModel);
};
createChat = (primaryChannel: string): ChatModel => {
const id: string = v4();
const chatModel = new ChatModel(
this.storageModel,
this.connectionModel,
this.settingsModel,
this,
id
);
chatModel.setPrimaryChannel(primaryChannel);
this.addChatModel(chatModel);
return chatModel;
};
untrackChat = (chat: ChatModel): void => {
this.chatModels.delete(chat);
};
// load
loadChats = (): void => {
const chatDir = StorageModel.getPath(StorageModelSubPath.Chat, filePaths.chat.base);
const chatIds = this.storageModel.list(chatDir);
for (const chatId of chatIds) {
const chatModel = new ChatModel(
this.storageModel,
this.connectionModel,
this.settingsModel,
this,
chatId
);
this.addChatModel(chatModel);
}
};
// init
constructor(
storageModel: StorageModel,
settingsModel: SettingsModel,
connectionModel: ConnectionModel
) {
this.storageModel = storageModel;
this.settingsModel = settingsModel;
this.connectionModel = connectionModel;
this.loadChats();
connectionModel.messageHandlerManager.addHandler(this.messageHandler);
connectionModel.messageSentHandlerManager.addHandler(
this.messageSentHandler
);
}
}
+321 -308
View File
@@ -3,15 +3,15 @@
import { DATA_VERSION, ValidObject } from "../Utility/typeSafety";
import FileModel, { FileContent } from "../Files/fileModel";
import {
HandlerManager,
createTimestamp,
localeCompare,
parseValidObject,
stringify,
HandlerManager,
createTimestamp,
localeCompare,
parseValidObject,
stringify,
} from "../Utility/utility";
import StorageModel, {
StorageModelSubPath,
filePaths,
StorageModelSubPath,
filePaths,
} from "../Global/storageModel";
import { decryptString, encryptString } from "../Utility/crypto";
@@ -22,345 +22,358 @@ import SettingsModel from "../Global/settingsModel";
import { v4 } from "uuid";
export default class ChatModel {
connectionModel: ConnectionModel;
storageModel: StorageModel;
settingsModel: SettingsModel;
chatListModel: ChatListModel;
connectionModel: ConnectionModel;
storageModel: StorageModel;
settingsModel: SettingsModel;
chatListModel: ChatListModel;
fileModel: FileModel;
fileModel: FileModel;
// data
id: string;
info: ChatInfo;
color: Color;
// data
id: string;
info: ChatInfo;
color: Color;
chatMessageHandlerManager: HandlerManager<ChatMessage> = new HandlerManager();
chatMessageHandlerManager: HandlerManager<ChatMessage> =
new HandlerManager();
get secondaryChannels(): string[] {
return this.info.secondaryChannels.sort(localeCompare);
}
// paths
getBasePath = (): string[] => {
return StorageModel.getPath(
StorageModelSubPath.Chat,
filePaths.chat.chatBase(this.id)
);
};
getInfoPath = (): string[] => {
return StorageModel.getPath(
StorageModelSubPath.Chat,
filePaths.chat.info(this.id)
);
};
getColorPath = (): string[] => {
return StorageModel.getPath(
StorageModelSubPath.Chat,
filePaths.chat.color(this.id)
);
};
getMessageDirPath = (): string[] => {
return StorageModel.getPath(
StorageModelSubPath.Chat,
filePaths.chat.messages(this.id)
);
};
getMessagePath = (id: string): string[] => {
return [...this.getMessageDirPath(), id];
};
// handlers
handleMessage = (body: string): void => {
const chatMessage: ChatMessage | null = parseValidObject(
body,
ChatMessageReference
);
if (chatMessage == null) return;
chatMessage.status = ChatMessageStatus.Received;
this.addMessage(chatMessage);
this.info.hasUnreadMessages = true;
this.storeInfo();
};
handleMessageSent = (chatMessage: ChatMessage): void => {
chatMessage.status = ChatMessageStatus.Sent;
this.addMessage(chatMessage);
};
// settings
setPrimaryChannel = (primaryChannel: string): void => {
this.info.primaryChannel = primaryChannel;
this.storeInfo();
this.subscribe();
};
setSecondaryChannels = (secondaryChannels: string[]): void => {
this.info.secondaryChannels = secondaryChannels;
this.storeInfo();
};
setEncryptionKey = (key: string): void => {
this.info.encryptionKey = key;
this.storeInfo();
};
setColor = (color: Color): void => {
this.color = color;
this.storeColor();
};
// messaging
addMessage = async (chatMessage: ChatMessage): Promise<void> => {
await this.decryptMessage(chatMessage);
// message
if (chatMessage.body != "") {
const messagePath: string[] = this.getMessagePath(chatMessage.id);
this.storageModel.writeStringifiable(messagePath, chatMessage);
this.chatMessageHandlerManager.trigger(chatMessage);
get secondaryChannels(): string[] {
return this.info.secondaryChannels.sort(localeCompare);
}
// file
this.fileModel.handleStringifiedFileContent(chatMessage.stringifiedFile);
};
sendMessage = async (
body: string,
fileContent?: FileContent<string>
): Promise<boolean> => {
const senderName = this.settingsModel.username;
if (senderName == "") return false;
const allChannels = [this.info.primaryChannel];
for (const secondaryChannel of this.info.secondaryChannels) {
allChannels.push(secondaryChannel);
}
const combinedChannel: string = allChannels.join("/");
const chatMessage: ChatMessage = await ChatModel.createChatMessage(
combinedChannel,
senderName,
this.info.encryptionKey,
body,
fileContent
);
this.addMessage(chatMessage);
this.connectionModel.sendMessageOrStore(chatMessage);
return true;
};
decryptMessage = async (chatMessage: ChatMessage): Promise<void> => {
const decryptedBody: string = await decryptString(
chatMessage.body,
this.info.encryptionKey
);
const decryptedFile: string = await decryptString(
chatMessage.stringifiedFile ?? "",
this.info.encryptionKey
);
chatMessage.body = decryptedBody;
chatMessage.stringifiedFile = decryptedFile;
};
subscribe = (): void => {
this.connectionModel.addChannel(this.info.primaryChannel);
};
markRead = (): void => {
this.info.hasUnreadMessages = false;
this.storeInfo();
}
// storage
storeInfo = (): void => {
this.storageModel.writeStringifiable(this.getInfoPath(), this.info);
};
storeColor = (): void => {
this.storageModel.write(this.getColorPath(), this.color);
};
delete = () => {
// untrack
this.chatListModel.untrackChat(this);
// delete
const dirPath: string[] = this.getBasePath();
this.storageModel.removeRecursively(dirPath);
};
// load
loadInfo = (): void => {
const info: ChatInfo | null = this.storageModel.readStringifiable(
this.getInfoPath(),
ChatInfoReference
);
if (info != null) {
this.info = info;
} else {
this.info = ChatModel.generateChatInfo("0");
}
};
loadColor = (): void => {
const path: string[] = this.getColorPath();
const color: string | null = this.storageModel.read(path);
if (!color) {
this.color = Color.Standard;
} else {
this.color = color as any;
}
};
get messages(): ChatMessage[] {
const messageIds: string[] = this.storageModel.list(
this.getMessageDirPath()
);
if (!Array.isArray(messageIds)) return [];
const chatMessages: ChatMessage[] = [];
for (const messageId of messageIds) {
const messagePath: string[] = this.getMessagePath(messageId);
const chatMessage: ChatMessage | any =
this.storageModel.readStringifiable(messagePath, ChatMessageReference);
chatMessages.push(chatMessage);
}
const sorted = chatMessages.sort((a, b) =>
a.dateSent.localeCompare(b.dateSent)
);
return sorted;
}
// init
constructor(
storageModel: StorageModel,
connectionModel: ConnectionModel,
settingsModel: SettingsModel,
chatListModel: ChatListModel,
chatId: string
) {
this.id = chatId;
this.connectionModel = connectionModel;
this.settingsModel = settingsModel;
this.storageModel = storageModel;
this.chatListModel = chatListModel;
this.loadInfo();
this.loadColor();
this.subscribe();
this.fileModel = new FileModel(this.storageModel, this.settingsModel, this);
}
// utility
static generateChatInfo = (primaryChannel: string): ChatInfo => {
return {
dataVersion: DATA_VERSION,
primaryChannel,
secondaryChannels: [],
encryptionKey: "",
hasUnreadMessages: false,
// paths
getBasePath = (): string[] => {
return StorageModel.getPath(
StorageModelSubPath.Chat,
filePaths.chat.chatBase(this.id),
);
};
};
static createChatMessage = async (
channel: string,
sender: string,
encryptionKey: string,
body: string,
fileContent?: FileContent<string>
): Promise<ChatMessage> => {
const chatMessage: ChatMessage = {
dataVersion: DATA_VERSION,
id: v4(),
channel,
sender,
body,
dateSent: createTimestamp(),
status: ChatMessageStatus.Outbox,
stringifiedFile: "",
getInfoPath = (): string[] => {
return StorageModel.getPath(
StorageModelSubPath.Chat,
filePaths.chat.info(this.id),
);
};
if (fileContent != undefined) {
const stringifiedFile = stringify(fileContent);
chatMessage.stringifiedFile = stringifiedFile;
getColorPath = (): string[] => {
return StorageModel.getPath(
StorageModelSubPath.Chat,
filePaths.chat.color(this.id),
);
};
getMessageDirPath = (): string[] => {
return StorageModel.getPath(
StorageModelSubPath.Chat,
filePaths.chat.messages(this.id),
);
};
getMessagePath = (id: string): string[] => {
return [...this.getMessageDirPath(), id];
};
// handlers
handleMessage = (body: string): void => {
const chatMessage: ChatMessage | null = parseValidObject(
body,
ChatMessageReference,
);
if (chatMessage == null) return;
chatMessage.status = ChatMessageStatus.Received;
this.addMessage(chatMessage);
this.info.hasUnreadMessages = true;
this.storeInfo();
};
handleMessageSent = (chatMessage: ChatMessage): void => {
chatMessage.status = ChatMessageStatus.Sent;
this.addMessage(chatMessage);
};
// settings
setPrimaryChannel = (primaryChannel: string): void => {
this.info.primaryChannel = primaryChannel;
this.storeInfo();
this.subscribe();
};
setSecondaryChannels = (secondaryChannels: string[]): void => {
this.info.secondaryChannels = secondaryChannels;
this.storeInfo();
};
setEncryptionKey = (key: string): void => {
this.info.encryptionKey = key;
this.storeInfo();
};
setColor = (color: Color): void => {
this.color = color;
this.storeColor();
};
// messaging
addMessage = async (chatMessage: ChatMessage): Promise<void> => {
await this.decryptMessage(chatMessage);
// message
if (chatMessage.body != "") {
const messagePath: string[] = this.getMessagePath(chatMessage.id);
this.storageModel.writeStringifiable(messagePath, chatMessage);
this.chatMessageHandlerManager.trigger(chatMessage);
}
// file
this.fileModel.handleStringifiedFileContent(
chatMessage.stringifiedFile,
);
};
sendMessage = async (
body: string,
fileContent?: FileContent<string>,
): Promise<boolean> => {
const senderName = this.settingsModel.username;
if (senderName == "") return false;
const allChannels = [this.info.primaryChannel];
for (const secondaryChannel of this.info.secondaryChannels) {
allChannels.push(secondaryChannel);
}
const combinedChannel: string = allChannels.join("/");
const chatMessage: ChatMessage = await ChatModel.createChatMessage(
combinedChannel,
senderName,
this.info.encryptionKey,
body,
fileContent,
);
this.addMessage(chatMessage);
this.connectionModel.sendMessageOrStore(chatMessage);
return true;
};
decryptMessage = async (chatMessage: ChatMessage): Promise<void> => {
const decryptedBody: string = await decryptString(
chatMessage.body,
this.info.encryptionKey,
);
const decryptedFile: string = await decryptString(
chatMessage.stringifiedFile ?? "",
this.info.encryptionKey,
);
chatMessage.body = decryptedBody;
chatMessage.stringifiedFile = decryptedFile;
};
subscribe = (): void => {
this.connectionModel.addChannel(this.info.primaryChannel);
};
markRead = (): void => {
this.info.hasUnreadMessages = false;
this.storeInfo();
};
// storage
storeInfo = (): void => {
this.storageModel.writeStringifiable(this.getInfoPath(), this.info);
};
storeColor = (): void => {
this.storageModel.write(this.getColorPath(), this.color);
};
delete = () => {
// untrack
this.chatListModel.untrackChat(this);
// delete
const dirPath: string[] = this.getBasePath();
this.storageModel.removeRecursively(dirPath);
};
// load
loadInfo = (): void => {
const info: ChatInfo | null = this.storageModel.readStringifiable(
this.getInfoPath(),
ChatInfoReference,
);
if (info != null) {
this.info = info;
} else {
this.info = ChatModel.generateChatInfo("0");
}
};
loadColor = (): void => {
const path: string[] = this.getColorPath();
const color: string | null = this.storageModel.read(path);
if (!color) {
this.color = Color.Standard;
} else {
this.color = color as any;
}
};
get messages(): ChatMessage[] {
const messageIds: string[] = this.storageModel.list(
this.getMessageDirPath(),
);
if (!Array.isArray(messageIds)) return [];
const chatMessages: ChatMessage[] = [];
for (const messageId of messageIds) {
const messagePath: string[] = this.getMessagePath(messageId);
const chatMessage: ChatMessage | any =
this.storageModel.readStringifiable(
messagePath,
ChatMessageReference,
);
chatMessages.push(chatMessage);
}
const sorted = chatMessages.sort((a, b) =>
a.dateSent.localeCompare(b.dateSent),
);
return sorted;
}
if (encryptionKey != "") {
chatMessage.body = await encryptString(chatMessage.body, encryptionKey);
chatMessage.stringifiedFile = await encryptString(
chatMessage.stringifiedFile,
encryptionKey
);
// init
constructor(
storageModel: StorageModel,
connectionModel: ConnectionModel,
settingsModel: SettingsModel,
chatListModel: ChatListModel,
chatId: string,
) {
this.id = chatId;
this.connectionModel = connectionModel;
this.settingsModel = settingsModel;
this.storageModel = storageModel;
this.chatListModel = chatListModel;
this.loadInfo();
this.loadColor();
this.subscribe();
this.fileModel = new FileModel(
this.storageModel,
this.settingsModel,
this,
);
}
return chatMessage;
};
// utility
static generateChatInfo = (primaryChannel: string): ChatInfo => {
return {
dataVersion: DATA_VERSION,
primaryChannel,
secondaryChannels: [],
encryptionKey: "",
hasUnreadMessages: false,
};
};
static createChatMessage = async (
channel: string,
sender: string,
encryptionKey: string,
body: string,
fileContent?: FileContent<string>,
): Promise<ChatMessage> => {
const chatMessage: ChatMessage = {
dataVersion: DATA_VERSION,
id: v4(),
channel,
sender,
body,
dateSent: createTimestamp(),
status: ChatMessageStatus.Outbox,
stringifiedFile: "",
};
if (fileContent != undefined) {
const stringifiedFile = stringify(fileContent);
chatMessage.stringifiedFile = stringifiedFile;
}
if (encryptionKey != "") {
chatMessage.body = await encryptString(
chatMessage.body,
encryptionKey,
);
chatMessage.stringifiedFile = await encryptString(
chatMessage.stringifiedFile,
encryptionKey,
);
}
return chatMessage;
};
}
// types
export interface ChatInfo extends ValidObject {
primaryChannel: string;
secondaryChannels: string[];
encryptionKey: string;
primaryChannel: string;
secondaryChannels: string[];
encryptionKey: string;
hasUnreadMessages: boolean;
hasUnreadMessages: boolean;
}
export enum ChatMessageStatus {
Outbox = "outbox",
Sent = "sent",
Received = "received",
Other = "other",
Outbox = "outbox",
Sent = "sent",
Received = "received",
Other = "other",
}
export interface ChatMessage extends ValidObject {
id: string;
id: string;
channel: string;
sender: string;
body: string;
dateSent: string;
channel: string;
sender: string;
body: string;
dateSent: string;
status: ChatMessageStatus;
status: ChatMessageStatus;
stringifiedFile: string;
stringifiedFile: string;
}
// references
export const ChatInfoReference: ChatInfo = {
dataVersion: DATA_VERSION,
dataVersion: DATA_VERSION,
primaryChannel: "",
secondaryChannels: [""],
encryptionKey: "",
primaryChannel: "",
secondaryChannels: [""],
encryptionKey: "",
hasUnreadMessages: true,
hasUnreadMessages: true,
};
export const ChatMessageReference: ChatMessage = {
dataVersion: DATA_VERSION,
dataVersion: DATA_VERSION,
id: "",
id: "",
channel: "",
sender: "",
body: "",
dateSent: "",
channel: "",
sender: "",
body: "",
dateSent: "",
status: "" as ChatMessageStatus,
status: "" as ChatMessageStatus,
stringifiedFile: "",
stringifiedFile: "",
};
+278 -270
View File
@@ -1,8 +1,8 @@
// this file is responsible for managing boards within chats and tasks within boards.
import {
DATA_VERSION,
checkMatchesObjectStructure,
DATA_VERSION,
checkMatchesObjectStructure,
} from "../Utility/typeSafety";
import FileModel, { FileContent, FileModelSubPath } from "./fileModel";
@@ -15,316 +15,324 @@ import StorageModel from "../Global/storageModel";
import { v4 } from "uuid";
export default class BoardsAndTasksModel {
storageModel: StorageModel;
settingsModel: SettingsModel;
chatModel: ChatModel;
fileModel: FileModel;
storageModel: StorageModel;
settingsModel: SettingsModel;
chatModel: ChatModel;
fileModel: FileModel;
calendarModel: CalendarModel;
calendarModel: CalendarModel;
// data
boardHandlerManager: HandlerManager<BoardInfoFileContent> =
new HandlerManager();
taskHandlerManager: HandlerManager<TaskFileContent> = new HandlerManager();
// data
boardHandlerManager: HandlerManager<BoardInfoFileContent> =
new HandlerManager();
taskHandlerManager: HandlerManager<TaskFileContent> = new HandlerManager();
// paths
getBasePath = (): string[] => {
return this.fileModel.getModelContainerPath(FileModelSubPath.ModelTask);
};
// paths
getBasePath = (): string[] => {
return this.fileModel.getModelContainerPath(FileModelSubPath.ModelTask);
};
getViewPath = (): string[] => {
return [...this.getBasePath(), FileModelSubPath.ModelView];
};
getViewPath = (): string[] => {
return [...this.getBasePath(), FileModelSubPath.ModelView];
};
getBoardFilePath = (boardId: string): string[] => {
return [...this.fileModel.getFilePath(boardId)];
};
getBoardFilePath = (boardId: string): string[] => {
return [...this.fileModel.getFilePath(boardId)];
};
getTaskFilePath = (taskId: string): string[] => {
return [...this.fileModel.getFilePath(taskId)];
};
getTaskFilePath = (taskId: string): string[] => {
return [...this.fileModel.getFilePath(taskId)];
};
getBoardContainerPath = (): string[] => {
return [...this.getBasePath(), TaskModelSubPaths.Boards];
};
getBoardContainerPath = (): string[] => {
return [...this.getBasePath(), TaskModelSubPaths.Boards];
};
getBoardDirectoryPath = (boardId: string): string[] => {
return [...this.getBoardContainerPath(), boardId];
};
getBoardDirectoryPath = (boardId: string): string[] => {
return [...this.getBoardContainerPath(), boardId];
};
getTaskContainerPath = (boardId: string): string[] => {
return [
...this.getBoardDirectoryPath(boardId),
TaskModelSubPaths.BoardTasks,
];
};
getTaskContainerPath = (boardId: string): string[] => {
return [
...this.getBoardDirectoryPath(boardId),
TaskModelSubPaths.BoardTasks,
];
};
getTaskReferencePath = (boardId: string, fileId: string): string[] => {
return [...this.getTaskContainerPath(boardId), fileId];
};
getTaskReferencePath = (boardId: string, fileId: string): string[] => {
return [...this.getTaskContainerPath(boardId), fileId];
};
// handlers
handleFileContent = (fileContent: FileContent<string>): void => {
if (
checkMatchesObjectStructure(fileContent, BoardInfoFileContentReference) ==
true
// handlers
handleFileContent = (fileContent: FileContent<string>): void => {
if (
checkMatchesObjectStructure(
fileContent,
BoardInfoFileContentReference,
) == true
) {
this.handleBoard(fileContent as BoardInfoFileContent);
} else if (
checkMatchesObjectStructure(
fileContent,
TaskFileContentReference,
) == true
) {
this.handleTask(fileContent as TaskFileContent);
}
};
handleBoard = (boardInfoFileContent: BoardInfoFileContent) => {
this.updateBoard(boardInfoFileContent);
};
handleTask = (taskFileContent: TaskFileContent) => {
this.updateTask(taskFileContent);
};
// boards
createBoard = (name: string): BoardInfoFileContent => {
const boardInfoFileContent: BoardInfoFileContent =
BoardsAndTasksModel.createBoardInfoFileContent(
v4(),
name,
Color.Standard,
);
return boardInfoFileContent;
};
updateBoard = (boardInfoFileContent: BoardInfoFileContent): void => {
this.storeBoard(boardInfoFileContent);
this.boardHandlerManager.trigger(boardInfoFileContent);
};
updateBoardAndSend = (boardInfoFileContent: BoardInfoFileContent): void => {
this.updateBoard(boardInfoFileContent);
this.chatModel.sendMessage("", boardInfoFileContent);
};
storeBoard = (boardInfoFileContent: BoardInfoFileContent): void => {
// store info
this.fileModel.storeFileContent(boardInfoFileContent);
// add to list
const boardDirectoryPath: string[] = this.getBoardDirectoryPath(
boardInfoFileContent.fileId,
);
this.storageModel.write(boardDirectoryPath, "");
};
deleteBoard = (boardId: string): void => {
const boardFilePath: string[] = this.getBoardFilePath(boardId);
const boardDirectoryPath: string[] =
this.getBoardDirectoryPath(boardId);
this.storageModel.removeRecursively(boardFilePath);
this.storageModel.removeRecursively(boardDirectoryPath);
};
listBoardIds = (): string[] => {
const boardContainerPath: string[] = this.getBoardContainerPath();
const boardIds: string[] = this.storageModel.list(boardContainerPath);
return boardIds;
};
getBoardInfo = (fileId: string): BoardInfoFileContent | null => {
const boardInfoFileContentOrNull: BoardInfoFileContent | null =
this.fileModel.getLatestFileContent(
fileId,
BoardInfoFileContentReference,
);
return boardInfoFileContentOrNull;
};
getBoardName = (boardId: string): string => {
const boardInfo: BoardInfoFileContent | null =
this.getBoardInfo(boardId);
if (boardInfo == null) return "";
return boardInfo.name;
};
//tasks
createTask = (boardId: string): TaskFileContent => {
const taskFileContent: TaskFileContent =
BoardsAndTasksModel.createTaskFileContent(v4(), "", boardId);
return taskFileContent;
};
updateTask = (taskFileContent: TaskFileContent): void => {
this.storeTask(taskFileContent);
this.taskHandlerManager.trigger(taskFileContent);
};
updateTaskAndSend = (taskFileContent: TaskFileContent): void => {
this.updateTask(taskFileContent);
this.chatModel.sendMessage("", taskFileContent);
};
storeTask = (taskFileContent: TaskFileContent): void => {
// store info
this.fileModel.storeFileContent(taskFileContent);
// add to board
const taskReferencePath: string[] = this.getTaskReferencePath(
taskFileContent.boardId,
taskFileContent.fileId,
);
this.storageModel.write(taskReferencePath, "");
// add to calendar
this.calendarModel.storeTaskReference(taskFileContent);
};
listTaskIds = (boardId: string): string[] => {
const taskContainerPath: string[] = this.getTaskContainerPath(boardId);
const fileIds: string[] = this.storageModel.list(taskContainerPath);
return fileIds;
};
listTaskVersionIds = (taskId: string): string[] => {
const versionIds: string[] = this.fileModel.listFileContentIds(taskId);
return versionIds;
};
getLatestTaskFileContent = (taskId: string): TaskFileContent | null => {
const taskFileContentOrNull: TaskFileContent | null =
this.fileModel.getLatestFileContent(
taskId,
TaskFileContentReference,
);
return taskFileContentOrNull;
};
getSpecificTaskFileContent = (
taskId: string,
versionId: string,
): TaskFileContent | null => {
const taskFileContentOrNull: TaskFileContent | null =
this.fileModel.getFileContent(
taskId,
versionId,
TaskFileContentReference,
);
return taskFileContentOrNull;
};
deleteTask = (boardId: string, taskId: string): void => {
const taskFilePath: string[] = this.getTaskFilePath(taskId);
this.storageModel.removeRecursively(taskFilePath);
this.deleteTaskReference(boardId, taskId);
};
deleteTaskReference = (boardId: string, taskId: string): void => {
const taskReferencePath: string[] = this.getTaskReferencePath(
boardId,
taskId,
);
this.storageModel.removeRecursively(taskReferencePath);
};
// init
constructor(
storageModel: StorageModel,
settingsModel: SettingsModel,
chatModel: ChatModel,
fileModel: FileModel,
) {
this.handleBoard(fileContent as BoardInfoFileContent);
} else if (
checkMatchesObjectStructure(fileContent, TaskFileContentReference) == true
) {
this.handleTask(fileContent as TaskFileContent);
this.storageModel = storageModel;
this.settingsModel = settingsModel;
this.chatModel = chatModel;
this.fileModel = fileModel;
this.calendarModel = new CalendarModel(
this.storageModel,
this.settingsModel,
this.fileModel,
);
}
};
handleBoard = (boardInfoFileContent: BoardInfoFileContent) => {
this.updateBoard(boardInfoFileContent);
};
// utility
static createBoardInfoFileContent = (
fileId: string,
name: string,
color: Color,
): BoardInfoFileContent => {
const fileContent: FileContent<"board-info"> =
FileModel.createFileContent(fileId, "board-info");
return {
...fileContent,
handleTask = (taskFileContent: TaskFileContent) => {
this.updateTask(taskFileContent);
};
// boards
createBoard = (name: string): BoardInfoFileContent => {
const boardInfoFileContent: BoardInfoFileContent =
BoardsAndTasksModel.createBoardInfoFileContent(
v4(),
name,
Color.Standard
);
return boardInfoFileContent;
};
updateBoard = (boardInfoFileContent: BoardInfoFileContent): void => {
this.storeBoard(boardInfoFileContent);
this.boardHandlerManager.trigger(boardInfoFileContent);
};
updateBoardAndSend = (boardInfoFileContent: BoardInfoFileContent): void => {
this.updateBoard(boardInfoFileContent);
this.chatModel.sendMessage("", boardInfoFileContent);
};
storeBoard = (boardInfoFileContent: BoardInfoFileContent): void => {
// store info
this.fileModel.storeFileContent(boardInfoFileContent);
// add to list
const boardDirectoryPath: string[] = this.getBoardDirectoryPath(
boardInfoFileContent.fileId
);
this.storageModel.write(boardDirectoryPath, "");
};
deleteBoard = (boardId: string): void => {
const boardFilePath: string[] = this.getBoardFilePath(boardId);
const boardDirectoryPath: string[] = this.getBoardDirectoryPath(boardId);
this.storageModel.removeRecursively(boardFilePath);
this.storageModel.removeRecursively(boardDirectoryPath);
};
listBoardIds = (): string[] => {
const boardContainerPath: string[] = this.getBoardContainerPath();
const boardIds: string[] = this.storageModel.list(boardContainerPath);
return boardIds;
};
getBoardInfo = (fileId: string): BoardInfoFileContent | null => {
const boardInfoFileContentOrNull: BoardInfoFileContent | null =
this.fileModel.getLatestFileContent(
fileId,
BoardInfoFileContentReference
);
return boardInfoFileContentOrNull;
};
getBoardName = (boardId: string): string => {
const boardInfo: BoardInfoFileContent|null = this.getBoardInfo(boardId);
if (boardInfo == null) return "";
return boardInfo.name;
}
//tasks
createTask = (boardId: string): TaskFileContent => {
const taskFileContent: TaskFileContent =
BoardsAndTasksModel.createTaskFileContent(v4(), "", boardId);
return taskFileContent;
};
updateTask = (taskFileContent: TaskFileContent): void => {
this.storeTask(taskFileContent);
this.taskHandlerManager.trigger(taskFileContent);
};
updateTaskAndSend = (taskFileContent: TaskFileContent): void => {
this.updateTask(taskFileContent);
this.chatModel.sendMessage("", taskFileContent);
};
storeTask = (taskFileContent: TaskFileContent): void => {
// store info
this.fileModel.storeFileContent(taskFileContent);
// add to board
const taskReferencePath: string[] = this.getTaskReferencePath(
taskFileContent.boardId,
taskFileContent.fileId
);
this.storageModel.write(taskReferencePath, "");
// add to calendar
this.calendarModel.storeTaskReference(taskFileContent);
};
listTaskIds = (boardId: string): string[] => {
const taskContainerPath: string[] = this.getTaskContainerPath(boardId);
const fileIds: string[] = this.storageModel.list(taskContainerPath);
return fileIds;
};
listTaskVersionIds = (taskId: string): string[] => {
const versionIds: string[] = this.fileModel.listFileContentIds(taskId);
return versionIds;
};
getLatestTaskFileContent = (taskId: string): TaskFileContent | null => {
const taskFileContentOrNull: TaskFileContent | null =
this.fileModel.getLatestFileContent(taskId, TaskFileContentReference);
return taskFileContentOrNull;
};
getSpecificTaskFileContent = (
taskId: string,
versionId: string
): TaskFileContent | null => {
const taskFileContentOrNull: TaskFileContent | null =
this.fileModel.getFileContent(
taskId,
versionId,
TaskFileContentReference
);
return taskFileContentOrNull;
};
deleteTask = (boardId: string, taskId: string): void => {
const taskFilePath: string[] = this.getTaskFilePath(taskId);
this.storageModel.removeRecursively(taskFilePath);
this.deleteTaskReference(boardId, taskId);
};
deleteTaskReference = (boardId: string, taskId: string): void => {
const taskReferencePath: string[] = this.getTaskReferencePath(
boardId,
taskId
);
this.storageModel.removeRecursively(taskReferencePath);
};
// init
constructor(
storageModel: StorageModel,
settingsModel: SettingsModel,
chatModel: ChatModel,
fileModel: FileModel
) {
this.storageModel = storageModel;
this.settingsModel = settingsModel;
this.chatModel = chatModel;
this.fileModel = fileModel;
this.calendarModel = new CalendarModel(
this.storageModel,
this.settingsModel,
this.fileModel
);
}
// utility
static createBoardInfoFileContent = (
fileId: string,
name: string,
color: Color
): BoardInfoFileContent => {
const fileContent: FileContent<"board-info"> = FileModel.createFileContent(
fileId,
"board-info"
);
return {
...fileContent,
name,
color,
name,
color,
};
};
};
static createTaskFileContent = (
fileId: string,
name: string,
boardId: string
): TaskFileContent => {
const fileContent: FileContent<"task"> = FileModel.createFileContent(
fileId,
"task"
);
return {
...fileContent,
static createTaskFileContent = (
fileId: string,
name: string,
boardId: string,
): TaskFileContent => {
const fileContent: FileContent<"task"> = FileModel.createFileContent(
fileId,
"task",
);
return {
...fileContent,
name,
boardId,
name,
boardId,
};
};
};
}
export enum TaskModelSubPaths {
Boards = "boards",
BoardTasks = "tasks",
Boards = "boards",
BoardTasks = "tasks",
}
// types
export interface BoardInfoFileContent extends FileContent<"board-info"> {
name: string;
color: Color;
name: string;
color: Color;
}
export interface TaskFileContent extends FileContent<"task"> {
name: string;
boardId: string;
name: string;
boardId: string;
description?: string;
description?: string;
category?: string;
status?: string;
priority?: string;
category?: string;
status?: string;
priority?: string;
date?: string;
time?: string;
date?: string;
time?: string;
}
// reference
export const BoardInfoFileContentReference: BoardInfoFileContent = {
dataVersion: DATA_VERSION,
dataVersion: DATA_VERSION,
fileId: "string",
fileContentId: "",
creationDate: "",
fileId: "string",
fileContentId: "",
creationDate: "",
type: "board-info",
type: "board-info",
name: "",
color: "" as Color,
name: "",
color: "" as Color,
};
export const TaskFileContentReference: TaskFileContent = {
dataVersion: DATA_VERSION,
dataVersion: DATA_VERSION,
fileId: "string",
fileContentId: "",
creationDate: "",
type: "task",
fileId: "string",
fileContentId: "",
creationDate: "",
type: "task",
name: "",
boardId: "",
name: "",
boardId: "",
};
+138 -136
View File
@@ -7,158 +7,160 @@ import StorageModel from "../Global/storageModel";
import { TaskFileContent } from "./boardsAndTasksModel";
export default class CalendarModel {
storageModel: StorageModel;
fileModel: FileModel;
settingsModel: SettingsModel;
storageModel: StorageModel;
fileModel: FileModel;
settingsModel: SettingsModel;
// paths
getBasePath = (): string[] => {
return this.fileModel.getModelContainerPath(FileModelSubPath.ModelCalendar);
};
getViewPath = (): string[] => {
return [...this.getBasePath(), FileModelSubPath.ModelView];
};
getMonthContainerPath = (): string[] => {
return [...this.getBasePath(), CalendarModelSubPaths.Months];
};
getMonthPath = (monthString: string): string[] => {
return [...this.getMonthContainerPath(), monthString];
};
// main
storeTaskReference = (taskFileContent: TaskFileContent): void => {
if (taskFileContent.date == undefined) return;
const monthString: string = CalendarModel.isoToMonthString(
taskFileContent.date
);
const monthPath: string[] = this.getMonthPath(monthString);
const referencePath: string[] = [...monthPath, taskFileContent.fileId];
this.storageModel.write(referencePath, "");
};
deleteTaskReference = (monthString: string, taskId: string): void => {
const monthPath: string[] = this.getMonthPath(monthString);
const referencePath: string[] = [...monthPath, taskId];
this.storageModel.write(referencePath, "");
};
listTaskIds = (monthString: string): string[] => {
const monthPath: string[] = this.getMonthPath(monthString);
return this.storageModel.list(monthPath);
};
generateMonthGrid = <T>(
year: number,
month: number,
defaultValueCreator: () => T
): MonthGrid<T> => {
// get today
const date: Date = new Date();
const isCurrentMonth: boolean =
year == date.getFullYear() && month == date.getMonth() + 1;
// get data for offset
date.setDate(1);
date.setMonth(month - 1);
date.setFullYear(year);
const firstWeekdayOfMonth = date.getDay();
const firstDayOfWeekSetting: number = parseInt(
this.settingsModel.firstDayOfWeek
);
const offset: number =
firstWeekdayOfMonth < firstDayOfWeekSetting
? 7 - firstDayOfWeekSetting
: firstWeekdayOfMonth - firstDayOfWeekSetting;
// get total day count
date.setMonth(month);
date.setDate(-1);
const daysInMonth: number = date.getDate() + 1;
const grid: MonthGrid<T> = {
offset,
firstDayOfWeek: parseInt(this.settingsModel.firstDayOfWeek),
isCurrentMonth,
year,
month,
days: {},
// paths
getBasePath = (): string[] => {
return this.fileModel.getModelContainerPath(
FileModelSubPath.ModelCalendar,
);
};
for (let i = 0; i < daysInMonth; i++) {
const paddedDate: string = CalendarModel.padDateOrMonth(
(i + 1).toString()
);
grid.days[paddedDate] = defaultValueCreator();
getViewPath = (): string[] => {
return [...this.getBasePath(), FileModelSubPath.ModelView];
};
getMonthContainerPath = (): string[] => {
return [...this.getBasePath(), CalendarModelSubPaths.Months];
};
getMonthPath = (monthString: string): string[] => {
return [...this.getMonthContainerPath(), monthString];
};
// main
storeTaskReference = (taskFileContent: TaskFileContent): void => {
if (taskFileContent.date == undefined) return;
const monthString: string = CalendarModel.isoToMonthString(
taskFileContent.date,
);
const monthPath: string[] = this.getMonthPath(monthString);
const referencePath: string[] = [...monthPath, taskFileContent.fileId];
this.storageModel.write(referencePath, "");
};
deleteTaskReference = (monthString: string, taskId: string): void => {
const monthPath: string[] = this.getMonthPath(monthString);
const referencePath: string[] = [...monthPath, taskId];
this.storageModel.write(referencePath, "");
};
listTaskIds = (monthString: string): string[] => {
const monthPath: string[] = this.getMonthPath(monthString);
return this.storageModel.list(monthPath);
};
generateMonthGrid = <T>(
year: number,
month: number,
defaultValueCreator: () => T,
): MonthGrid<T> => {
// get today
const date: Date = new Date();
const isCurrentMonth: boolean =
year == date.getFullYear() && month == date.getMonth() + 1;
// get data for offset
date.setDate(1);
date.setMonth(month - 1);
date.setFullYear(year);
const firstWeekdayOfMonth = date.getDay();
const firstDayOfWeekSetting: number = parseInt(
this.settingsModel.firstDayOfWeek,
);
const offset: number =
firstWeekdayOfMonth < firstDayOfWeekSetting
? 7 - firstDayOfWeekSetting
: firstWeekdayOfMonth - firstDayOfWeekSetting;
// get total day count
date.setMonth(month);
date.setDate(-1);
const daysInMonth: number = date.getDate() + 1;
const grid: MonthGrid<T> = {
offset,
firstDayOfWeek: parseInt(this.settingsModel.firstDayOfWeek),
isCurrentMonth,
year,
month,
days: {},
};
for (let i = 0; i < daysInMonth; i++) {
const paddedDate: string = CalendarModel.padDateOrMonth(
(i + 1).toString(),
);
grid.days[paddedDate] = defaultValueCreator();
}
return grid;
};
// init
constructor(
storageModel: StorageModel,
settingsModel: SettingsModel,
fileModel: FileModel,
) {
this.storageModel = storageModel;
this.settingsModel = settingsModel;
this.fileModel = fileModel;
}
return grid;
};
// utility
static isoToMonthString = (dateISOString: string): string => {
const [year, month, _] = dateISOString.split("-");
return CalendarModel.getMonthString(year, month);
};
// init
constructor(
storageModel: StorageModel,
settingsModel: SettingsModel,
fileModel: FileModel
) {
this.storageModel = storageModel;
this.settingsModel = settingsModel;
this.fileModel = fileModel;
}
static isoToDateString = (dateISOString: string): string => {
const [year, month, date, _] = dateISOString.split("-");
const paddedDate = CalendarModel.padDateOrMonth(date ?? "");
return paddedDate;
};
// utility
static isoToMonthString = (dateISOString: string): string => {
const [year, month, _] = dateISOString.split("-");
return CalendarModel.getMonthString(year, month);
};
static getMonthString = (year: string = "", month: string = ""): string => {
const paddedYear: string = year.padStart(4, "0");
const paddedMonth: string = CalendarModel.padDateOrMonth(month);
return `${paddedYear}-${paddedMonth}`;
};
static isoToDateString = (dateISOString: string): string => {
const [year, month, date, _] = dateISOString.split("-");
const paddedDate = CalendarModel.padDateOrMonth(date ?? "");
return paddedDate;
};
static getISODateString = (
year: string,
month: string,
date: string,
): string => {
const monthString: string = CalendarModel.getMonthString(year, month);
const paddedDate: string = CalendarModel.padDateOrMonth(date);
return `${monthString}-${paddedDate}`;
};
static getMonthString = (year: string = "", month: string = ""): string => {
const paddedYear: string = year.padStart(4, "0");
const paddedMonth: string = CalendarModel.padDateOrMonth(month);
return `${paddedYear}-${paddedMonth}`;
};
static getISODateString = (
year: string,
month: string,
date: string
): string => {
const monthString: string = CalendarModel.getMonthString(year, month);
const paddedDate: string = CalendarModel.padDateOrMonth(date);
return `${monthString}-${paddedDate}`;
};
static padDateOrMonth = (input: string): string => {
return input.padStart(2, "0");
};
static padDateOrMonth = (input: string): string => {
return input.padStart(2, "0");
};
}
export enum CalendarModelSubPaths {
Months = "months",
Months = "months",
}
// types
export interface MonthGrid<T> {
offset: number;
firstDayOfWeek: number;
isCurrentMonth: boolean;
offset: number;
firstDayOfWeek: number;
isCurrentMonth: boolean;
year: number;
month: number;
year: number;
month: number;
days: { [date: string]: T };
days: { [date: string]: T };
}
+178 -175
View File
@@ -2,14 +2,14 @@
import { DATA_VERSION, ValidObject } from "../Utility/typeSafety";
import StorageModel, {
StorageModelSubPath,
filePaths,
StorageModelSubPath,
filePaths,
} from "../Global/storageModel";
import {
StringEntryObject,
createTimestamp,
parseValidObject,
stringify,
StringEntryObject,
createTimestamp,
parseValidObject,
stringify,
} from "../Utility/utility";
import BoardsAndTasksModel from "./boardsAndTasksModel";
@@ -18,191 +18,194 @@ import SettingsModel from "../Global/settingsModel";
import { v4 } from "uuid";
export default class FileModel {
storageModel: StorageModel;
settingsModel: SettingsModel;
storageModel: StorageModel;
settingsModel: SettingsModel;
chatModel: ChatModel;
boardsAndTasksModel: BoardsAndTasksModel;
chatModel: ChatModel;
boardsAndTasksModel: BoardsAndTasksModel;
// paths
getBasePath = (): string[] => {
return StorageModel.getPath(
StorageModelSubPath.Chat,
filePaths.chat.files(this.chatModel.id)
);
};
getFileContainerPath = (): string[] => {
return [...this.getBasePath(), FileModelSubPath.Data];
};
getModelContainerPath = (modelName: FileModelSubPath): string[] => {
return [...this.getBasePath(), FileModelSubPath.Model, modelName];
};
getFilePath = (fileId: string): string[] => {
return [...this.getFileContainerPath(), fileId];
};
getFileContentPath = (fileId: string, fileContentId: string): string[] => {
const filePath: string[] = this.getFilePath(fileId);
return [...filePath, fileContentId];
};
// handlers
handleStringifiedFileContent = (stringifiedFileContent: string): void => {
const fileContent: FileContent<string> | null = parseValidObject(
stringifiedFileContent,
FileContentReference
);
if (fileContent == null) return;
this.handleFileContent(fileContent);
};
handleFileContent = (fileContent: FileContent<string>): void => {
const didStore: boolean = this.storeFileContent(fileContent);
if (didStore == false) return;
this.boardsAndTasksModel.handleFileContent(fileContent);
};
// methods
addFileContentAndSend = (fileContent: FileContent<string>): void => {
this.handleFileContent(fileContent);
this.chatModel.sendMessage("", fileContent);
};
// storage
storeFileContent = (fileContent: FileContent<string>): boolean => {
const fileContentPath: string[] = this.getFileContentPath(
fileContent.fileId,
fileContent.fileContentId
);
// check if fileContent already exists
const existingFileContent: string | null =
this.storageModel.read(fileContentPath);
if (existingFileContent != null) return false;
const stringifiedContent: string = stringify(fileContent);
this.storageModel.write(fileContentPath, stringifiedContent);
return true;
};
listFileIds = (): string[] => {
return this.storageModel.list(this.getBasePath());
};
listFileContentIds = (fileId: string): string[] => {
const filePath: string[] = this.getFilePath(fileId);
return this.storageModel.list(filePath);
};
selectLatestFileContentId = (
fileContentIds: string[]
): string | undefined => {
return fileContentIds[fileContentIds.length - 1];
};
getFileContent = <T extends FileContent<string>>(
fileId: string,
fileContentName: string,
reference: T
): T | null => {
const filePath: string[] = this.getFileContentPath(fileId, fileContentName);
const fileContentOrNull: T | null = this.storageModel.readStringifiable(
filePath,
reference
);
return fileContentOrNull;
};
getLatestFileContent = <T extends FileContent<string>>(
fileId: string,
reference: T
): T | null => {
const fileContentsIds: string[] = this.listFileContentIds(fileId);
const latestFileContentId: string | undefined =
this.selectLatestFileContentId(fileContentsIds);
if (latestFileContentId == undefined) return null;
const fileContent: T | null = this.getFileContent(
fileId,
latestFileContentId,
reference
);
return fileContent;
};
// init
constructor(
storageModel: StorageModel,
settingsModel: SettingsModel,
chatModel: ChatModel
) {
this.chatModel = chatModel;
this.settingsModel = settingsModel;
this.storageModel = storageModel;
this.boardsAndTasksModel = new BoardsAndTasksModel(
this.storageModel,
this.settingsModel,
chatModel,
this
);
}
// utility
static generateFileContentId = (creationDate: string): string => {
return creationDate + v4();
};
static createFileContent = <T extends string>(
fileId: string,
type: T
): FileContent<T> => {
const creationDate: string = createTimestamp();
const fileContentId: string = FileModel.generateFileContentId(creationDate);
return {
dataVersion: DATA_VERSION,
fileId,
fileContentId,
creationDate,
type,
// paths
getBasePath = (): string[] => {
return StorageModel.getPath(
StorageModelSubPath.Chat,
filePaths.chat.files(this.chatModel.id),
);
};
getFileContainerPath = (): string[] => {
return [...this.getBasePath(), FileModelSubPath.Data];
};
getModelContainerPath = (modelName: FileModelSubPath): string[] => {
return [...this.getBasePath(), FileModelSubPath.Model, modelName];
};
getFilePath = (fileId: string): string[] => {
return [...this.getFileContainerPath(), fileId];
};
getFileContentPath = (fileId: string, fileContentId: string): string[] => {
const filePath: string[] = this.getFilePath(fileId);
return [...filePath, fileContentId];
};
// handlers
handleStringifiedFileContent = (stringifiedFileContent: string): void => {
const fileContent: FileContent<string> | null = parseValidObject(
stringifiedFileContent,
FileContentReference,
);
if (fileContent == null) return;
this.handleFileContent(fileContent);
};
handleFileContent = (fileContent: FileContent<string>): void => {
const didStore: boolean = this.storeFileContent(fileContent);
if (didStore == false) return;
this.boardsAndTasksModel.handleFileContent(fileContent);
};
// methods
addFileContentAndSend = (fileContent: FileContent<string>): void => {
this.handleFileContent(fileContent);
this.chatModel.sendMessage("", fileContent);
};
// storage
storeFileContent = (fileContent: FileContent<string>): boolean => {
const fileContentPath: string[] = this.getFileContentPath(
fileContent.fileId,
fileContent.fileContentId,
);
// check if fileContent already exists
const existingFileContent: string | null =
this.storageModel.read(fileContentPath);
if (existingFileContent != null) return false;
const stringifiedContent: string = stringify(fileContent);
this.storageModel.write(fileContentPath, stringifiedContent);
return true;
};
listFileIds = (): string[] => {
return this.storageModel.list(this.getBasePath());
};
listFileContentIds = (fileId: string): string[] => {
const filePath: string[] = this.getFilePath(fileId);
return this.storageModel.list(filePath);
};
selectLatestFileContentId = (
fileContentIds: string[],
): string | undefined => {
return fileContentIds[fileContentIds.length - 1];
};
getFileContent = <T extends FileContent<string>>(
fileId: string,
fileContentName: string,
reference: T,
): T | null => {
const filePath: string[] = this.getFileContentPath(
fileId,
fileContentName,
);
const fileContentOrNull: T | null = this.storageModel.readStringifiable(
filePath,
reference,
);
return fileContentOrNull;
};
getLatestFileContent = <T extends FileContent<string>>(
fileId: string,
reference: T,
): T | null => {
const fileContentsIds: string[] = this.listFileContentIds(fileId);
const latestFileContentId: string | undefined =
this.selectLatestFileContentId(fileContentsIds);
if (latestFileContentId == undefined) return null;
const fileContent: T | null = this.getFileContent(
fileId,
latestFileContentId,
reference,
);
return fileContent;
};
// init
constructor(
storageModel: StorageModel,
settingsModel: SettingsModel,
chatModel: ChatModel,
) {
this.chatModel = chatModel;
this.settingsModel = settingsModel;
this.storageModel = storageModel;
this.boardsAndTasksModel = new BoardsAndTasksModel(
this.storageModel,
this.settingsModel,
chatModel,
this,
);
}
// utility
static generateFileContentId = (creationDate: string): string => {
return creationDate + v4();
};
static createFileContent = <T extends string>(
fileId: string,
type: T,
): FileContent<T> => {
const creationDate: string = createTimestamp();
const fileContentId: string =
FileModel.generateFileContentId(creationDate);
return {
dataVersion: DATA_VERSION,
fileId,
fileContentId,
creationDate,
type,
};
};
};
}
// paths
export enum FileModelSubPath {
Data = "data",
Model = "model",
ModelView = "view",
ModelTask = "tasks",
ModelCalendar = "calendar",
Data = "data",
Model = "model",
ModelView = "view",
ModelTask = "tasks",
ModelCalendar = "calendar",
}
// types
export interface FileContent<T extends string>
extends ValidObject,
StringEntryObject {
fileId: string;
fileContentId: string;
creationDate: string;
extends ValidObject, StringEntryObject {
fileId: string;
fileContentId: string;
creationDate: string;
type: T;
type: T;
}
// references
export const FileContentReference: FileContent<string> = {
dataVersion: DATA_VERSION,
dataVersion: DATA_VERSION,
fileId: "",
fileContentId: "",
creationDate: "",
fileId: "",
fileContentId: "",
creationDate: "",
type: "",
type: "",
};
+257 -234
View File
@@ -6,255 +6,278 @@ import StorageModel, { StorageModelSubPath, filePaths } from "./storageModel";
import UDNFrontend, { Message } from "udn-frontend";
export default class ConnectionModel {
udn: UDNFrontend;
storageModel: StorageModel;
udn: UDNFrontend;
storageModel: StorageModel;
reconnectInterval: number | undefined = undefined;
shouldAttemptReconnect: boolean = false;
// data
get isConnected(): boolean {
return this.udn.ws != undefined && this.udn.ws.readyState == 1;
}
get address(): string | undefined {
return this.udn.ws?.url;
}
connectionChangeHandlerManager: HandlerManager<void> = new HandlerManager();
messageHandlerManager: HandlerManager<Message> = new HandlerManager();
messageSentHandlerManager: HandlerManager<ChatMessage> = new HandlerManager();
channelsToSubscribe: Set<string> = new Set();
// handlers
handleMessage = (data: Message): void => {
this.messageHandlerManager.trigger(data);
};
handleConnectionChange = (): void => {
console.log("connection status:", this.isConnected, this.address);
this.connectionChangeHandlerManager.trigger();
if (this.isConnected == false) return;
if (this.address == undefined) return;
this.storeAddress(this.address);
this.sendSubscriptionRequest();
this.sendMessagesInOutbox();
};
// connection
connect = (address: string): void => {
this.udn.connect(address);
};
disconnect = (): void => {
this.udn.disconnect();
// do not reconnect
const reconnectAddressPath: string[] = StorageModel.getPath(
StorageModelSubPath.ConnectionModel,
filePaths.connectionModel.reconnectAddress
);
this.storageModel.remove(reconnectAddressPath);
};
// mailbox
getMailboxPath = (address: string): string[] => {
const mailboxDirPath = StorageModel.getPath(
StorageModelSubPath.ConnectionModel,
filePaths.connectionModel.mailboxes
);
const mailboxFilePath = [...mailboxDirPath, address];
return mailboxFilePath;
};
requestNewMailbox = (): void => {
console.log("requesting new mailbox");
this.udn.requestMailbox();
};
connectMailbox = (): void => {
if (this.address == undefined) return;
const mailboxId = this.storageModel.read(this.getMailboxPath(this.address));
console.log("connecting mailbox", mailboxId);
if (mailboxId == null) return this.requestNewMailbox();
this.udn.connectMailbox(mailboxId);
};
storeMailbox = (mailboxId: string): void => {
if (this.address == undefined) return;
this.storageModel.write(this.getMailboxPath(this.address), mailboxId);
};
// subscription
addChannel = (channel: string): void => {
this.channelsToSubscribe.add(channel);
this.sendSubscriptionRequest();
};
sendSubscriptionRequest = (): void => {
if (this.isConnected == false) return;
for (const channel of this.channelsToSubscribe) {
this.udn.subscribe(channel);
// data
get isConnected(): boolean {
return this.udn.ws != undefined && this.udn.ws.readyState == 1;
}
// update mailbox
this.connectMailbox();
};
// outbox
getOutboxPath = (): string[] => {
return StorageModel.getPath(
StorageModelSubPath.ConnectionModel,
filePaths.connectionModel.outbox
);
};
get address(): string | undefined {
return this.udn.ws?.url;
}
getOutboxMessags = (): ChatMessage[] => {
const outboxPath: string[] = this.getOutboxPath();
const messageIds: string[] = this.storageModel.list(outboxPath);
connectionChangeHandlerManager: HandlerManager<void> = new HandlerManager();
messageHandlerManager: HandlerManager<Message> = new HandlerManager();
messageSentHandlerManager: HandlerManager<ChatMessage> =
new HandlerManager();
let chatMessages: ChatMessage[] = [];
for (const messageId of messageIds) {
const chatMessage: ChatMessage | null =
this.storageModel.readStringifiable(
[...outboxPath, messageId],
ChatMessageReference
channelsToSubscribe: Set<string> = new Set();
// handlers
handleMessage = (data: Message): void => {
this.messageHandlerManager.trigger(data);
};
handleConnectionChange = (): void => {
console.log("connection status:", this.isConnected, this.address);
this.connectionChangeHandlerManager.trigger();
if (this.isConnected == false) {
if (this.shouldAttemptReconnect == false) return;
this.reconnectInterval = setInterval(() => {
if (this.isConnected == true) {
clearInterval(this.reconnectInterval);
return;
}
this.reconnect();
}, 5000);
return;
}
if (this.address == undefined) return;
this.storeAddress(this.address);
this.sendSubscriptionRequest();
this.sendMessagesInOutbox();
this.shouldAttemptReconnect = true;
};
// connection
connect = (address: string): void => {
this.udn.connect(address);
};
disconnect = (): void => {
this.udn.disconnect();
// do not reconnect
const reconnectAddressPath: string[] = StorageModel.getPath(
StorageModelSubPath.ConnectionModel,
filePaths.connectionModel.reconnectAddress,
);
this.storageModel.remove(reconnectAddressPath);
this.shouldAttemptReconnect = false;
};
if (chatMessage == null) continue;
chatMessages.push(chatMessage);
reconnect = (): void => {
const reconnectAddressPath: string[] = this.getReconnectAddressPath();
const reconnectAddress: string | null =
this.storageModel.read(reconnectAddressPath);
if (reconnectAddress == null) return;
console.log("reconnecting...");
this.connect(reconnectAddress);
};
// mailbox
getMailboxPath = (address: string): string[] => {
const mailboxDirPath = StorageModel.getPath(
StorageModelSubPath.ConnectionModel,
filePaths.connectionModel.mailboxes,
);
const mailboxFilePath = [...mailboxDirPath, address];
return mailboxFilePath;
};
requestNewMailbox = (): void => {
console.log("requesting new mailbox");
this.udn.requestMailbox();
};
connectMailbox = (): void => {
if (this.address == undefined) return;
const mailboxId = this.storageModel.read(
this.getMailboxPath(this.address),
);
console.log("connecting mailbox", mailboxId);
if (mailboxId == null) return this.requestNewMailbox();
this.udn.connectMailbox(mailboxId);
};
storeMailbox = (mailboxId: string): void => {
if (this.address == undefined) return;
this.storageModel.write(this.getMailboxPath(this.address), mailboxId);
};
// subscription
addChannel = (channel: string): void => {
this.channelsToSubscribe.add(channel);
this.sendSubscriptionRequest();
};
sendSubscriptionRequest = (): void => {
if (this.isConnected == false) return;
for (const channel of this.channelsToSubscribe) {
this.udn.subscribe(channel);
}
// update mailbox
this.connectMailbox();
};
// outbox
getOutboxPath = (): string[] => {
return StorageModel.getPath(
StorageModelSubPath.ConnectionModel,
filePaths.connectionModel.outbox,
);
};
getOutboxMessags = (): ChatMessage[] => {
const outboxPath: string[] = this.getOutboxPath();
const messageIds: string[] = this.storageModel.list(outboxPath);
let chatMessages: ChatMessage[] = [];
for (const messageId of messageIds) {
const chatMessage: ChatMessage | null =
this.storageModel.readStringifiable(
[...outboxPath, messageId],
ChatMessageReference,
);
if (chatMessage == null) continue;
chatMessages.push(chatMessage);
}
return chatMessages;
};
addToOutbox = (chatMessage: ChatMessage): void => {
const messagePath: string[] = [...this.getOutboxPath(), chatMessage.id];
this.storageModel.writeStringifiable(messagePath, chatMessage);
};
removeFromOutbox = (chatMessage: ChatMessage): void => {
const messagePath: string[] = [...this.getOutboxPath(), chatMessage.id];
this.storageModel.remove(messagePath);
};
sendMessagesInOutbox = (): void => {
const messages: ChatMessage[] = this.getOutboxMessags();
for (const message of messages) {
const isSent: boolean = this.tryToSendMessage(message);
if (isSent == false) return;
this.removeFromOutbox(message);
}
};
// messaging
sendMessageOrStore = (chatMessage: ChatMessage): void => {
const isSent = this.tryToSendMessage(chatMessage);
if (isSent == true) return;
this.addToOutbox(chatMessage);
};
tryToSendMessage = (chatMessage: ChatMessage): boolean => {
const stringifiedBody: string = stringify(chatMessage);
const isSent: boolean = this.sendPlainMessage(
chatMessage.channel,
stringifiedBody,
);
if (isSent) this.messageSentHandlerManager.trigger(chatMessage);
return isSent;
};
sendPlainMessage = (channel: string, body: string): boolean => {
return this.udn.sendMessage(channel, body);
};
// storage
getPreviousAddressPath = (): string[] => {
return StorageModel.getPath(
StorageModelSubPath.ConnectionModel,
filePaths.connectionModel.previousAddresses,
);
};
getAddressPath = (address: string): string[] => {
const dirPath = this.getPreviousAddressPath();
return [...dirPath, address];
};
getReconnectAddressPath = (): string[] => {
return StorageModel.getPath(
StorageModelSubPath.ConnectionModel,
filePaths.connectionModel.reconnectAddress,
);
};
storeAddress = (address: string): void => {
// history
const addressPath = this.getAddressPath(address);
this.storageModel.write(addressPath, "");
// reconnect
const reconnectAddressPath: string[] = this.getReconnectAddressPath();
this.storageModel.write(reconnectAddressPath, address);
};
removeAddress = (address: string): void => {
const addressPath = this.getAddressPath(address);
this.storageModel.remove(addressPath);
};
get addresses(): string[] {
const dirPath = this.getPreviousAddressPath();
return this.storageModel.list(dirPath);
}
return chatMessages;
};
// init
constructor(storageModel: StorageModel) {
// create frontend
this.udn = new UDNFrontend();
this.storageModel = storageModel;
addToOutbox = (chatMessage: ChatMessage): void => {
const messagePath: string[] = [...this.getOutboxPath(), chatMessage.id];
this.storageModel.writeStringifiable(messagePath, chatMessage);
};
// setup handlers
this.udn.onmessage = (data: Message) => {
this.handleMessage(data);
};
this.udn.onconnect = () => {
this.handleConnectionChange();
};
this.udn.ondisconnect = () => {
this.handleConnectionChange();
};
removeFromOutbox = (chatMessage: ChatMessage): void => {
const messagePath: string[] = [...this.getOutboxPath(), chatMessage.id];
this.storageModel.remove(messagePath);
};
this.udn.onmailboxcreate = (mailboxId: string) => {
console.log("created mailbox", mailboxId);
this.storeMailbox(mailboxId);
this.connectMailbox();
};
this.udn.onmailboxdelete = (mailboxId: string) => {
console.log(`mailbox ${mailboxId} deleted`);
this.requestNewMailbox();
};
this.udn.onmailboxconnect = (mailboxId: string) => {
console.log(`using mailbox ${mailboxId}`);
};
sendMessagesInOutbox = (): void => {
const messages: ChatMessage[] = this.getOutboxMessags();
for (const message of messages) {
const isSent: boolean = this.tryToSendMessage(message);
if (isSent == false) return;
this.removeFromOutbox(message);
this.reconnect();
}
};
// messaging
sendMessageOrStore = (chatMessage: ChatMessage): void => {
const isSent = this.tryToSendMessage(chatMessage);
if (isSent == true) return;
this.addToOutbox(chatMessage);
};
tryToSendMessage = (chatMessage: ChatMessage): boolean => {
const stringifiedBody: string = stringify(chatMessage);
const isSent: boolean = this.sendPlainMessage(
chatMessage.channel,
stringifiedBody
);
if (isSent) this.messageSentHandlerManager.trigger(chatMessage);
return isSent;
};
sendPlainMessage = (channel: string, body: string): boolean => {
return this.udn.sendMessage(channel, body);
};
// storage
getPreviousAddressPath = (): string[] => {
return StorageModel.getPath(
StorageModelSubPath.ConnectionModel,
filePaths.connectionModel.previousAddresses
);
};
getAddressPath = (address: string): string[] => {
const dirPath = this.getPreviousAddressPath();
return [...dirPath, address];
};
getReconnectAddressPath = (): string[] => {
return StorageModel.getPath(
StorageModelSubPath.ConnectionModel,
filePaths.connectionModel.reconnectAddress
);
};
storeAddress = (address: string): void => {
// history
const addressPath = this.getAddressPath(address);
this.storageModel.write(addressPath, "");
// reconnect
const reconnectAddressPath: string[] = this.getReconnectAddressPath();
this.storageModel.write(reconnectAddressPath, address);
};
removeAddress = (address: string): void => {
const addressPath = this.getAddressPath(address);
this.storageModel.remove(addressPath);
};
get addresses(): string[] {
const dirPath = this.getPreviousAddressPath();
return this.storageModel.list(dirPath);
}
// init
constructor(storageModel: StorageModel) {
// create frontend
this.udn = new UDNFrontend();
this.storageModel = storageModel;
// setup handlers
this.udn.onmessage = (data: Message) => {
this.handleMessage(data);
};
this.udn.onconnect = () => {
this.handleConnectionChange();
};
this.udn.ondisconnect = () => {
this.handleConnectionChange();
};
this.udn.onmailboxcreate = (mailboxId: string) => {
console.log("created mailbox", mailboxId);
this.storeMailbox(mailboxId);
this.connectMailbox();
};
this.udn.onmailboxdelete = (mailboxId: string) => {
console.log(`mailbox ${mailboxId} deleted`);
this.requestNewMailbox();
};
this.udn.onmailboxconnect = (mailboxId: string) => {
console.log(`using mailbox ${mailboxId}`);
};
// reconnect
const reconnectAddressPath: string[] = this.getReconnectAddressPath();
const reconnectAddress: string | null =
storageModel.read(reconnectAddressPath);
if (reconnectAddress != null) {
this.connect(reconnectAddress);
}
}
}
// paths
export enum settingsModelStorageFiles {
username = "user-name",
firstDayOfWeek = "first-day-of-week",
username = "user-name",
firstDayOfWeek = "first-day-of-week",
}
+97 -95
View File
@@ -1,10 +1,10 @@
// this file is responsible for sending and handling files.
import {
HandlerManager,
generateRandomToken,
parse,
stringify,
HandlerManager,
generateRandomToken,
parse,
stringify,
} from "../Utility/utility";
import { decryptString, encryptString } from "../Utility/crypto";
@@ -14,118 +14,120 @@ import StorageModel from "./storageModel";
import { checkMatchesObjectStructure } from "../Utility/typeSafety";
export default class FileTransferModel {
storageModel: StorageModel;
connectionModel: ConnectionModel;
storageModel: StorageModel;
connectionModel: ConnectionModel;
// data
transferData: TransferData | undefined;
fileHandlerManager: HandlerManager<string> = new HandlerManager();
// data
transferData: TransferData | undefined;
fileHandlerManager: HandlerManager<string> = new HandlerManager();
// general
generateTransferData = (): TransferData => {
const transferData: TransferData = {
channel: generateRandomToken(2).substring(0, 4),
key: generateRandomToken(3).substring(0, 6),
// general
generateTransferData = (): TransferData => {
const transferData: TransferData = {
channel: generateRandomToken(2).substring(0, 4),
key: generateRandomToken(3).substring(0, 6),
};
this.transferData = transferData;
return transferData;
};
this.transferData = transferData;
return transferData;
};
prepareToReceive = (transferData: TransferData) => {
this.connectionModel.addChannel(transferData.channel);
this.transferData = transferData;
};
prepareToReceive = (transferData: TransferData) => {
this.connectionModel.addChannel(transferData.channel);
this.transferData = transferData;
};
// handlers
handleMessage = (data: Message): void => {
if (this.transferData == undefined) return;
if (data.messageBody == undefined) return;
if (data.messageChannel != this.transferData.channel) return;
// handlers
handleMessage = (data: Message): void => {
if (this.transferData == undefined) return;
if (data.messageBody == undefined) return;
if (data.messageChannel != this.transferData.channel) return;
this.handleFile(data.messageBody);
};
this.handleFile(data.messageBody);
};
handleFile = async (encryptedFileData: string): Promise<void> => {
if (this.transferData == undefined) return;
handleFile = async (encryptedFileData: string): Promise<void> => {
if (this.transferData == undefined) return;
const decrypted: string = await decryptString(
encryptedFileData,
this.transferData.key
);
console.log(decrypted);
const parsed: any = parse(decrypted);
const isFileData: boolean = checkMatchesObjectStructure(
parsed,
FileDataReference
);
if (isFileData == false) return;
const fileData: FileData = parsed;
this.storageModel.write(fileData.path, fileData.body);
const pathString: string = StorageModel.pathComponentsToString(
...fileData.path
);
this.fileHandlerManager.trigger(pathString);
};
// sending
sendFiles = (
directoryPaths: IteratorObject<string[]>,
callback: (path: string) => void
): void => {
for (const directoryPath of directoryPaths) {
this.storageModel.recurse(directoryPath, (filePath: string[]) => {
this.sendFile(filePath);
const pathString: string = StorageModel.pathComponentsToString(
...filePath
const decrypted: string = await decryptString(
encryptedFileData,
this.transferData.key,
);
callback(pathString);
});
}
};
console.log(decrypted);
sendFile = async (filePath: string[]): Promise<void> => {
if (this.transferData == undefined) return;
const fileContent: string | null = this.storageModel.read(filePath);
if (fileContent == null) return;
const parsed: any = parse(decrypted);
const isFileData: boolean = checkMatchesObjectStructure(
parsed,
FileDataReference,
);
if (isFileData == false) return;
const fileData: FileData = {
path: filePath,
body: fileContent,
const fileData: FileData = parsed;
this.storageModel.write(fileData.path, fileData.body);
const pathString: string = StorageModel.pathComponentsToString(
...fileData.path,
);
this.fileHandlerManager.trigger(pathString);
};
const stringifiedFileData: string = stringify(fileData);
const encryptedFileData: string = await encryptString(
stringifiedFileData,
this.transferData.key
);
this.connectionModel.sendPlainMessage(
this.transferData.channel,
encryptedFileData
);
};
// init
constructor(storageModel: StorageModel, connectionModel: ConnectionModel) {
this.storageModel = storageModel;
this.connectionModel = connectionModel;
// sending
sendFiles = (
directoryPaths: IteratorObject<string[]>,
callback: (path: string) => void,
): void => {
for (const directoryPath of directoryPaths) {
this.storageModel.recurse(directoryPath, (filePath: string[]) => {
this.sendFile(filePath);
const pathString: string = StorageModel.pathComponentsToString(
...filePath,
);
callback(pathString);
});
}
};
this.connectionModel.messageHandlerManager.addHandler(this.handleMessage);
}
sendFile = async (filePath: string[]): Promise<void> => {
if (this.transferData == undefined) return;
const fileContent: string | null = this.storageModel.read(filePath);
if (fileContent == null) return;
const fileData: FileData = {
path: filePath,
body: fileContent,
};
const stringifiedFileData: string = stringify(fileData);
const encryptedFileData: string = await encryptString(
stringifiedFileData,
this.transferData.key,
);
this.connectionModel.sendPlainMessage(
this.transferData.channel,
encryptedFileData,
);
};
// init
constructor(storageModel: StorageModel, connectionModel: ConnectionModel) {
this.storageModel = storageModel;
this.connectionModel = connectionModel;
this.connectionModel.messageHandlerManager.addHandler(
this.handleMessage,
);
}
}
export interface TransferData {
channel: string;
key: string;
channel: string;
key: string;
}
export interface FileData {
path: string[];
body: string;
path: string[];
body: string;
}
export const FileDataReference: FileData = {
path: [""],
body: "",
path: [""],
body: "",
};
+44 -44
View File
@@ -3,55 +3,55 @@
import StorageModel, { StorageModelSubPath, filePaths } from "./storageModel";
export default class SettingsModel {
storageModel: StorageModel;
storageModel: StorageModel;
// data
username: string;
firstDayOfWeek: string;
// data
username: string;
firstDayOfWeek: string;
// storage
setName(newValue: string): void {
this.username = newValue;
const path = StorageModel.getPath(
StorageModelSubPath.SettingsModel,
filePaths.settingsModel.username
);
this.storageModel.write(path, newValue);
}
// storage
setName(newValue: string): void {
this.username = newValue;
const path = StorageModel.getPath(
StorageModelSubPath.SettingsModel,
filePaths.settingsModel.username,
);
this.storageModel.write(path, newValue);
}
setFirstDayOfWeek(newValue: string): void {
this.firstDayOfWeek = newValue;
const path = StorageModel.getPath(
StorageModelSubPath.SettingsModel,
filePaths.settingsModel.firstDayOfWeek
);
this.storageModel.write(path, newValue);
}
setFirstDayOfWeek(newValue: string): void {
this.firstDayOfWeek = newValue;
const path = StorageModel.getPath(
StorageModelSubPath.SettingsModel,
filePaths.settingsModel.firstDayOfWeek,
);
this.storageModel.write(path, newValue);
}
// load
loadUsernam(): void {
const path = StorageModel.getPath(
StorageModelSubPath.SettingsModel,
filePaths.settingsModel.username
);
const content = this.storageModel.read(path);
this.username = content ?? "";
}
// load
loadUsernam(): void {
const path = StorageModel.getPath(
StorageModelSubPath.SettingsModel,
filePaths.settingsModel.username,
);
const content = this.storageModel.read(path);
this.username = content ?? "";
}
loadFirstDayofWeek(): void {
const path = StorageModel.getPath(
StorageModelSubPath.SettingsModel,
filePaths.settingsModel.firstDayOfWeek
);
const content = this.storageModel.read(path);
this.firstDayOfWeek = content ?? "0";
}
loadFirstDayofWeek(): void {
const path = StorageModel.getPath(
StorageModelSubPath.SettingsModel,
filePaths.settingsModel.firstDayOfWeek,
);
const content = this.storageModel.read(path);
this.firstDayOfWeek = content ?? "0";
}
// init
constructor(storageModel: StorageModel) {
this.storageModel = storageModel;
// init
constructor(storageModel: StorageModel) {
this.storageModel = storageModel;
this.loadUsernam();
this.loadFirstDayofWeek();
}
this.loadUsernam();
this.loadFirstDayofWeek();
}
}
+229 -219
View File
@@ -6,207 +6,217 @@ import { localeCompare, parseValidObject, stringify } from "../Utility/utility";
export const PATH_COMPONENT_SEPARATOR = "\\";
export default class StorageModel {
storageEntryTree: StorageEntry = {};
storageEntryTree: StorageEntry = {};
// read
read = (pathComponents: string[]): string | null => {
const pathString: string = StorageModel.pathComponentsToString(
...pathComponents
);
return localStorage.getItem(pathString);
};
// read
read = (pathComponents: string[]): string | null => {
const pathString: string = StorageModel.pathComponentsToString(
...pathComponents,
);
return localStorage.getItem(pathString);
};
list = (pathComponents: string[]): string[] => {
let currentParent: StorageEntry = this.storageEntryTree;
for (const component of pathComponents) {
const nextParent: StorageEntry | undefined = currentParent[component];
if (nextParent == undefined) return [];
currentParent = nextParent;
list = (pathComponents: string[]): string[] => {
let currentParent: StorageEntry = this.storageEntryTree;
for (const component of pathComponents) {
const nextParent: StorageEntry | undefined =
currentParent[component];
if (nextParent == undefined) return [];
currentParent = nextParent;
}
return [...Object.keys(currentParent).sort(localeCompare)];
};
// write
write = (pathComponents: string[], value: string): void => {
const pathString: string = StorageModel.pathComponentsToString(
...pathComponents,
);
localStorage.setItem(pathString, value);
this.updateTree(...pathComponents);
};
remove = (
pathComponents: string[],
shouldInitialize: boolean = true,
): void => {
const pathString: string = StorageModel.pathComponentsToString(
...pathComponents,
);
localStorage.removeItem(pathString);
if (shouldInitialize == true) {
this.initializeTree();
}
};
rename = (
sourcePathComponents: string[],
destinationPathComponents: string[],
shouldInitialize: boolean = true,
): boolean => {
const content: string | null = this.read(sourcePathComponents);
if (content == null) return false;
this.write(destinationPathComponents, content);
this.remove(sourcePathComponents);
if (shouldInitialize == true) {
this.initializeTree();
}
return true;
};
// recursion
recurse = (rootDirectory: string[], fn: (path: string[]) => void): void => {
loop_over_files: for (const key of Object.keys(localStorage)) {
// get path of current entity
const pathComponentsOfCurrentEntity: string[] =
StorageModel.stringToPathComponents(key);
// exit if entity does not match
loop_over_path_components: for (
let i = 0;
i < rootDirectory.length;
i++
) {
if (!pathComponentsOfCurrentEntity[i]) continue loop_over_files;
if (pathComponentsOfCurrentEntity[i] != rootDirectory[i])
continue loop_over_files;
}
// execute
fn(pathComponentsOfCurrentEntity);
}
this.initializeTree();
};
removeRecursively = (pathComponents: string[]): void => {
this.recurse(pathComponents, (path: string[]) =>
this.remove(path, false),
);
this.initializeTree();
};
renameRecursively = (
sourcePathComponents: string[],
destinationPathComponents: string[],
): void => {
this.recurse(sourcePathComponents, (path: string[]) => {
// assemble destinationPath
const relativePathOfCurrentEntity = path.slice(
sourcePathComponents.length,
);
const destinationPathComponentsOfCurrentEntity = [
...destinationPathComponents,
...relativePathOfCurrentEntity,
];
// rename
this.rename(path, destinationPathComponentsOfCurrentEntity, false);
});
this.initializeTree();
};
// stringifiable
writeStringifiable = (
pathComponents: string[],
value: ValidObject,
): void => {
const valueString: string = stringify(value);
this.write(pathComponents, valueString);
};
readStringifiable = <T extends ValidObject>(
pathComponents: string[],
reference: T,
): T | null => {
const valueString: string | null = this.read(pathComponents);
if (!valueString) return null;
const object: any | null = parseValidObject(valueString, reference);
if (object == null) return null;
return object;
};
// cleaning
removeJunk = (): void => {
this.recurse([], (path: string[]) => {
if (path[0] == DATA_VERSION) return;
this.remove(path);
});
};
// tree
initializeTree = (): void => {
console.log("initializing tree");
this.storageEntryTree = {};
for (const key of Object.keys(localStorage)) {
const components: string[] =
StorageModel.stringToPathComponents(key);
this.updateTree(...components);
}
};
updateTree = (...pathComponents: string[]): void => {
let currentParent: StorageEntry = this.storageEntryTree;
for (const pathPart of pathComponents) {
if (!currentParent[pathPart]) {
currentParent[pathPart] = {};
}
currentParent = currentParent[pathPart];
}
};
printTree = (): string => {
return stringify(this.storageEntryTree);
};
// init
constructor() {
this.initializeTree();
}
return [...Object.keys(currentParent).sort(localeCompare)];
};
// utility
static getFileName = (pathComponents: string[]): string => {
return pathComponents[pathComponents.length - 1] || "\\";
};
// write
write = (pathComponents: string[], value: string): void => {
const pathString: string = StorageModel.pathComponentsToString(
...pathComponents
);
localStorage.setItem(pathString, value);
this.updateTree(...pathComponents);
};
static getFileNameFromString = (pathString: string): string => {
const pathComponents: string[] =
this.stringToPathComponents(pathString);
return pathComponents[pathComponents.length - 1] || "\\";
};
remove = (
pathComponents: string[],
shouldInitialize: boolean = true
): void => {
const pathString: string = StorageModel.pathComponentsToString(
...pathComponents
);
localStorage.removeItem(pathString);
static pathComponentsToString = (...pathComponents: string[]): string => {
return pathComponents
.filter((x) => x != "")
.join(PATH_COMPONENT_SEPARATOR);
};
if (shouldInitialize == true) {
this.initializeTree();
static stringToPathComponents = (string: string): string[] => {
return string.split(PATH_COMPONENT_SEPARATOR).filter((x) => x != "");
};
static join = (...items: string[]): string => {
let allComponents: string[] = [];
for (const item of items) {
const parts = this.stringToPathComponents(item);
allComponents.push(...parts);
}
return StorageModel.pathComponentsToString(...allComponents);
};
static getPath(
locationName: StorageModelSubPath,
filePath: string[],
): string[] {
return [DATA_VERSION, locationName, ...filePath];
}
};
rename = (
sourcePathComponents: string[],
destinationPathComponents: string[],
shouldInitialize: boolean = true
): boolean => {
const content: string | null = this.read(sourcePathComponents);
if (content == null) return false;
this.write(destinationPathComponents, content);
this.remove(sourcePathComponents);
if (shouldInitialize == true) {
this.initializeTree();
}
return true;
};
// recursion
recurse = (rootDirectory: string[], fn: (path: string[]) => void): void => {
loop_over_files: for (const key of Object.keys(localStorage)) {
// get path of current entity
const pathComponentsOfCurrentEntity: string[] =
StorageModel.stringToPathComponents(key);
// exit if entity does not match
loop_over_path_components: for (
let i = 0;
i < rootDirectory.length;
i++
) {
if (!pathComponentsOfCurrentEntity[i]) continue loop_over_files;
if (pathComponentsOfCurrentEntity[i] != rootDirectory[i])
continue loop_over_files;
}
// execute
fn(pathComponentsOfCurrentEntity);
}
this.initializeTree();
};
removeRecursively = (pathComponents: string[]): void => {
this.recurse(pathComponents, (path: string[]) => this.remove(path, false));
this.initializeTree();
};
renameRecursively = (
sourcePathComponents: string[],
destinationPathComponents: string[]
): void => {
this.recurse(sourcePathComponents, (path: string[]) => {
// assemble destinationPath
const relativePathOfCurrentEntity = path.slice(
sourcePathComponents.length
);
const destinationPathComponentsOfCurrentEntity = [
...destinationPathComponents,
...relativePathOfCurrentEntity,
];
// rename
this.rename(path, destinationPathComponentsOfCurrentEntity, false);
});
this.initializeTree();
};
// stringifiable
writeStringifiable = (pathComponents: string[], value: ValidObject): void => {
const valueString: string = stringify(value);
this.write(pathComponents, valueString);
};
readStringifiable = <T extends ValidObject>(
pathComponents: string[],
reference: T
): T | null => {
const valueString: string | null = this.read(pathComponents);
if (!valueString) return null;
const object: any | null = parseValidObject(valueString, reference);
if (object == null) return null;
return object;
};
// cleaning
removeJunk = (): void => {
this.recurse([], (path: string[]) => {
if (path[0] == DATA_VERSION) return;
this.remove(path);
})
}
// tree
initializeTree = (): void => {
console.log("initializing tree");
this.storageEntryTree = {};
for (const key of Object.keys(localStorage)) {
const components: string[] = StorageModel.stringToPathComponents(key);
this.updateTree(...components);
}
};
updateTree = (...pathComponents: string[]): void => {
let currentParent: StorageEntry = this.storageEntryTree;
for (const pathPart of pathComponents) {
if (!currentParent[pathPart]) {
currentParent[pathPart] = {};
}
currentParent = currentParent[pathPart];
}
};
printTree = (): string => {
return stringify(this.storageEntryTree);
};
// init
constructor() {
this.initializeTree();
}
// utility
static getFileName = (pathComponents: string[]): string => {
return pathComponents[pathComponents.length - 1] || "\\";
};
static getFileNameFromString = (pathString: string): string => {
const pathComponents: string[] = this.stringToPathComponents(pathString);
return pathComponents[pathComponents.length - 1] || "\\";
};
static pathComponentsToString = (...pathComponents: string[]): string => {
return pathComponents.filter((x) => x != "").join(PATH_COMPONENT_SEPARATOR);
};
static stringToPathComponents = (string: string): string[] => {
return string.split(PATH_COMPONENT_SEPARATOR).filter((x) => x != "");
};
static join = (...items: string[]): string => {
let allComponents: string[] = [];
for (const item of items) {
const parts = this.stringToPathComponents(item);
allComponents.push(...parts);
}
return StorageModel.pathComponentsToString(...allComponents);
};
static getPath(
locationName: StorageModelSubPath,
filePath: string[]
): string[] {
return [DATA_VERSION, locationName, ...filePath];
}
}
// types
@@ -214,37 +224,37 @@ export type StorageEntry = { [key: string]: StorageEntry };
// locations
export enum StorageModelSubPath {
Chat = "chat",
Chat = "chat",
ConnectionModel = "connection",
SettingsModel = "settings",
ConnectionModel = "connection",
SettingsModel = "settings",
}
export const filePaths = {
connectionModel: {
socketAddress: ["socket-address"],
reconnectAddress: ["reconnect-address"],
outbox: ["outbox"],
mailboxes: ["mailboxes"],
connectionModel: {
socketAddress: ["socket-address"],
reconnectAddress: ["reconnect-address"],
outbox: ["outbox"],
mailboxes: ["mailboxes"],
previousAddresses: ["previous-addresses"],
},
previousAddresses: ["previous-addresses"],
},
chat: {
base: [],
chatBase: (id: string) => [id],
info: (id: string) => [...filePaths.chat.chatBase(id), "info"],
color: (id: string) => [...filePaths.chat.chatBase(id), "color"],
messages: (id: string) => [...filePaths.chat.chatBase(id), "messages"],
lastUsedPage: (id: string) => [
...filePaths.chat.chatBase(id),
"last-used-page",
],
files: (id: string) => [...filePaths.chat.chatBase(id), "files"],
},
chat: {
base: [],
chatBase: (id: string) => [id],
info: (id: string) => [...filePaths.chat.chatBase(id), "info"],
color: (id: string) => [...filePaths.chat.chatBase(id), "color"],
messages: (id: string) => [...filePaths.chat.chatBase(id), "messages"],
lastUsedPage: (id: string) => [
...filePaths.chat.chatBase(id),
"last-used-page",
],
files: (id: string) => [...filePaths.chat.chatBase(id), "files"],
},
settingsModel: {
username: ["user-name"],
firstDayOfWeek: ["first-day-of-week"],
},
settingsModel: {
username: ["user-name"],
firstDayOfWeek: ["first-day-of-week"],
},
};
+60 -60
View File
@@ -3,113 +3,113 @@ const ENCRYPTION_ALG = "AES-GCM";
// minimal
export async function encryptString(
plaintext: string,
passphrase: string
plaintext: string,
passphrase: string,
): Promise<string> {
if (!window.crypto.subtle) return plaintext;
if (!window.crypto.subtle) return plaintext;
const iv = generateIV();
const key = await importKey(passphrase, "encrypt");
const encryptedArray = await encrypt(iv, key, plaintext);
const iv = generateIV();
const key = await importKey(passphrase, "encrypt");
const encryptedArray = await encrypt(iv, key, plaintext);
const encryptionData = {
iv: uInt8ToArray(iv),
encryptedArray: uInt8ToArray(encryptedArray),
};
return btoa(JSON.stringify(encryptionData));
const encryptionData = {
iv: uInt8ToArray(iv),
encryptedArray: uInt8ToArray(encryptedArray),
};
return btoa(JSON.stringify(encryptionData));
}
export async function decryptString(
cyphertext: string,
passphrase: string
cyphertext: string,
passphrase: string,
): Promise<string> {
try {
const encrypionData = JSON.parse(atob(cyphertext));
const iv = arrayToUint8(encrypionData.iv);
const encryptedArray = arrayToUint8(encrypionData.encryptedArray);
try {
const encrypionData = JSON.parse(atob(cyphertext));
const iv = arrayToUint8(encrypionData.iv);
const encryptedArray = arrayToUint8(encrypionData.encryptedArray);
const key = await importKey(passphrase, "decrypt");
return await decrypt(iv, key, encryptedArray);
} catch {
return cyphertext;
}
const key = await importKey(passphrase, "decrypt");
return await decrypt(iv, key, encryptedArray);
} catch {
return cyphertext;
}
}
// process
export function encode(string: string): Uint8Array {
return new TextEncoder().encode(string);
return new TextEncoder().encode(string);
}
export function decode(array: Uint8Array): string {
return new TextDecoder("utf-8").decode(array);
return new TextDecoder("utf-8").decode(array);
}
export async function encrypt(
iv: Uint8Array,
key: CryptoKey,
message: string
iv: Uint8Array,
key: CryptoKey,
message: string,
): Promise<Uint8Array> {
const arrayBuffer = await window.crypto.subtle.encrypt(
{ name: ENCRYPTION_ALG, iv },
key,
encode(message)
);
return new Uint8Array(arrayBuffer);
const arrayBuffer = await window.crypto.subtle.encrypt(
{ name: ENCRYPTION_ALG, iv },
key,
encode(message),
);
return new Uint8Array(arrayBuffer);
}
export async function decrypt(
iv: Uint8Array,
key: CryptoKey,
cyphertext: Uint8Array
iv: Uint8Array,
key: CryptoKey,
cyphertext: Uint8Array,
): Promise<string> {
const arrayBuffer = await crypto.subtle.decrypt(
{ name: ENCRYPTION_ALG, iv },
key,
cyphertext
);
return arrayBufferToString(arrayBuffer);
const arrayBuffer = await crypto.subtle.decrypt(
{ name: ENCRYPTION_ALG, iv },
key,
cyphertext,
);
return arrayBufferToString(arrayBuffer);
}
export async function hash(encoded: Uint8Array): Promise<ArrayBuffer> {
return await crypto.subtle.digest("SHA-256", encoded);
return await crypto.subtle.digest("SHA-256", encoded);
}
// iv
export function generateIV(): Uint8Array {
return crypto.getRandomValues(new Uint8Array(IV_SIZE));
return crypto.getRandomValues(new Uint8Array(IV_SIZE));
}
export function splitMergedString(mergedString: string): [Uint8Array, string] {
const iv = mergedString.slice(0, IV_SIZE);
const encryptedString = mergedString.slice(IV_SIZE);
const iv = mergedString.slice(0, IV_SIZE);
const encryptedString = mergedString.slice(IV_SIZE);
return [JSON.parse(iv), encryptedString];
return [JSON.parse(iv), encryptedString];
}
// key
export async function importKey(
passphrase: string,
purpose: "encrypt" | "decrypt"
passphrase: string,
purpose: "encrypt" | "decrypt",
): Promise<CryptoKey> {
return await crypto.subtle.importKey(
"raw",
await hash(encode(passphrase)),
{ name: ENCRYPTION_ALG },
false,
[purpose]
);
return await crypto.subtle.importKey(
"raw",
await hash(encode(passphrase)),
{ name: ENCRYPTION_ALG },
false,
[purpose],
);
}
// conversion
export function arrayBufferToString(arrayBuffer: ArrayBuffer): string {
const uInt8Array = new Uint8Array(arrayBuffer);
return decode(uInt8Array);
const uInt8Array = new Uint8Array(arrayBuffer);
return decode(uInt8Array);
}
export function uInt8ToArray(uInt8Array: Uint8Array): number[] {
return Array.from(uInt8Array);
return Array.from(uInt8Array);
}
export function arrayToUint8(array: number[]): Uint8Array {
return new Uint8Array(array);
return new Uint8Array(array);
}
+35 -35
View File
@@ -1,48 +1,48 @@
export const DATA_VERSION = "v2";
export function checkIsValidObject(object: any): boolean {
return object.dataVersion == DATA_VERSION;
return object.dataVersion == DATA_VERSION;
}
export function checkMatchesObjectStructure(
objectToCheck: any,
reference: any
objectToCheck: any,
reference: any,
): boolean {
if (typeof reference != "object") {
return typeof objectToCheck == typeof reference;
}
for (const key of Object.keys(reference)) {
const requiredType = typeof reference[key];
const actualType = typeof objectToCheck[key];
if (requiredType != actualType) return false;
if (Array.isArray(reference[key]) || reference[key] instanceof Set) {
// only check if array is not empty
if (objectToCheck[key].length == 0) continue;
if (objectToCheck[key].size == 0) continue;
// check first item of array
const [firstOfObjectToCheck] = objectToCheck[key];
const [fisrtOfReference] = reference[key];
const doesFirstItemMatch = checkMatchesObjectStructure(
firstOfObjectToCheck,
fisrtOfReference
);
if (doesFirstItemMatch == false) return false;
} else if (requiredType == "object") {
// recurse into objects
const doesNestedObjectMatch = checkMatchesObjectStructure(
objectToCheck[key],
reference[key]
);
if (doesNestedObjectMatch == false) return false;
if (typeof reference != "object") {
return typeof objectToCheck == typeof reference;
}
}
return true;
for (const key of Object.keys(reference)) {
const requiredType = typeof reference[key];
const actualType = typeof objectToCheck[key];
if (requiredType != actualType) return false;
if (Array.isArray(reference[key]) || reference[key] instanceof Set) {
// only check if array is not empty
if (objectToCheck[key].length == 0) continue;
if (objectToCheck[key].size == 0) continue;
// check first item of array
const [firstOfObjectToCheck] = objectToCheck[key];
const [fisrtOfReference] = reference[key];
const doesFirstItemMatch = checkMatchesObjectStructure(
firstOfObjectToCheck,
fisrtOfReference,
);
if (doesFirstItemMatch == false) return false;
} else if (requiredType == "object") {
// recurse into objects
const doesNestedObjectMatch = checkMatchesObjectStructure(
objectToCheck[key],
reference[key],
);
if (doesNestedObjectMatch == false) return false;
}
}
return true;
}
export interface ValidObject {
dataVersion: "v2";
dataVersion: "v2";
}
+182 -181
View File
@@ -1,277 +1,278 @@
import { checkIsValidObject, checkMatchesObjectStructure } from "./typeSafety";
export interface Stringifiable {
toString(): string;
toString(): string;
}
// crypto
export function generateRandomToken(length: number): string {
const array = new Uint8Array(length);
crypto.getRandomValues(array);
return array.join("");
const array = new Uint8Array(length);
crypto.getRandomValues(array);
return array.join("");
}
// date
export function createTimestamp(): string {
return new Date().toISOString();
return new Date().toISOString();
}
// filters etc
export type StringEntryObject = { [key: string]: Stringifiable | undefined };
export function filterObjectsByStringEntries<T>(
reference: StringEntryObject,
converter: (object: T) => StringEntryObject,
objects: T[]
reference: StringEntryObject,
converter: (object: T) => StringEntryObject,
objects: T[],
): Set<T> {
const matches: Set<T> = new Set();
const matches: Set<T> = new Set();
object_loop: for (const object of objects) {
const doesMatch: boolean = checkDoesObjectMatchReference(
reference,
converter(object)
);
if (doesMatch) matches.add(object);
}
object_loop: for (const object of objects) {
const doesMatch: boolean = checkDoesObjectMatchReference(
reference,
converter(object),
);
if (doesMatch) matches.add(object);
}
return matches;
return matches;
}
export function checkDoesObjectMatchReference(
reference: StringEntryObject,
stringEntryObject: StringEntryObject
reference: StringEntryObject,
stringEntryObject: StringEntryObject,
): boolean {
reference_entry_loop: for (const referenceEntry of Object.entries(
reference
)) {
const [referenceKey, referenceValue] = referenceEntry;
const stringEntryObjectValue: Stringifiable | undefined =
stringEntryObject[referenceKey];
reference_entry_loop: for (const referenceEntry of Object.entries(
reference,
)) {
const [referenceKey, referenceValue] = referenceEntry;
const stringEntryObjectValue: Stringifiable | undefined =
stringEntryObject[referenceKey];
if (referenceValue == undefined) return false;
if (referenceValue == undefined) return false;
if (referenceValue[0] == "-") {
const strippedReferenceValue: string = referenceValue
.toString()
.substring(1);
// property may not exist
if (
strippedReferenceValue == "" &&
stringEntryObjectValue != undefined &&
stringEntryObjectValue != ""
) {
return false;
}
if (referenceValue[0] == "-") {
const strippedReferenceValue: string = referenceValue
.toString()
.substring(1);
// property may not exist
if (
strippedReferenceValue == "" &&
stringEntryObjectValue != undefined &&
stringEntryObjectValue != ""
) {
return false;
}
// property may not match
if (stringEntryObjectValue == strippedReferenceValue) {
return false;
}
} else {
// property must exist but be anything
if (
referenceValue == "" &&
(stringEntryObjectValue == undefined || stringEntryObjectValue == "")
) {
return false;
} else if (referenceValue == "") {
continue reference_entry_loop;
}
// property may not match
if (stringEntryObjectValue == strippedReferenceValue) {
return false;
}
} else {
// property must exist but be anything
if (
referenceValue == "" &&
(stringEntryObjectValue == undefined ||
stringEntryObjectValue == "")
) {
return false;
} else if (referenceValue == "") {
continue reference_entry_loop;
}
// property must match
if (stringEntryObjectValue != referenceValue) {
return false;
}
// property must match
if (stringEntryObjectValue != referenceValue) {
return false;
}
}
}
}
return true;
return true;
}
export function collectObjectValuesForKey<T>(
key: string,
converter: (object: T) => StringEntryObject,
objects: T[]
key: string,
converter: (object: T) => StringEntryObject,
objects: T[],
): string[] {
const values: Set<string> = new Set();
const values: Set<string> = new Set();
for (const object of objects) {
const stringEntryObject: StringEntryObject = converter(object);
const stringEntryObjectValue: Stringifiable | undefined =
stringEntryObject[key];
if (stringEntryObjectValue == undefined || stringEntryObjectValue == "")
continue;
for (const object of objects) {
const stringEntryObject: StringEntryObject = converter(object);
const stringEntryObjectValue: Stringifiable | undefined =
stringEntryObject[key];
if (stringEntryObjectValue == undefined || stringEntryObjectValue == "")
continue;
values.add(stringEntryObjectValue.toString());
}
values.add(stringEntryObjectValue.toString());
}
return [...values.values()];
return [...values.values()];
}
export function filterObjectsByWords<T>(
query: string,
getStringsOfObject: (object: T) => string[],
objects: T[]
query: string,
getStringsOfObject: (object: T) => string[],
objects: T[],
): Set<T> {
const matches: Set<T> = new Set();
const matches: Set<T> = new Set();
object_loop: for (const object of objects) {
const doesMatch: boolean = checkDoesObjectMatchSearch(
query,
getStringsOfObject,
object
);
if (doesMatch) matches.add(object);
}
object_loop: for (const object of objects) {
const doesMatch: boolean = checkDoesObjectMatchSearch(
query,
getStringsOfObject,
object,
);
if (doesMatch) matches.add(object);
}
return matches;
return matches;
}
export function checkDoesObjectMatchSearch<T>(
query: string,
getStringsOfObject: (object: T) => string[],
object: T
query: string,
getStringsOfObject: (object: T) => string[],
object: T,
): boolean {
if (query == "") return true;
if (query == "") return true;
const stringsInObject: string[] = getStringsOfObject(object);
const wordsInObject: string[] = [];
for (const string of stringsInObject) {
const lowercaseWordsInString = string
.toString()
.toLowerCase()
.split(" ")
.filter((word) => word != "");
wordsInObject.push(...lowercaseWordsInString);
}
const lowercaseWordsInQuery = query
.toLowerCase()
.split(" ")
.filter((word) => word != "");
for (const queryWord of lowercaseWordsInQuery) {
if (queryWord[0] == "-") {
// exclusion
const wordContent = queryWord.substring(1);
if (wordsInObject.includes(wordContent)) {
return false;
}
} else {
if (wordsInObject.includes(queryWord) == false) {
return false;
}
const stringsInObject: string[] = getStringsOfObject(object);
const wordsInObject: string[] = [];
for (const string of stringsInObject) {
const lowercaseWordsInString = string
.toString()
.toLowerCase()
.split(" ")
.filter((word) => word != "");
wordsInObject.push(...lowercaseWordsInString);
}
}
return true;
const lowercaseWordsInQuery = query
.toLowerCase()
.split(" ")
.filter((word) => word != "");
for (const queryWord of lowercaseWordsInQuery) {
if (queryWord[0] == "-") {
// exclusion
const wordContent = queryWord.substring(1);
if (wordsInObject.includes(wordContent)) {
return false;
}
} else {
if (wordsInObject.includes(queryWord) == false) {
return false;
}
}
}
return true;
}
// handlers
export type Handler<T> = (item: T) => void;
export class HandlerManager<T> {
handlers: Set<Handler<T>> = new Set();
handlers: Set<Handler<T>> = new Set();
// manage
addHandler = (handler: Handler<T>): void => {
this.handlers.add(handler);
};
// manage
addHandler = (handler: Handler<T>): void => {
this.handlers.add(handler);
};
deleteHandler = (handler: Handler<T>): void => {
this.handlers.delete(handler);
};
deleteHandler = (handler: Handler<T>): void => {
this.handlers.delete(handler);
};
// trigger
trigger = (item: T): void => {
for (const handler of this.handlers) {
handler(item);
}
};
// trigger
trigger = (item: T): void => {
for (const handler of this.handlers) {
handler(item);
}
};
}
// sorting
export class IndexManager<T> {
private itemToString: (item: T) => string;
private itemToString: (item: T) => string;
sortedStrings: string[] = [];
sortedStrings: string[] = [];
// methods
update = (items: T[]): void => {
this.sortedStrings = [];
// methods
update = (items: T[]): void => {
this.sortedStrings = [];
let strings: string[] = [];
for (const item of items) {
const string: string = this.itemToString(item);
strings.push(string);
let strings: string[] = [];
for (const item of items) {
const string: string = this.itemToString(item);
strings.push(string);
}
this.sortedStrings = strings.sort(localeCompare);
};
getIndex = (item: T): number => {
const string: string = this.itemToString(item);
const index: number = this.sortedStrings.indexOf(string);
return index;
};
// init
constructor(itemToString: (item: T) => string) {
this.itemToString = itemToString;
}
this.sortedStrings = strings.sort(localeCompare);
};
getIndex = (item: T): number => {
const string: string = this.itemToString(item);
const index: number = this.sortedStrings.indexOf(string);
return index;
};
// init
constructor(itemToString: (item: T) => string) {
this.itemToString = itemToString;
}
}
// storage
export function getLocalStorageItemAndClear(key: string): string | null {
const value: string | null = localStorage.getItem(key);
localStorage.removeItem(key);
if (value != null) localStorage.setItem(`_${key}`, value);
return value;
const value: string | null = localStorage.getItem(key);
localStorage.removeItem(key);
if (value != null) localStorage.setItem(`_${key}`, value);
return value;
}
// string & parsing
export function stringify(data: any): string {
return JSON.stringify(data, null, 4);
return JSON.stringify(data, null, 4);
}
export function padZero(string: string | undefined, length: number): string {
return (string ?? "").padStart(length, "0");
return (string ?? "").padStart(length, "0");
}
export function parse(string: string): any {
try {
return JSON.parse(string);
} catch {
return {};
}
try {
return JSON.parse(string);
} catch {
return {};
}
}
export function parseValidObject<T>(string: string, reference: T): T | null {
const parsed: any = parse(string);
if (checkIsValidObject(parsed) == false) return null;
const parsed: any = parse(string);
if (checkIsValidObject(parsed) == false) return null;
const doesMatchReference: boolean = checkMatchesObjectStructure(
parsed,
reference
);
if (doesMatchReference == false) return null;
const doesMatchReference: boolean = checkMatchesObjectStructure(
parsed,
reference,
);
if (doesMatchReference == false) return null;
return parsed;
return parsed;
}
export function parseOrFallback(inputString: string): any {
try {
return JSON.parse(inputString);
} catch {
return inputString;
}
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;
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);
return a.localeCompare(b);
}