format
This commit is contained in:
+283
-274
@@ -37,433 +37,442 @@ export default class ChatModel {
|
||||
info: ChatInfoFileContent; /* load function called in constructor */
|
||||
|
||||
get secondaryChannels(): string[] {
|
||||
return this.info.secondaryChannels.sort(localeCompare);
|
||||
return this.info.secondaryChannels.sort(localeCompare);
|
||||
}
|
||||
|
||||
get color(): Colors {
|
||||
const color = this.info.color as Colors;
|
||||
if ([...Object.values(Colors)].includes(color)) return color;
|
||||
return Colors.Standard;
|
||||
const color = this.info.color as Colors;
|
||||
if ([...Object.values(Colors)].includes(color)) return color;
|
||||
return Colors.Standard;
|
||||
}
|
||||
|
||||
// handler managers
|
||||
readonly chatMessageHandlerManager: HandlerManager<ChatMessage> =
|
||||
new HandlerManager();
|
||||
new HandlerManager();
|
||||
readonly reactionHandlerManager: HandlerManager<ChatMessageReaction> =
|
||||
new HandlerManager();
|
||||
new HandlerManager();
|
||||
readonly changeHandlerManager: HandlerManager<null> = new HandlerManager();
|
||||
|
||||
// paths
|
||||
readonly getBasePath = (): string[] => {
|
||||
return StorageModel.getPath(
|
||||
StorageModelSubPaths.Chat,
|
||||
filePaths.chat.chatBase(this.id),
|
||||
);
|
||||
return StorageModel.getPath(
|
||||
StorageModelSubPaths.Chat,
|
||||
filePaths.chat.chatBase(this.id),
|
||||
);
|
||||
};
|
||||
|
||||
readonly getInfoPath = (): string[] => {
|
||||
return StorageModel.getPath(
|
||||
StorageModelSubPaths.Chat,
|
||||
filePaths.chat.info(this.id),
|
||||
);
|
||||
return StorageModel.getPath(
|
||||
StorageModelSubPaths.Chat,
|
||||
filePaths.chat.info(this.id),
|
||||
);
|
||||
};
|
||||
|
||||
readonly getColorPath = (): string[] => {
|
||||
return StorageModel.getPath(
|
||||
StorageModelSubPaths.Chat,
|
||||
filePaths.chat.color(this.id),
|
||||
);
|
||||
return StorageModel.getPath(
|
||||
StorageModelSubPaths.Chat,
|
||||
filePaths.chat.color(this.id),
|
||||
);
|
||||
};
|
||||
|
||||
readonly getMessageDirPath = (): string[] => {
|
||||
return StorageModel.getPath(
|
||||
StorageModelSubPaths.Chat,
|
||||
filePaths.chat.messages(this.id),
|
||||
);
|
||||
return StorageModel.getPath(
|
||||
StorageModelSubPaths.Chat,
|
||||
filePaths.chat.messages(this.id),
|
||||
);
|
||||
};
|
||||
|
||||
readonly getMessagePath = (id: string): string[] => {
|
||||
return [...this.getMessageDirPath(), id];
|
||||
return [...this.getMessageDirPath(), id];
|
||||
};
|
||||
|
||||
readonly getReactionDirPath = (): string[] => {
|
||||
return StorageModel.getPath(
|
||||
StorageModelSubPaths.Chat,
|
||||
filePaths.chat.reactions(this.id),
|
||||
);
|
||||
return StorageModel.getPath(
|
||||
StorageModelSubPaths.Chat,
|
||||
filePaths.chat.reactions(this.id),
|
||||
);
|
||||
};
|
||||
readonly getReactionPath = (id: string): string[] => {
|
||||
return [...this.getReactionDirPath(), id];
|
||||
return [...this.getReactionDirPath(), id];
|
||||
};
|
||||
|
||||
readonly getPreviousFilterPath = (): string[] => {
|
||||
return [StorageModelSubPaths.Chat, ...filePaths.chat.previousFilter(this.id)];
|
||||
}
|
||||
return [
|
||||
StorageModelSubPaths.Chat,
|
||||
...filePaths.chat.previousFilter(this.id),
|
||||
];
|
||||
};
|
||||
|
||||
// handlers
|
||||
readonly handleMessage = (body: string): void => {
|
||||
const chatMessage: ChatMessage | null = parseValidObject(
|
||||
body,
|
||||
ChatMessageReference,
|
||||
);
|
||||
if (chatMessage == null) return;
|
||||
const chatMessage: ChatMessage | null = parseValidObject(
|
||||
body,
|
||||
ChatMessageReference,
|
||||
);
|
||||
if (chatMessage == null) return;
|
||||
|
||||
chatMessage.status = ChatMessageStatuses.Received;
|
||||
chatMessage.status = ChatMessageStatuses.Received;
|
||||
|
||||
this.addMessage(chatMessage);
|
||||
this.addMessage(chatMessage);
|
||||
|
||||
if (chatMessage.stringifiedFile) return;
|
||||
this.setReadStatus(true);
|
||||
if (chatMessage.stringifiedFile) return;
|
||||
this.setReadStatus(true);
|
||||
};
|
||||
|
||||
readonly handleReaction = (reaction: ChatMessageReaction | any): void => {
|
||||
if (
|
||||
!checkMatchesObjectStructure(reaction, ChatMessageReactionReference)
|
||||
)
|
||||
return;
|
||||
const reactionPath: string[] = this.getReactionPath(reaction.fileId);
|
||||
if (
|
||||
!checkMatchesObjectStructure(reaction, ChatMessageReactionReference)
|
||||
)
|
||||
return;
|
||||
const reactionPath: string[] = this.getReactionPath(reaction.fileId);
|
||||
|
||||
if (reaction.isDeleting == true) {
|
||||
this.storageModel.remove(reactionPath);
|
||||
} else {
|
||||
this.storageModel.writeStringifiable(reactionPath, reaction);
|
||||
}
|
||||
this.reactionHandlerManager.trigger(reaction);
|
||||
if (reaction.isDeleting == true) {
|
||||
this.storageModel.remove(reactionPath);
|
||||
} else {
|
||||
this.storageModel.writeStringifiable(reactionPath, reaction);
|
||||
}
|
||||
this.reactionHandlerManager.trigger(reaction);
|
||||
};
|
||||
|
||||
readonly handleMessageSent = (chatMessage: ChatMessage): void => {
|
||||
chatMessage.status = ChatMessageStatuses.Sent;
|
||||
this.addMessage(chatMessage);
|
||||
chatMessage.status = ChatMessageStatuses.Sent;
|
||||
this.addMessage(chatMessage);
|
||||
};
|
||||
|
||||
// settings
|
||||
readonly setName = (name: string): void => {
|
||||
this.info.name = name;
|
||||
this.storeInfo();
|
||||
this.syncInfo();
|
||||
this.subscribe();
|
||||
this.info.name = name;
|
||||
this.storeInfo();
|
||||
this.syncInfo();
|
||||
this.subscribe();
|
||||
};
|
||||
|
||||
readonly setSecondaryChannels = (secondaryChannels: string[]): void => {
|
||||
this.info.secondaryChannels = secondaryChannels;
|
||||
this.storeInfo();
|
||||
this.info.secondaryChannels = secondaryChannels;
|
||||
this.storeInfo();
|
||||
};
|
||||
|
||||
readonly setEncryptionKey = (key: string): void => {
|
||||
this.info.encryptionKey = key;
|
||||
this.storeInfo();
|
||||
this.info.encryptionKey = key;
|
||||
this.storeInfo();
|
||||
};
|
||||
|
||||
readonly setColor = (color: Colors): void => {
|
||||
this.info.color = color;
|
||||
this.syncInfo();
|
||||
this.storeInfo();
|
||||
this.info.color = color;
|
||||
this.syncInfo();
|
||||
this.storeInfo();
|
||||
};
|
||||
|
||||
// messaging
|
||||
readonly addMessage = async (chatMessage: ChatMessage): Promise<void> => {
|
||||
await this.decryptMessage(chatMessage);
|
||||
await this.decryptMessage(chatMessage);
|
||||
|
||||
// message
|
||||
if (chatMessage.body != "") {
|
||||
const messagePath: string[] = this.getMessagePath(chatMessage.id);
|
||||
this.storageModel.writeStringifiable(messagePath, chatMessage);
|
||||
this.chatMessageHandlerManager.trigger(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,
|
||||
);
|
||||
// file
|
||||
this.fileModel.handleStringifiedFileContent(
|
||||
chatMessage.stringifiedFile,
|
||||
);
|
||||
};
|
||||
|
||||
readonly getNameAndChannel = (): [string, string] | false => {
|
||||
const senderName: string = this.settingsModel.username;
|
||||
if (senderName == "") return false;
|
||||
const senderName: string = this.settingsModel.username;
|
||||
if (senderName == "") return false;
|
||||
|
||||
const allChannels: string[] = [this.id];
|
||||
for (const secondaryChannel of this.info.secondaryChannels) {
|
||||
allChannels.push(secondaryChannel);
|
||||
}
|
||||
const allChannels: string[] = [this.id];
|
||||
for (const secondaryChannel of this.info.secondaryChannels) {
|
||||
allChannels.push(secondaryChannel);
|
||||
}
|
||||
|
||||
const combinedChannel: string = allChannels.join("/");
|
||||
const combinedChannel: string = allChannels.join("/");
|
||||
|
||||
return [senderName, combinedChannel];
|
||||
return [senderName, combinedChannel];
|
||||
};
|
||||
|
||||
readonly sendMessage = async (
|
||||
body: string,
|
||||
inlineReplyId?: string,
|
||||
fileContent?: FileContent<string>,
|
||||
): Promise<string|false> => {
|
||||
const nameAndChannel = this.getNameAndChannel();
|
||||
if (nameAndChannel == false) return false;
|
||||
const [senderName, combinedChannel] = nameAndChannel;
|
||||
body: string,
|
||||
inlineReplyId?: string,
|
||||
fileContent?: FileContent<string>,
|
||||
): Promise<string | false> => {
|
||||
const nameAndChannel = this.getNameAndChannel();
|
||||
if (nameAndChannel == false) return false;
|
||||
const [senderName, combinedChannel] = nameAndChannel;
|
||||
|
||||
const chatMessage: ChatMessage = await ChatModel.createChatMessage(
|
||||
combinedChannel,
|
||||
senderName,
|
||||
this.info.encryptionKey,
|
||||
body,
|
||||
inlineReplyId,
|
||||
fileContent,
|
||||
);
|
||||
const chatMessage: ChatMessage = await ChatModel.createChatMessage(
|
||||
combinedChannel,
|
||||
senderName,
|
||||
this.info.encryptionKey,
|
||||
body,
|
||||
inlineReplyId,
|
||||
fileContent,
|
||||
);
|
||||
|
||||
this.addMessage(chatMessage);
|
||||
this.connectionModel.sendMessageOrStore(chatMessage);
|
||||
return chatMessage.id;
|
||||
this.addMessage(chatMessage);
|
||||
this.connectionModel.sendMessageOrStore(chatMessage);
|
||||
return chatMessage.id;
|
||||
};
|
||||
|
||||
readonly decryptMessage = async (
|
||||
chatMessage: ChatMessage,
|
||||
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;
|
||||
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;
|
||||
};
|
||||
|
||||
readonly sendReaction = async (
|
||||
messageId: string,
|
||||
content: ReactionSymbols,
|
||||
isDeleting: boolean = false,
|
||||
messageId: string,
|
||||
content: ReactionSymbols,
|
||||
isDeleting: boolean = false,
|
||||
): Promise<void> => {
|
||||
const nameAndChannel = this.getNameAndChannel();
|
||||
if (nameAndChannel == false) return;
|
||||
const [senderName] = nameAndChannel;
|
||||
const nameAndChannel = this.getNameAndChannel();
|
||||
if (nameAndChannel == false) return;
|
||||
const [senderName] = nameAndChannel;
|
||||
|
||||
const reaction = ChatModel.createMessageReaction(
|
||||
messageId,
|
||||
senderName,
|
||||
content,
|
||||
isDeleting,
|
||||
);
|
||||
this.sendMessage("", undefined, reaction);
|
||||
this.handleReaction(reaction);
|
||||
const reaction = ChatModel.createMessageReaction(
|
||||
messageId,
|
||||
senderName,
|
||||
content,
|
||||
isDeleting,
|
||||
);
|
||||
this.sendMessage("", undefined, reaction);
|
||||
this.handleReaction(reaction);
|
||||
};
|
||||
|
||||
readonly subscribe = (): void => {
|
||||
this.connectionModel.addChannel(this.id);
|
||||
this.connectionModel.addChannel(this.id);
|
||||
};
|
||||
|
||||
readonly setReadStatus = (hasUnreadMessages: boolean): void => {
|
||||
this.info.hasUnreadMessages = hasUnreadMessages;
|
||||
this.storeInfo();
|
||||
this.info.hasUnreadMessages = hasUnreadMessages;
|
||||
this.storeInfo();
|
||||
};
|
||||
|
||||
// storage
|
||||
readonly storeInfo = (): void => {
|
||||
this.storageModel.writeStringifiable(this.getInfoPath(), this.info);
|
||||
this.storageModel.writeStringifiable(this.getInfoPath(), this.info);
|
||||
};
|
||||
|
||||
readonly syncInfo = (): void => {
|
||||
this.sendMessage("", undefined, this.info);
|
||||
}
|
||||
this.sendMessage("", undefined, this.info);
|
||||
};
|
||||
|
||||
readonly storeFilter = (filter: string): void => {
|
||||
this.storageModel.write(this.getPreviousFilterPath(), filter);
|
||||
}
|
||||
this.storageModel.write(this.getPreviousFilterPath(), filter);
|
||||
};
|
||||
|
||||
readonly getFilter = (): string=> {
|
||||
return this.storageModel.read(this.getPreviousFilterPath()) || "";
|
||||
}
|
||||
readonly getFilter = (): string => {
|
||||
return this.storageModel.read(this.getPreviousFilterPath()) || "";
|
||||
};
|
||||
|
||||
readonly delete = () => {
|
||||
// untrack
|
||||
this.chatListModel.untrackChat(this);
|
||||
// untrack
|
||||
this.chatListModel.untrackChat(this);
|
||||
|
||||
// delete
|
||||
const dirPath: string[] = this.getBasePath();
|
||||
this.storageModel.removeRecursively(dirPath);
|
||||
// delete
|
||||
const dirPath: string[] = this.getBasePath();
|
||||
this.storageModel.removeRecursively(dirPath);
|
||||
};
|
||||
|
||||
// load
|
||||
readonly loadInfo = (): void => {
|
||||
let info: ChatInfoFileContent | null = this.storageModel.readStringifiable(
|
||||
this.getInfoPath(),
|
||||
ChatInfoReference,
|
||||
);
|
||||
this.info = (info || ChatModel.generateChatInfo("0", "0", Colors.Standard));
|
||||
this.storeInfo();
|
||||
let info: ChatInfoFileContent | null =
|
||||
this.storageModel.readStringifiable(
|
||||
this.getInfoPath(),
|
||||
ChatInfoReference,
|
||||
);
|
||||
this.info =
|
||||
info || ChatModel.generateChatInfo("0", "0", Colors.Standard);
|
||||
this.storeInfo();
|
||||
};
|
||||
|
||||
readonly handleInfo = (info: ChatInfoFileContent): void => {
|
||||
this.info = info;
|
||||
this.changeHandlerManager.trigger(null);
|
||||
this.storeInfo();
|
||||
}
|
||||
this.info = info;
|
||||
this.changeHandlerManager.trigger(null);
|
||||
this.storeInfo();
|
||||
};
|
||||
|
||||
get messages(): ChatMessage[] {
|
||||
const messageIds: string[] = this.storageModel.list(
|
||||
this.getMessageDirPath(),
|
||||
);
|
||||
if (!Array.isArray(messageIds)) return [];
|
||||
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 | null =
|
||||
this.storageModel.readStringifiable(
|
||||
messagePath,
|
||||
ChatMessageReference,
|
||||
);
|
||||
if (chatMessage == null) continue;
|
||||
chatMessages.push(chatMessage);
|
||||
}
|
||||
const chatMessages: ChatMessage[] = [];
|
||||
for (const messageId of messageIds) {
|
||||
const messagePath: string[] = this.getMessagePath(messageId);
|
||||
const chatMessage: ChatMessage | null =
|
||||
this.storageModel.readStringifiable(
|
||||
messagePath,
|
||||
ChatMessageReference,
|
||||
);
|
||||
if (chatMessage == null) continue;
|
||||
chatMessages.push(chatMessage);
|
||||
}
|
||||
|
||||
const sorted = chatMessages.sort((a, b) =>
|
||||
a.dateSent.localeCompare(b.dateSent),
|
||||
);
|
||||
return sorted;
|
||||
const sorted = chatMessages.sort((a, b) =>
|
||||
a.dateSent.localeCompare(b.dateSent),
|
||||
);
|
||||
return sorted;
|
||||
}
|
||||
|
||||
get reactions(): ChatMessageReaction[] {
|
||||
const reactionIds: string[] = this.storageModel.list(
|
||||
this.getReactionDirPath(),
|
||||
);
|
||||
if (!Array.isArray(reactionIds)) return [];
|
||||
const reactionIds: string[] = this.storageModel.list(
|
||||
this.getReactionDirPath(),
|
||||
);
|
||||
if (!Array.isArray(reactionIds)) return [];
|
||||
|
||||
const reactions: ChatMessageReaction[] = [];
|
||||
for (const reactionId of reactionIds) {
|
||||
const reactionPath: string[] = this.getReactionPath(reactionId);
|
||||
const reaction: ChatMessageReaction | null =
|
||||
this.storageModel.readStringifiable(
|
||||
reactionPath,
|
||||
ChatMessageReactionReference,
|
||||
);
|
||||
if (reaction == null) continue;
|
||||
reactions.push(reaction);
|
||||
}
|
||||
const reactions: ChatMessageReaction[] = [];
|
||||
for (const reactionId of reactionIds) {
|
||||
const reactionPath: string[] = this.getReactionPath(reactionId);
|
||||
const reaction: ChatMessageReaction | null =
|
||||
this.storageModel.readStringifiable(
|
||||
reactionPath,
|
||||
ChatMessageReactionReference,
|
||||
);
|
||||
if (reaction == null) continue;
|
||||
reactions.push(reaction);
|
||||
}
|
||||
|
||||
return reactions;
|
||||
return reactions;
|
||||
}
|
||||
|
||||
// init
|
||||
constructor(
|
||||
storageModel: StorageModel,
|
||||
connectionModel: ConnectionModel,
|
||||
settingsModel: SettingsModel,
|
||||
chatListModel: ChatListModel,
|
||||
chatId: string,
|
||||
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.id = chatId;
|
||||
this.connectionModel = connectionModel;
|
||||
this.settingsModel = settingsModel;
|
||||
this.storageModel = storageModel;
|
||||
this.chatListModel = chatListModel;
|
||||
|
||||
this.loadInfo();
|
||||
this.subscribe();
|
||||
this.loadInfo();
|
||||
this.subscribe();
|
||||
|
||||
this.fileModel = new FileModel(
|
||||
this.storageModel,
|
||||
this.settingsModel,
|
||||
this,
|
||||
);
|
||||
this.fileModel = new FileModel(
|
||||
this.storageModel,
|
||||
this.settingsModel,
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
// utility
|
||||
static splitChannel(channelString: string): string[] {
|
||||
return channelString.split("/");
|
||||
return channelString.split("/");
|
||||
}
|
||||
|
||||
static generateChatInfo = (name: string, id: string, color: Colors): ChatInfoFileContent => {
|
||||
const file = FileModel.createFileContent(id, "chat-info");
|
||||
static generateChatInfo = (
|
||||
name: string,
|
||||
id: string,
|
||||
color: Colors,
|
||||
): ChatInfoFileContent => {
|
||||
const file = FileModel.createFileContent(id, "chat-info");
|
||||
|
||||
return {
|
||||
...file,
|
||||
return {
|
||||
...file,
|
||||
|
||||
name,
|
||||
secondaryChannels: [],
|
||||
encryptionKey: "",
|
||||
color,
|
||||
hasUnreadMessages: false,
|
||||
};
|
||||
name,
|
||||
secondaryChannels: [],
|
||||
encryptionKey: "",
|
||||
color,
|
||||
hasUnreadMessages: false,
|
||||
};
|
||||
};
|
||||
|
||||
static createChatMessage = async (
|
||||
channel: string,
|
||||
sender: string,
|
||||
encryptionKey: string,
|
||||
body: string,
|
||||
inlineReplyId?: string,
|
||||
fileContent?: FileContent<string>,
|
||||
channel: string,
|
||||
sender: string,
|
||||
encryptionKey: string,
|
||||
body: string,
|
||||
inlineReplyId?: string,
|
||||
fileContent?: FileContent<string>,
|
||||
): Promise<ChatMessage> => {
|
||||
const chatMessage: ChatMessage = {
|
||||
dataVersion: DATA_VERSION,
|
||||
const chatMessage: ChatMessage = {
|
||||
dataVersion: DATA_VERSION,
|
||||
|
||||
id: v4(),
|
||||
id: v4(),
|
||||
|
||||
channel,
|
||||
sender,
|
||||
body,
|
||||
dateSent: createTimestamp(),
|
||||
inlineReplyId,
|
||||
channel,
|
||||
sender,
|
||||
body,
|
||||
dateSent: createTimestamp(),
|
||||
inlineReplyId,
|
||||
|
||||
status: ChatMessageStatuses.Outbox,
|
||||
stringifiedFile: "",
|
||||
};
|
||||
if (fileContent != undefined) {
|
||||
const stringifiedFile: string = stringify(fileContent);
|
||||
chatMessage.stringifiedFile = stringifiedFile;
|
||||
}
|
||||
status: ChatMessageStatuses.Outbox,
|
||||
stringifiedFile: "",
|
||||
};
|
||||
if (fileContent != undefined) {
|
||||
const stringifiedFile: string = stringify(fileContent);
|
||||
chatMessage.stringifiedFile = stringifiedFile;
|
||||
}
|
||||
|
||||
if (encryptionKey != "") {
|
||||
chatMessage.body = await encryptString(
|
||||
chatMessage.body,
|
||||
encryptionKey,
|
||||
);
|
||||
chatMessage.stringifiedFile = await encryptString(
|
||||
chatMessage.stringifiedFile,
|
||||
encryptionKey,
|
||||
);
|
||||
}
|
||||
if (encryptionKey != "") {
|
||||
chatMessage.body = await encryptString(
|
||||
chatMessage.body,
|
||||
encryptionKey,
|
||||
);
|
||||
chatMessage.stringifiedFile = await encryptString(
|
||||
chatMessage.stringifiedFile,
|
||||
encryptionKey,
|
||||
);
|
||||
}
|
||||
|
||||
return chatMessage;
|
||||
return chatMessage;
|
||||
};
|
||||
|
||||
static createMessageReaction = (
|
||||
messageId: string,
|
||||
sender: string,
|
||||
content: ReactionSymbols,
|
||||
isDeleting: boolean,
|
||||
messageId: string,
|
||||
sender: string,
|
||||
content: ReactionSymbols,
|
||||
isDeleting: boolean,
|
||||
): ChatMessageReaction => {
|
||||
const fileContent: FileContent<"reaction"> =
|
||||
FileModel.createFileContent(v4(), "reaction");
|
||||
const reaction: ChatMessageReaction = {
|
||||
...fileContent,
|
||||
fileId: ChatModel.createMessageReactionId(messageId, sender),
|
||||
const fileContent: FileContent<"reaction"> =
|
||||
FileModel.createFileContent(v4(), "reaction");
|
||||
const reaction: ChatMessageReaction = {
|
||||
...fileContent,
|
||||
fileId: ChatModel.createMessageReactionId(messageId, sender),
|
||||
|
||||
messageId,
|
||||
sender,
|
||||
content,
|
||||
isDeleting,
|
||||
};
|
||||
return reaction;
|
||||
messageId,
|
||||
sender,
|
||||
content,
|
||||
isDeleting,
|
||||
};
|
||||
return reaction;
|
||||
};
|
||||
|
||||
static createMessageReactionId = (
|
||||
messageId: string,
|
||||
sender: string,
|
||||
messageId: string,
|
||||
sender: string,
|
||||
): string => {
|
||||
return messageId + sender;
|
||||
return messageId + sender;
|
||||
};
|
||||
}
|
||||
|
||||
// types
|
||||
export enum ReactionSymbols {
|
||||
ThumbsUp = "👍",
|
||||
Check = "✅",
|
||||
Stop = "🛑",
|
||||
Attention = "❗️",
|
||||
DoubleAttention = "‼️",
|
||||
Question = "❓",
|
||||
Check = "✅",
|
||||
Stop = "🛑",
|
||||
Attention = "❗️",
|
||||
DoubleAttention = "‼️",
|
||||
Question = "❓",
|
||||
}
|
||||
|
||||
export interface ChatInfoFileContent extends FileContent<"chat-info"> {
|
||||
@@ -478,9 +487,9 @@ export interface ChatInfoFileContent extends FileContent<"chat-info"> {
|
||||
|
||||
export enum ChatMessageStatuses {
|
||||
Outbox = "outbox",
|
||||
Sent = "sent",
|
||||
Received = "received",
|
||||
Other = "other",
|
||||
Sent = "sent",
|
||||
Received = "received",
|
||||
Other = "other",
|
||||
}
|
||||
|
||||
export interface ChatMessage extends ValidObject {
|
||||
@@ -498,7 +507,7 @@ export interface ChatMessage extends ValidObject {
|
||||
}
|
||||
|
||||
export interface ChatMessageReaction
|
||||
extends ValidObject, FileContent<"reaction"> {
|
||||
extends ValidObject, FileContent<"reaction"> {
|
||||
readonly messageId: string;
|
||||
readonly sender: string;
|
||||
readonly content: ReactionSymbols | string;
|
||||
@@ -552,4 +561,4 @@ export const ChatMessageReactionReference: ChatMessageReaction = {
|
||||
content: "",
|
||||
|
||||
isDeleting: false,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
import FileModel, { FileModelSubPath } from "./fileModel";
|
||||
import { TaskFileContent } from "./boardsAndTasksModel";
|
||||
import { isoToMonthString, padZero } from "../Utility/utility";
|
||||
import StorageModel from "../Global/storageModel";
|
||||
import SettingsModel from "../Global/settingsModel";
|
||||
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
|
||||
import {isoToMonthString, padZero} from "../Utility/utility";
|
||||
|
||||
export default class CalendarModel {
|
||||
readonly storageModel: StorageModel;
|
||||
@@ -35,9 +35,7 @@ export default class CalendarModel {
|
||||
readonly storeTaskReference = (taskFileContent: TaskFileContent): void => {
|
||||
if (taskFileContent.date == undefined) return;
|
||||
|
||||
const monthString: string = isoToMonthString(
|
||||
taskFileContent.date,
|
||||
);
|
||||
const monthString: string = isoToMonthString(taskFileContent.date);
|
||||
|
||||
const monthPath: string[] = this.getMonthPath(monthString);
|
||||
const referencePath: string[] = [...monthPath, taskFileContent.fileId];
|
||||
@@ -103,9 +101,7 @@ export default class CalendarModel {
|
||||
};
|
||||
|
||||
for (let i = 0; i < daysInMonth; i++) {
|
||||
const paddedDate: string = padZero(
|
||||
(i + 1).toString(), 2
|
||||
);
|
||||
const paddedDate: string = padZero((i + 1).toString(), 2);
|
||||
grid.days[paddedDate] = defaultValueCreator();
|
||||
}
|
||||
|
||||
|
||||
+108
-96
@@ -8,13 +8,21 @@ import {
|
||||
parseValidObject,
|
||||
stringify,
|
||||
} from "../Utility/utility";
|
||||
import { checkIsValidObject, checkMatchesObjectStructure, DATA_VERSION, ValidObject } from "../Utility/typeSafety";
|
||||
import {
|
||||
checkIsValidObject,
|
||||
checkMatchesObjectStructure,
|
||||
DATA_VERSION,
|
||||
ValidObject,
|
||||
} from "../Utility/typeSafety";
|
||||
import StorageModel, {
|
||||
StorageModelSubPaths,
|
||||
filePaths,
|
||||
} from "../Global/storageModel";
|
||||
import SettingsModel from "../Global/settingsModel";
|
||||
import ChatModel, {ChatInfoFileContent, ChatInfoReference} from "../Chat/chatModel";
|
||||
import ChatModel, {
|
||||
ChatInfoFileContent,
|
||||
ChatInfoReference,
|
||||
} from "../Chat/chatModel";
|
||||
|
||||
export default class FileModel {
|
||||
readonly storageModel: StorageModel;
|
||||
@@ -67,161 +75,165 @@ export default class FileModel {
|
||||
};
|
||||
|
||||
readonly handleFileContent = (fileContent: FileContent<string>): void => {
|
||||
console.log(fileContent.type);
|
||||
console.log(fileContent.type);
|
||||
|
||||
if (fileContent.type == "chat-info") {
|
||||
if (checkMatchesObjectStructure(fileContent, ChatInfoReference) == false) return;
|
||||
this.chatModel.handleInfo(fileContent as ChatInfoFileContent);
|
||||
}
|
||||
if (fileContent.type == "chat-info") {
|
||||
if (
|
||||
checkMatchesObjectStructure(fileContent, ChatInfoReference) ==
|
||||
false
|
||||
)
|
||||
return;
|
||||
this.chatModel.handleInfo(fileContent as ChatInfoFileContent);
|
||||
}
|
||||
|
||||
const didStore: boolean = this.storeFileContent(fileContent);
|
||||
if (didStore == false) return;
|
||||
const didStore: boolean = this.storeFileContent(fileContent);
|
||||
if (didStore == false) return;
|
||||
|
||||
switch (fileContent.type) {
|
||||
case "board-info":
|
||||
case "task":
|
||||
this.boardsAndTasksModel.handleFileContent(fileContent);
|
||||
break;
|
||||
case "reaction":
|
||||
this.chatModel.handleReaction(fileContent);
|
||||
break;
|
||||
}
|
||||
switch (fileContent.type) {
|
||||
case "board-info":
|
||||
case "task":
|
||||
this.boardsAndTasksModel.handleFileContent(fileContent);
|
||||
break;
|
||||
case "reaction":
|
||||
this.chatModel.handleReaction(fileContent);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// methods
|
||||
readonly addFileContentAndSend = (
|
||||
fileContent: FileContent<string>,
|
||||
fileContent: FileContent<string>,
|
||||
): void => {
|
||||
this.handleFileContent(fileContent);
|
||||
this.chatModel.sendMessage("", undefined, fileContent);
|
||||
this.handleFileContent(fileContent);
|
||||
this.chatModel.sendMessage("", undefined, fileContent);
|
||||
};
|
||||
|
||||
// storage
|
||||
readonly storeFileContent = (fileContent: FileContent<string>): boolean => {
|
||||
const fileContentPath: string[] = this.getFileContentPath(
|
||||
fileContent.fileId,
|
||||
fileContent.fileContentId,
|
||||
);
|
||||
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;
|
||||
// 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;
|
||||
const stringifiedContent: string = stringify(fileContent);
|
||||
this.storageModel.write(fileContentPath, stringifiedContent);
|
||||
return true;
|
||||
};
|
||||
|
||||
readonly listFileIds = (): string[] => {
|
||||
return this.storageModel.list(this.basePath);
|
||||
return this.storageModel.list(this.basePath);
|
||||
};
|
||||
|
||||
readonly listFileContentIds = (fileId: string): string[] => {
|
||||
const filePath: string[] = this.getFilePath(fileId);
|
||||
return this.storageModel.list(filePath);
|
||||
const filePath: string[] = this.getFilePath(fileId);
|
||||
return this.storageModel.list(filePath);
|
||||
};
|
||||
|
||||
readonly selectLatestFileContentId = (
|
||||
fileContentIds: string[],
|
||||
fileContentIds: string[],
|
||||
): string | undefined => {
|
||||
return fileContentIds[fileContentIds.length - 1];
|
||||
return fileContentIds[fileContentIds.length - 1];
|
||||
};
|
||||
|
||||
readonly getFileContent = <T extends FileContent<string>>(
|
||||
fileId: string,
|
||||
fileContentName: string,
|
||||
reference: T,
|
||||
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;
|
||||
const filePath: string[] = this.getFileContentPath(
|
||||
fileId,
|
||||
fileContentName,
|
||||
);
|
||||
const fileContentOrNull: T | null = this.storageModel.readStringifiable(
|
||||
filePath,
|
||||
reference,
|
||||
);
|
||||
return fileContentOrNull;
|
||||
};
|
||||
|
||||
readonly getLatestFileContent = <T extends FileContent<string>>(
|
||||
fileId: string,
|
||||
reference: T,
|
||||
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 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;
|
||||
const fileContent: T | null = this.getFileContent(
|
||||
fileId,
|
||||
latestFileContentId,
|
||||
reference,
|
||||
);
|
||||
return fileContent;
|
||||
};
|
||||
|
||||
// init
|
||||
constructor(
|
||||
storageModel: StorageModel,
|
||||
settingsModel: SettingsModel,
|
||||
chatModel: ChatModel,
|
||||
storageModel: StorageModel,
|
||||
settingsModel: SettingsModel,
|
||||
chatModel: ChatModel,
|
||||
) {
|
||||
this.chatModel = chatModel;
|
||||
this.settingsModel = settingsModel;
|
||||
this.storageModel = storageModel;
|
||||
this.chatModel = chatModel;
|
||||
this.settingsModel = settingsModel;
|
||||
this.storageModel = storageModel;
|
||||
|
||||
this.boardsAndTasksModel = new BoardsAndTasksModel(
|
||||
this.storageModel,
|
||||
this.settingsModel,
|
||||
chatModel,
|
||||
this,
|
||||
);
|
||||
this.boardsAndTasksModel = new BoardsAndTasksModel(
|
||||
this.storageModel,
|
||||
this.settingsModel,
|
||||
chatModel,
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
// utility
|
||||
static generateFileContentId = (creationDate: string): string => {
|
||||
return creationDate + v4();
|
||||
return creationDate + v4();
|
||||
};
|
||||
|
||||
static createFileContent = <T extends string>(
|
||||
fileId: string,
|
||||
type: T,
|
||||
fileId: string,
|
||||
type: T,
|
||||
): FileContent<T> => {
|
||||
const creationDate: string = createTimestamp();
|
||||
const fileContentId: string =
|
||||
FileModel.generateFileContentId(creationDate);
|
||||
const creationDate: string = createTimestamp();
|
||||
const fileContentId: string =
|
||||
FileModel.generateFileContentId(creationDate);
|
||||
|
||||
return {
|
||||
dataVersion: DATA_VERSION,
|
||||
return {
|
||||
dataVersion: DATA_VERSION,
|
||||
|
||||
fileId,
|
||||
fileContentId,
|
||||
creationDate,
|
||||
type,
|
||||
};
|
||||
fileId,
|
||||
fileContentId,
|
||||
creationDate,
|
||||
type,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// paths
|
||||
export enum FileModelSubPath {
|
||||
Data = "data",
|
||||
Model = "model",
|
||||
Chat = "chat",
|
||||
ModelView = "view",
|
||||
ModelTask = "tasks",
|
||||
ModelCalendar = "calendar",
|
||||
Model = "model",
|
||||
Chat = "chat",
|
||||
ModelView = "view",
|
||||
ModelTask = "tasks",
|
||||
ModelCalendar = "calendar",
|
||||
}
|
||||
|
||||
// types
|
||||
export interface FileContent<T extends string>
|
||||
extends ValidObject, StringEntryObject {
|
||||
readonly fileId: string;
|
||||
readonly fileContentId: string;
|
||||
readonly creationDate: string;
|
||||
readonly fileId: string;
|
||||
readonly fileContentId: string;
|
||||
readonly creationDate: string;
|
||||
|
||||
readonly type: T;
|
||||
}
|
||||
readonly type: T;
|
||||
}
|
||||
|
||||
// references
|
||||
export const FileContentReference: FileContent<string> = {
|
||||
|
||||
@@ -103,7 +103,7 @@ export default class FileTransferModel {
|
||||
};
|
||||
|
||||
readonly handleDecryptedFile = (data: string): void => {
|
||||
const parsed: any = parse(data);
|
||||
const parsed: any = parse(data);
|
||||
const isFileData: boolean = checkMatchesObjectStructure(
|
||||
parsed,
|
||||
FileDataReference,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
|
||||
import { bytesToMB, localeCompare, parseValidObject, stringify } from "../Utility/utility";
|
||||
import {
|
||||
bytesToMB,
|
||||
localeCompare,
|
||||
parseValidObject,
|
||||
stringify,
|
||||
} from "../Utility/utility";
|
||||
import { DATA_VERSION, ValidObject } from "../Utility/typeSafety";
|
||||
|
||||
export const PATH_COMPONENT_SEPARATOR = "\\";
|
||||
@@ -152,36 +156,36 @@ export default class StorageModel {
|
||||
};
|
||||
|
||||
readonly determineCapacity = (): number => {
|
||||
const KEY = "storage-capacity-check";
|
||||
const KEY = "storage-capacity-check";
|
||||
|
||||
let chunk = "0";
|
||||
for (let i = 0; i < 1024*256; i++) {
|
||||
chunk += "0";
|
||||
}
|
||||
let chunk = "0";
|
||||
for (let i = 0; i < 1024 * 256; i++) {
|
||||
chunk += "0";
|
||||
}
|
||||
|
||||
localStorage.setItem(KEY, "");
|
||||
while (true) {
|
||||
try {
|
||||
const current = localStorage.getItem(KEY);
|
||||
console.log(current);
|
||||
localStorage.setItem(KEY, current + chunk);
|
||||
} catch (e) {
|
||||
const capacity = this.calculateUsage();
|
||||
localStorage.removeItem(KEY);
|
||||
return capacity;
|
||||
}
|
||||
}
|
||||
}
|
||||
localStorage.setItem(KEY, "");
|
||||
while (true) {
|
||||
try {
|
||||
const current = localStorage.getItem(KEY);
|
||||
console.log(current);
|
||||
localStorage.setItem(KEY, current + chunk);
|
||||
} catch (e) {
|
||||
const capacity = this.calculateUsage();
|
||||
localStorage.removeItem(KEY);
|
||||
return capacity;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
readonly calculateUsage = (): number => {
|
||||
let data = JSON.stringify(localStorage);
|
||||
const encoder = new TextEncoder();
|
||||
const encoded = encoder.encode(data);
|
||||
const bytes = encoded.byteLength;
|
||||
const mb = bytesToMB(bytes);
|
||||
const rounded = Math.round(mb * 100)/100;
|
||||
return rounded;
|
||||
}
|
||||
let data = JSON.stringify(localStorage);
|
||||
const encoder = new TextEncoder();
|
||||
const encoded = encoder.encode(data);
|
||||
const bytes = encoded.byteLength;
|
||||
const mb = bytesToMB(bytes);
|
||||
const rounded = Math.round(mb * 100) / 100;
|
||||
return rounded;
|
||||
};
|
||||
|
||||
// tree
|
||||
readonly initializeTree = (): void => {
|
||||
@@ -285,7 +289,10 @@ export const filePaths = {
|
||||
...filePaths.chat.chatBase(id),
|
||||
"reactions",
|
||||
],
|
||||
previousFilter: (id: string) => [...filePaths.chat.chatBase(id), "previous-filter"],
|
||||
previousFilter: (id: string) => [
|
||||
...filePaths.chat.chatBase(id),
|
||||
"previous-filter",
|
||||
],
|
||||
lastUsedPage: (id: string) => [
|
||||
...filePaths.chat.chatBase(id),
|
||||
"last-used-page",
|
||||
|
||||
+237
-218
@@ -1,7 +1,7 @@
|
||||
import { checkIsValidObject, checkMatchesObjectStructure } from "./typeSafety";
|
||||
import * as React from "../../react";
|
||||
import {CalendarPage} from "../../View/ChatPages/calendarPage";
|
||||
import CalendarModel from "../Files/calendarModel";
|
||||
import * as React from "../../react";
|
||||
import { CalendarPage } from "../../View/ChatPages/calendarPage";
|
||||
|
||||
export interface Stringifiable {
|
||||
toString(): string;
|
||||
@@ -20,37 +20,48 @@ export function createTimestamp(): string {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
export function isoToDateString (dateISOString: string): string {
|
||||
export function isoToDateString(dateISOString: string): string {
|
||||
const [year, month, date] = dateISOString.split("-");
|
||||
const paddedDate = padZero(date ?? "", 2);
|
||||
return paddedDate;
|
||||
};
|
||||
}
|
||||
|
||||
export function isoToMonthString (dateISOString: string): string {
|
||||
export function isoToMonthString(dateISOString: string): string {
|
||||
const [year, month, _] = dateISOString.split("-");
|
||||
return formatMonthStringFromParts(year, month);
|
||||
};
|
||||
}
|
||||
|
||||
export function formatISO(date: Date): string {
|
||||
return formatISOFromParts(date.getFullYear(), date.getMonth()+1, date.getDate())
|
||||
return formatISOFromParts(
|
||||
date.getFullYear(),
|
||||
date.getMonth() + 1,
|
||||
date.getDate(),
|
||||
);
|
||||
}
|
||||
|
||||
export function formatISOFromParts(year: number|string, month: number|string, date: number|string): string {
|
||||
export function formatISOFromParts(
|
||||
year: number | string,
|
||||
month: number | string,
|
||||
date: number | string,
|
||||
): string {
|
||||
return [year, month, date]
|
||||
.map((x, i) => i == 0 ? x : padZero(x.toString(), 2))
|
||||
.join("-");
|
||||
.map((x, i) => (i == 0 ? x : padZero(x.toString(), 2)))
|
||||
.join("-");
|
||||
}
|
||||
|
||||
export function formatMonthStringFromParts(
|
||||
year: string = "",
|
||||
month: string = "",
|
||||
): string {
|
||||
const paddedYear: string = padZero(year, 4);
|
||||
const paddedMonth: string = padZero(month, 2);
|
||||
return `${paddedYear}-${paddedMonth}`;
|
||||
}
|
||||
|
||||
export function formatMonthStringFromParts (year: string = "", month: string = ""): string {
|
||||
const paddedYear: string = padZero(year, 4);
|
||||
const paddedMonth: string = padZero(month, 2);
|
||||
return `${paddedYear}-${paddedMonth}`;
|
||||
};
|
||||
|
||||
export function formatTime(date: Date): string {
|
||||
return [date.getHours(), date.getMinutes()]
|
||||
.map(x => padZero(x.toString(), 2))
|
||||
.join(":");
|
||||
.map((x) => padZero(x.toString(), 2))
|
||||
.join(":");
|
||||
}
|
||||
|
||||
// filters
|
||||
@@ -64,11 +75,11 @@ export function filterObjectsByStringEntries<T>(
|
||||
const matches: Set<T> = new Set();
|
||||
|
||||
for (const object of objects) {
|
||||
const doesMatch: boolean = checkDoesObjectMatchReference(
|
||||
reference,
|
||||
converter(object),
|
||||
);
|
||||
if (doesMatch) matches.add(object);
|
||||
const doesMatch: boolean = checkDoesObjectMatchReference(
|
||||
reference,
|
||||
converter(object),
|
||||
);
|
||||
if (doesMatch) matches.add(object);
|
||||
}
|
||||
|
||||
return matches;
|
||||
@@ -82,12 +93,12 @@ export function filterObjectsByWords<T>(
|
||||
const matches: Set<T> = new Set();
|
||||
|
||||
for (const object of objects) {
|
||||
const doesMatch: boolean = checkDoesObjectMatchSearch(
|
||||
query,
|
||||
getStringsOfObject,
|
||||
object,
|
||||
);
|
||||
if (doesMatch) matches.add(object);
|
||||
const doesMatch: boolean = checkDoesObjectMatchSearch(
|
||||
query,
|
||||
getStringsOfObject,
|
||||
object,
|
||||
);
|
||||
if (doesMatch) matches.add(object);
|
||||
}
|
||||
|
||||
return matches;
|
||||
@@ -103,50 +114,55 @@ export function checkDoesObjectMatchSearch<T>(
|
||||
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 lowercaseWordsInString = string
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.split(" ")
|
||||
.filter((word) => word != "");
|
||||
wordsInObject.push(...lowercaseWordsInString);
|
||||
}
|
||||
|
||||
const lowercaseWordsInQuery = query
|
||||
.toLowerCase()
|
||||
.split(" ")
|
||||
.filter((word) => word != "");
|
||||
.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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
export default function implementFilter<T>(
|
||||
allItems: React.ListState<T>|React.MapState<T>,
|
||||
allItems: React.ListState<T> | React.MapState<T>,
|
||||
matches: React.ListState<T>,
|
||||
query: React.State<string>,
|
||||
itemToString: (item: T) => string,
|
||||
) {
|
||||
function update() {
|
||||
matches.clear();
|
||||
allItems.value.forEach((item: T) => {
|
||||
if (!itemToString(item).toLowerCase().includes(query.value.toLowerCase())) return;
|
||||
matches.add(item);
|
||||
allItems.handleRemoval(item, () => {
|
||||
matches.remove(item);
|
||||
});
|
||||
});
|
||||
matches.clear();
|
||||
allItems.value.forEach((item: T) => {
|
||||
if (
|
||||
!itemToString(item)
|
||||
.toLowerCase()
|
||||
.includes(query.value.toLowerCase())
|
||||
)
|
||||
return;
|
||||
matches.add(item);
|
||||
allItems.handleRemoval(item, () => {
|
||||
matches.remove(item);
|
||||
});
|
||||
});
|
||||
}
|
||||
query.subscribe(update);
|
||||
allItems.handleAddition(update);
|
||||
@@ -160,16 +176,16 @@ export class HandlerManager<T> {
|
||||
|
||||
// manage
|
||||
setHandler = (id: string, handler: Handler<T>): void => {
|
||||
this.handlers.set(id, handler);
|
||||
this.handlers.set(id, handler);
|
||||
};
|
||||
|
||||
deleteHandler = (id: string): void => {
|
||||
this.handlers.delete(id);
|
||||
this.handlers.delete(id);
|
||||
};
|
||||
|
||||
// trigger
|
||||
trigger = (item: T): void => {
|
||||
[...this.handlers.values()].forEach((handler) => handler(item));
|
||||
[...this.handlers.values()].forEach((handler) => handler(item));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -181,26 +197,26 @@ export class IndexManager<T> {
|
||||
|
||||
// methods
|
||||
update = (items: T[]): void => {
|
||||
this.sortedStrings = [];
|
||||
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);
|
||||
this.sortedStrings = strings.sort(localeCompare);
|
||||
};
|
||||
|
||||
getIndex = (item: T): number => {
|
||||
const string: string = this.itemToString(item);
|
||||
const index: number = this.sortedStrings.indexOf(string);
|
||||
return index;
|
||||
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.itemToString = itemToString;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,7 +229,7 @@ export function getLocalStorageItemAndClear(key: string): string | null {
|
||||
}
|
||||
|
||||
export function bytesToMB(bytes: number): number {
|
||||
return bytes / (1024*1024);
|
||||
return bytes / (1024 * 1024);
|
||||
}
|
||||
|
||||
// string & parsing
|
||||
@@ -227,9 +243,9 @@ export function padZero(string: string | undefined, length: number): string {
|
||||
|
||||
export function parse(string: string): any {
|
||||
try {
|
||||
return JSON.parse(string);
|
||||
return JSON.parse(string);
|
||||
} catch {
|
||||
return {};
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,8 +254,8 @@ export function parseValidObject<T>(string: string, reference: T): T | null {
|
||||
if (checkIsValidObject(parsed) == false) return null;
|
||||
|
||||
const doesMatchReference: boolean = checkMatchesObjectStructure(
|
||||
parsed,
|
||||
reference,
|
||||
parsed,
|
||||
reference,
|
||||
);
|
||||
if (doesMatchReference == false) return null;
|
||||
|
||||
@@ -248,9 +264,9 @@ export function parseValidObject<T>(string: string, reference: T): T | null {
|
||||
|
||||
export function parseOrFallback(inputString: string): any {
|
||||
try {
|
||||
return JSON.parse(inputString);
|
||||
return JSON.parse(inputString);
|
||||
} catch {
|
||||
return inputString;
|
||||
return inputString;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,27 +287,30 @@ export interface PinchToZoomData {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
export function implementPinchZoom(canvas: HTMLElement, data: React.State<PinchToZoomData>) {
|
||||
export function implementPinchZoom(
|
||||
canvas: HTMLElement,
|
||||
data: React.State<PinchToZoomData>,
|
||||
) {
|
||||
let pinching = false;
|
||||
let dragging = false;
|
||||
let lastElement: HTMLElement | undefined = undefined;
|
||||
|
||||
let initialDistance: number,
|
||||
initialZoom: number,
|
||||
initialX: number,
|
||||
initialY: number,
|
||||
initialTouchX: number,
|
||||
initialTouchY: number;
|
||||
initialZoom: number,
|
||||
initialX: number,
|
||||
initialY: number,
|
||||
initialTouchX: number,
|
||||
initialTouchY: number;
|
||||
|
||||
function reset() {
|
||||
initialDistance = 0;
|
||||
initialDistance = 0;
|
||||
|
||||
initialZoom = 1;
|
||||
initialZoom = 1;
|
||||
|
||||
initialX = 0;
|
||||
initialY = 0;
|
||||
initialTouchX = 0;
|
||||
initialTouchY = 0;
|
||||
initialX = 0;
|
||||
initialY = 0;
|
||||
initialTouchX = 0;
|
||||
initialTouchY = 0;
|
||||
}
|
||||
|
||||
reset();
|
||||
@@ -301,117 +320,117 @@ export function implementPinchZoom(canvas: HTMLElement, data: React.State<PinchT
|
||||
|
||||
const element = (): HTMLElement | null => canvas.querySelector(".zoom");
|
||||
const distance = (e: TouchEvent) =>
|
||||
Math.hypot(
|
||||
e.touches[0].pageX - e.touches[1].pageX,
|
||||
e.touches[0].pageY - e.touches[1].pageY,
|
||||
);
|
||||
Math.hypot(
|
||||
e.touches[0].pageX - e.touches[1].pageX,
|
||||
e.touches[0].pageY - e.touches[1].pageY,
|
||||
);
|
||||
const point = (e: TouchEvent, direction: "x" | "y", i: number) =>
|
||||
e.touches[i][direction == "x" ? "clientX" : "clientY"];
|
||||
e.touches[i][direction == "x" ? "clientX" : "clientY"];
|
||||
const midpoint = (e: TouchEvent, direction: "x" | "y") =>
|
||||
(point(e, direction, 0) + point(e, direction, 1)) / 2;
|
||||
(point(e, direction, 0) + point(e, direction, 1)) / 2;
|
||||
|
||||
function apply(zoom: number, _offset?: [number, number]) {
|
||||
if (zoom < MIN) return apply(MIN, _offset);
|
||||
if (zoom > MAX) return apply(5, _offset);
|
||||
if (zoom < MIN) return apply(MIN, _offset);
|
||||
if (zoom > MAX) return apply(5, _offset);
|
||||
|
||||
let {x, y} = data.value;
|
||||
if (_offset) [x, y] = _offset;
|
||||
let { x, y } = data.value;
|
||||
if (_offset) [x, y] = _offset;
|
||||
|
||||
data.value = {
|
||||
zoom, x, y
|
||||
}
|
||||
data.value = {
|
||||
zoom,
|
||||
x,
|
||||
y,
|
||||
};
|
||||
}
|
||||
|
||||
data.subscribe((data) => {
|
||||
const el = element();
|
||||
if (!el) return;
|
||||
el.style.transform = `scale(${data.zoom.toString()}) translate(${data.x}px, ${data.y}px)`;
|
||||
})
|
||||
const el = element();
|
||||
if (!el) return;
|
||||
el.style.transform = `scale(${data.zoom.toString()}) translate(${data.x}px, ${data.y}px)`;
|
||||
});
|
||||
|
||||
canvas.addEventListener("wheel", (event: WheelEvent) => {
|
||||
event.preventDefault();
|
||||
event.preventDefault();
|
||||
|
||||
initialZoom = data.value.zoom;
|
||||
initialX = data.value.x;
|
||||
initialY = data.value.y;
|
||||
initialZoom = data.value.zoom;
|
||||
initialX = data.value.x;
|
||||
initialY = data.value.y;
|
||||
|
||||
apply(data.value.zoom - event.deltaY * 0.005);
|
||||
apply(data.value.zoom - event.deltaY * 0.005);
|
||||
});
|
||||
canvas.addEventListener("scroll", (event: Event) => {
|
||||
event.preventDefault();
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
canvas.addEventListener("mousedown", (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement|undefined;
|
||||
console.log(target);
|
||||
if (!target || !target.classList.contains("allow-drag-move")) return;
|
||||
event.preventDefault();
|
||||
initialZoom = data.value.zoom;
|
||||
initialX = data.value.x;
|
||||
initialY = data.value.y;
|
||||
initialTouchX = event.clientX;
|
||||
initialTouchY = event.clientY;
|
||||
dragging = true;
|
||||
})
|
||||
const target = event.target as HTMLElement | undefined;
|
||||
console.log(target);
|
||||
if (!target || !target.classList.contains("allow-drag-move")) return;
|
||||
event.preventDefault();
|
||||
initialZoom = data.value.zoom;
|
||||
initialX = data.value.x;
|
||||
initialY = data.value.y;
|
||||
initialTouchX = event.clientX;
|
||||
initialTouchY = event.clientY;
|
||||
dragging = true;
|
||||
});
|
||||
canvas.addEventListener("mouseup", () => {
|
||||
dragging = false;
|
||||
})
|
||||
dragging = false;
|
||||
});
|
||||
canvas.addEventListener("mousemove", (event: MouseEvent) => {
|
||||
if (!dragging) return;
|
||||
event.preventDefault();
|
||||
apply(data.value.zoom, [
|
||||
initialX +
|
||||
(event.clientX - initialTouchX) / initialZoom,
|
||||
initialY +
|
||||
(event.clientY - initialTouchY) / initialZoom,
|
||||
]);
|
||||
})
|
||||
if (!dragging) return;
|
||||
event.preventDefault();
|
||||
apply(data.value.zoom, [
|
||||
initialX + (event.clientX - initialTouchX) / initialZoom,
|
||||
initialY + (event.clientY - initialTouchY) / initialZoom,
|
||||
]);
|
||||
});
|
||||
|
||||
canvas.addEventListener("touchstart", (event: TouchEvent) => {
|
||||
(document.activeElement as HTMLElement)?.blur();
|
||||
(document.activeElement as HTMLElement)?.blur();
|
||||
|
||||
const el = element();
|
||||
if (!el) return;
|
||||
if (lastElement != undefined && lastElement != el) reset();
|
||||
lastElement = el;
|
||||
const el = element();
|
||||
if (!el) return;
|
||||
if (lastElement != undefined && lastElement != el) reset();
|
||||
lastElement = el;
|
||||
|
||||
initialZoom = data.value.zoom;
|
||||
initialX = data.value.x;
|
||||
initialY = data.value.y;
|
||||
initialZoom = data.value.zoom;
|
||||
initialX = data.value.x;
|
||||
initialY = data.value.y;
|
||||
|
||||
if (event.touches.length != 2) {
|
||||
pinching = false;
|
||||
initialTouchX = event.touches[0].clientX;
|
||||
initialTouchY = event.touches[0].clientY;
|
||||
return;
|
||||
}
|
||||
if (event.touches.length != 2) {
|
||||
pinching = false;
|
||||
initialTouchX = event.touches[0].clientX;
|
||||
initialTouchY = event.touches[0].clientY;
|
||||
return;
|
||||
}
|
||||
|
||||
pinching = true;
|
||||
initialDistance = distance(event);
|
||||
initialTouchX = midpoint(event, "x");
|
||||
initialTouchY = midpoint(event, "y");
|
||||
pinching = true;
|
||||
initialDistance = distance(event);
|
||||
initialTouchX = midpoint(event, "x");
|
||||
initialTouchY = midpoint(event, "y");
|
||||
});
|
||||
canvas.addEventListener("touchmove", (event: TouchEvent) => {
|
||||
if (!pinching) {
|
||||
apply(data.value.zoom, [
|
||||
initialX +
|
||||
(event.touches[0].clientX - initialTouchX) / initialZoom,
|
||||
initialY +
|
||||
(event.touches[0].clientY - initialTouchY) / initialZoom,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
if (event.touches.length < 2) return;
|
||||
event.preventDefault();
|
||||
const currentDistance = distance(event);
|
||||
const midX = midpoint(event, "x");
|
||||
const midY = midpoint(event, "y");
|
||||
const ratio = currentDistance / initialDistance;
|
||||
const difference = currentDistance - initialDistance;
|
||||
apply(initialZoom * ratio, [
|
||||
initialX + (midX - difference - initialTouchX) / initialZoom / 2,
|
||||
initialY + (midY - difference - initialTouchY) / initialZoom / 2,
|
||||
]);
|
||||
if (!pinching) {
|
||||
apply(data.value.zoom, [
|
||||
initialX +
|
||||
(event.touches[0].clientX - initialTouchX) / initialZoom,
|
||||
initialY +
|
||||
(event.touches[0].clientY - initialTouchY) / initialZoom,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
if (event.touches.length < 2) return;
|
||||
event.preventDefault();
|
||||
const currentDistance = distance(event);
|
||||
const midX = midpoint(event, "x");
|
||||
const midY = midpoint(event, "y");
|
||||
const ratio = currentDistance / initialDistance;
|
||||
const difference = currentDistance - initialDistance;
|
||||
apply(initialZoom * ratio, [
|
||||
initialX + (midX - difference - initialTouchX) / initialZoom / 2,
|
||||
initialY + (midY - difference - initialTouchY) / initialZoom / 2,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -424,12 +443,12 @@ export function collectObjectValuesForKey<T>(
|
||||
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) continue;
|
||||
const stringEntryObject: StringEntryObject = converter(object);
|
||||
const stringEntryObjectValue: Stringifiable | undefined =
|
||||
stringEntryObject[key];
|
||||
if (stringEntryObjectValue == undefined) continue;
|
||||
|
||||
values.add(stringEntryObjectValue.toString());
|
||||
values.add(stringEntryObjectValue.toString());
|
||||
}
|
||||
|
||||
return [...values.values()];
|
||||
@@ -442,50 +461,50 @@ export function checkDoesObjectMatchReference(
|
||||
explicitEmptyValue: boolean = false,
|
||||
): boolean {
|
||||
reference_entry_loop: for (const referenceEntry of Object.entries(
|
||||
reference,
|
||||
reference,
|
||||
)) {
|
||||
const [referenceKey, referenceValue] = referenceEntry;
|
||||
const stringEntryObjectValue: Stringifiable | undefined =
|
||||
stringEntryObject[referenceKey];
|
||||
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 {
|
||||
if (explicitEmptyValue == false) {
|
||||
// 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 {
|
||||
if (explicitEmptyValue == false) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user