implement auto reconnect
This commit is contained in:
@@ -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
@@ -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: "",
|
||||
};
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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: "",
|
||||
};
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user