format
This commit is contained in:
+283
-274
@@ -37,433 +37,442 @@ export default class ChatModel {
|
|||||||
info: ChatInfoFileContent; /* load function called in constructor */
|
info: ChatInfoFileContent; /* load function called in constructor */
|
||||||
|
|
||||||
get secondaryChannels(): string[] {
|
get secondaryChannels(): string[] {
|
||||||
return this.info.secondaryChannels.sort(localeCompare);
|
return this.info.secondaryChannels.sort(localeCompare);
|
||||||
}
|
}
|
||||||
|
|
||||||
get color(): Colors {
|
get color(): Colors {
|
||||||
const color = this.info.color as Colors;
|
const color = this.info.color as Colors;
|
||||||
if ([...Object.values(Colors)].includes(color)) return color;
|
if ([...Object.values(Colors)].includes(color)) return color;
|
||||||
return Colors.Standard;
|
return Colors.Standard;
|
||||||
}
|
}
|
||||||
|
|
||||||
// handler managers
|
// handler managers
|
||||||
readonly chatMessageHandlerManager: HandlerManager<ChatMessage> =
|
readonly chatMessageHandlerManager: HandlerManager<ChatMessage> =
|
||||||
new HandlerManager();
|
new HandlerManager();
|
||||||
readonly reactionHandlerManager: HandlerManager<ChatMessageReaction> =
|
readonly reactionHandlerManager: HandlerManager<ChatMessageReaction> =
|
||||||
new HandlerManager();
|
new HandlerManager();
|
||||||
readonly changeHandlerManager: HandlerManager<null> = new HandlerManager();
|
readonly changeHandlerManager: HandlerManager<null> = new HandlerManager();
|
||||||
|
|
||||||
// paths
|
// paths
|
||||||
readonly getBasePath = (): string[] => {
|
readonly getBasePath = (): string[] => {
|
||||||
return StorageModel.getPath(
|
return StorageModel.getPath(
|
||||||
StorageModelSubPaths.Chat,
|
StorageModelSubPaths.Chat,
|
||||||
filePaths.chat.chatBase(this.id),
|
filePaths.chat.chatBase(this.id),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
readonly getInfoPath = (): string[] => {
|
readonly getInfoPath = (): string[] => {
|
||||||
return StorageModel.getPath(
|
return StorageModel.getPath(
|
||||||
StorageModelSubPaths.Chat,
|
StorageModelSubPaths.Chat,
|
||||||
filePaths.chat.info(this.id),
|
filePaths.chat.info(this.id),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
readonly getColorPath = (): string[] => {
|
readonly getColorPath = (): string[] => {
|
||||||
return StorageModel.getPath(
|
return StorageModel.getPath(
|
||||||
StorageModelSubPaths.Chat,
|
StorageModelSubPaths.Chat,
|
||||||
filePaths.chat.color(this.id),
|
filePaths.chat.color(this.id),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
readonly getMessageDirPath = (): string[] => {
|
readonly getMessageDirPath = (): string[] => {
|
||||||
return StorageModel.getPath(
|
return StorageModel.getPath(
|
||||||
StorageModelSubPaths.Chat,
|
StorageModelSubPaths.Chat,
|
||||||
filePaths.chat.messages(this.id),
|
filePaths.chat.messages(this.id),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
readonly getMessagePath = (id: string): string[] => {
|
readonly getMessagePath = (id: string): string[] => {
|
||||||
return [...this.getMessageDirPath(), id];
|
return [...this.getMessageDirPath(), id];
|
||||||
};
|
};
|
||||||
|
|
||||||
readonly getReactionDirPath = (): string[] => {
|
readonly getReactionDirPath = (): string[] => {
|
||||||
return StorageModel.getPath(
|
return StorageModel.getPath(
|
||||||
StorageModelSubPaths.Chat,
|
StorageModelSubPaths.Chat,
|
||||||
filePaths.chat.reactions(this.id),
|
filePaths.chat.reactions(this.id),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
readonly getReactionPath = (id: string): string[] => {
|
readonly getReactionPath = (id: string): string[] => {
|
||||||
return [...this.getReactionDirPath(), id];
|
return [...this.getReactionDirPath(), id];
|
||||||
};
|
};
|
||||||
|
|
||||||
readonly getPreviousFilterPath = (): string[] => {
|
readonly getPreviousFilterPath = (): string[] => {
|
||||||
return [StorageModelSubPaths.Chat, ...filePaths.chat.previousFilter(this.id)];
|
return [
|
||||||
}
|
StorageModelSubPaths.Chat,
|
||||||
|
...filePaths.chat.previousFilter(this.id),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
// handlers
|
// handlers
|
||||||
readonly handleMessage = (body: string): void => {
|
readonly handleMessage = (body: string): void => {
|
||||||
const chatMessage: ChatMessage | null = parseValidObject(
|
const chatMessage: ChatMessage | null = parseValidObject(
|
||||||
body,
|
body,
|
||||||
ChatMessageReference,
|
ChatMessageReference,
|
||||||
);
|
);
|
||||||
if (chatMessage == null) return;
|
if (chatMessage == null) return;
|
||||||
|
|
||||||
chatMessage.status = ChatMessageStatuses.Received;
|
chatMessage.status = ChatMessageStatuses.Received;
|
||||||
|
|
||||||
this.addMessage(chatMessage);
|
this.addMessage(chatMessage);
|
||||||
|
|
||||||
if (chatMessage.stringifiedFile) return;
|
if (chatMessage.stringifiedFile) return;
|
||||||
this.setReadStatus(true);
|
this.setReadStatus(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
readonly handleReaction = (reaction: ChatMessageReaction | any): void => {
|
readonly handleReaction = (reaction: ChatMessageReaction | any): void => {
|
||||||
if (
|
if (
|
||||||
!checkMatchesObjectStructure(reaction, ChatMessageReactionReference)
|
!checkMatchesObjectStructure(reaction, ChatMessageReactionReference)
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
const reactionPath: string[] = this.getReactionPath(reaction.fileId);
|
const reactionPath: string[] = this.getReactionPath(reaction.fileId);
|
||||||
|
|
||||||
if (reaction.isDeleting == true) {
|
if (reaction.isDeleting == true) {
|
||||||
this.storageModel.remove(reactionPath);
|
this.storageModel.remove(reactionPath);
|
||||||
} else {
|
} else {
|
||||||
this.storageModel.writeStringifiable(reactionPath, reaction);
|
this.storageModel.writeStringifiable(reactionPath, reaction);
|
||||||
}
|
}
|
||||||
this.reactionHandlerManager.trigger(reaction);
|
this.reactionHandlerManager.trigger(reaction);
|
||||||
};
|
};
|
||||||
|
|
||||||
readonly handleMessageSent = (chatMessage: ChatMessage): void => {
|
readonly handleMessageSent = (chatMessage: ChatMessage): void => {
|
||||||
chatMessage.status = ChatMessageStatuses.Sent;
|
chatMessage.status = ChatMessageStatuses.Sent;
|
||||||
this.addMessage(chatMessage);
|
this.addMessage(chatMessage);
|
||||||
};
|
};
|
||||||
|
|
||||||
// settings
|
// settings
|
||||||
readonly setName = (name: string): void => {
|
readonly setName = (name: string): void => {
|
||||||
this.info.name = name;
|
this.info.name = name;
|
||||||
this.storeInfo();
|
this.storeInfo();
|
||||||
this.syncInfo();
|
this.syncInfo();
|
||||||
this.subscribe();
|
this.subscribe();
|
||||||
};
|
};
|
||||||
|
|
||||||
readonly setSecondaryChannels = (secondaryChannels: string[]): void => {
|
readonly setSecondaryChannels = (secondaryChannels: string[]): void => {
|
||||||
this.info.secondaryChannels = secondaryChannels;
|
this.info.secondaryChannels = secondaryChannels;
|
||||||
this.storeInfo();
|
this.storeInfo();
|
||||||
};
|
};
|
||||||
|
|
||||||
readonly setEncryptionKey = (key: string): void => {
|
readonly setEncryptionKey = (key: string): void => {
|
||||||
this.info.encryptionKey = key;
|
this.info.encryptionKey = key;
|
||||||
this.storeInfo();
|
this.storeInfo();
|
||||||
};
|
};
|
||||||
|
|
||||||
readonly setColor = (color: Colors): void => {
|
readonly setColor = (color: Colors): void => {
|
||||||
this.info.color = color;
|
this.info.color = color;
|
||||||
this.syncInfo();
|
this.syncInfo();
|
||||||
this.storeInfo();
|
this.storeInfo();
|
||||||
};
|
};
|
||||||
|
|
||||||
// messaging
|
// messaging
|
||||||
readonly addMessage = async (chatMessage: ChatMessage): Promise<void> => {
|
readonly addMessage = async (chatMessage: ChatMessage): Promise<void> => {
|
||||||
await this.decryptMessage(chatMessage);
|
await this.decryptMessage(chatMessage);
|
||||||
|
|
||||||
// message
|
// message
|
||||||
if (chatMessage.body != "") {
|
if (chatMessage.body != "") {
|
||||||
const messagePath: string[] = this.getMessagePath(chatMessage.id);
|
const messagePath: string[] = this.getMessagePath(chatMessage.id);
|
||||||
this.storageModel.writeStringifiable(messagePath, chatMessage);
|
this.storageModel.writeStringifiable(messagePath, chatMessage);
|
||||||
this.chatMessageHandlerManager.trigger(chatMessage);
|
this.chatMessageHandlerManager.trigger(chatMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
// file
|
// file
|
||||||
this.fileModel.handleStringifiedFileContent(
|
this.fileModel.handleStringifiedFileContent(
|
||||||
chatMessage.stringifiedFile,
|
chatMessage.stringifiedFile,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
readonly getNameAndChannel = (): [string, string] | false => {
|
readonly getNameAndChannel = (): [string, string] | false => {
|
||||||
const senderName: string = this.settingsModel.username;
|
const senderName: string = this.settingsModel.username;
|
||||||
if (senderName == "") return false;
|
if (senderName == "") return false;
|
||||||
|
|
||||||
const allChannels: string[] = [this.id];
|
const allChannels: string[] = [this.id];
|
||||||
for (const secondaryChannel of this.info.secondaryChannels) {
|
for (const secondaryChannel of this.info.secondaryChannels) {
|
||||||
allChannels.push(secondaryChannel);
|
allChannels.push(secondaryChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
const combinedChannel: string = allChannels.join("/");
|
const combinedChannel: string = allChannels.join("/");
|
||||||
|
|
||||||
return [senderName, combinedChannel];
|
return [senderName, combinedChannel];
|
||||||
};
|
};
|
||||||
|
|
||||||
readonly sendMessage = async (
|
readonly sendMessage = async (
|
||||||
body: string,
|
body: string,
|
||||||
inlineReplyId?: string,
|
inlineReplyId?: string,
|
||||||
fileContent?: FileContent<string>,
|
fileContent?: FileContent<string>,
|
||||||
): Promise<string|false> => {
|
): Promise<string | false> => {
|
||||||
const nameAndChannel = this.getNameAndChannel();
|
const nameAndChannel = this.getNameAndChannel();
|
||||||
if (nameAndChannel == false) return false;
|
if (nameAndChannel == false) return false;
|
||||||
const [senderName, combinedChannel] = nameAndChannel;
|
const [senderName, combinedChannel] = nameAndChannel;
|
||||||
|
|
||||||
const chatMessage: ChatMessage = await ChatModel.createChatMessage(
|
const chatMessage: ChatMessage = await ChatModel.createChatMessage(
|
||||||
combinedChannel,
|
combinedChannel,
|
||||||
senderName,
|
senderName,
|
||||||
this.info.encryptionKey,
|
this.info.encryptionKey,
|
||||||
body,
|
body,
|
||||||
inlineReplyId,
|
inlineReplyId,
|
||||||
fileContent,
|
fileContent,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.addMessage(chatMessage);
|
this.addMessage(chatMessage);
|
||||||
this.connectionModel.sendMessageOrStore(chatMessage);
|
this.connectionModel.sendMessageOrStore(chatMessage);
|
||||||
return chatMessage.id;
|
return chatMessage.id;
|
||||||
};
|
};
|
||||||
|
|
||||||
readonly decryptMessage = async (
|
readonly decryptMessage = async (
|
||||||
chatMessage: ChatMessage,
|
chatMessage: ChatMessage,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const decryptedBody: string = await decryptString(
|
const decryptedBody: string = await decryptString(
|
||||||
chatMessage.body,
|
chatMessage.body,
|
||||||
this.info.encryptionKey,
|
this.info.encryptionKey,
|
||||||
);
|
);
|
||||||
const decryptedFile: string = await decryptString(
|
const decryptedFile: string = await decryptString(
|
||||||
chatMessage.stringifiedFile ?? "",
|
chatMessage.stringifiedFile ?? "",
|
||||||
this.info.encryptionKey,
|
this.info.encryptionKey,
|
||||||
);
|
);
|
||||||
chatMessage.body = decryptedBody;
|
chatMessage.body = decryptedBody;
|
||||||
chatMessage.stringifiedFile = decryptedFile;
|
chatMessage.stringifiedFile = decryptedFile;
|
||||||
};
|
};
|
||||||
|
|
||||||
readonly sendReaction = async (
|
readonly sendReaction = async (
|
||||||
messageId: string,
|
messageId: string,
|
||||||
content: ReactionSymbols,
|
content: ReactionSymbols,
|
||||||
isDeleting: boolean = false,
|
isDeleting: boolean = false,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const nameAndChannel = this.getNameAndChannel();
|
const nameAndChannel = this.getNameAndChannel();
|
||||||
if (nameAndChannel == false) return;
|
if (nameAndChannel == false) return;
|
||||||
const [senderName] = nameAndChannel;
|
const [senderName] = nameAndChannel;
|
||||||
|
|
||||||
const reaction = ChatModel.createMessageReaction(
|
const reaction = ChatModel.createMessageReaction(
|
||||||
messageId,
|
messageId,
|
||||||
senderName,
|
senderName,
|
||||||
content,
|
content,
|
||||||
isDeleting,
|
isDeleting,
|
||||||
);
|
);
|
||||||
this.sendMessage("", undefined, reaction);
|
this.sendMessage("", undefined, reaction);
|
||||||
this.handleReaction(reaction);
|
this.handleReaction(reaction);
|
||||||
};
|
};
|
||||||
|
|
||||||
readonly subscribe = (): void => {
|
readonly subscribe = (): void => {
|
||||||
this.connectionModel.addChannel(this.id);
|
this.connectionModel.addChannel(this.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
readonly setReadStatus = (hasUnreadMessages: boolean): void => {
|
readonly setReadStatus = (hasUnreadMessages: boolean): void => {
|
||||||
this.info.hasUnreadMessages = hasUnreadMessages;
|
this.info.hasUnreadMessages = hasUnreadMessages;
|
||||||
this.storeInfo();
|
this.storeInfo();
|
||||||
};
|
};
|
||||||
|
|
||||||
// storage
|
// storage
|
||||||
readonly storeInfo = (): void => {
|
readonly storeInfo = (): void => {
|
||||||
this.storageModel.writeStringifiable(this.getInfoPath(), this.info);
|
this.storageModel.writeStringifiable(this.getInfoPath(), this.info);
|
||||||
};
|
};
|
||||||
|
|
||||||
readonly syncInfo = (): void => {
|
readonly syncInfo = (): void => {
|
||||||
this.sendMessage("", undefined, this.info);
|
this.sendMessage("", undefined, this.info);
|
||||||
}
|
};
|
||||||
|
|
||||||
readonly storeFilter = (filter: string): void => {
|
readonly storeFilter = (filter: string): void => {
|
||||||
this.storageModel.write(this.getPreviousFilterPath(), filter);
|
this.storageModel.write(this.getPreviousFilterPath(), filter);
|
||||||
}
|
};
|
||||||
|
|
||||||
readonly getFilter = (): string=> {
|
readonly getFilter = (): string => {
|
||||||
return this.storageModel.read(this.getPreviousFilterPath()) || "";
|
return this.storageModel.read(this.getPreviousFilterPath()) || "";
|
||||||
}
|
};
|
||||||
|
|
||||||
readonly delete = () => {
|
readonly delete = () => {
|
||||||
// untrack
|
// untrack
|
||||||
this.chatListModel.untrackChat(this);
|
this.chatListModel.untrackChat(this);
|
||||||
|
|
||||||
// delete
|
// delete
|
||||||
const dirPath: string[] = this.getBasePath();
|
const dirPath: string[] = this.getBasePath();
|
||||||
this.storageModel.removeRecursively(dirPath);
|
this.storageModel.removeRecursively(dirPath);
|
||||||
};
|
};
|
||||||
|
|
||||||
// load
|
// load
|
||||||
readonly loadInfo = (): void => {
|
readonly loadInfo = (): void => {
|
||||||
let info: ChatInfoFileContent | null = this.storageModel.readStringifiable(
|
let info: ChatInfoFileContent | null =
|
||||||
this.getInfoPath(),
|
this.storageModel.readStringifiable(
|
||||||
ChatInfoReference,
|
this.getInfoPath(),
|
||||||
);
|
ChatInfoReference,
|
||||||
this.info = (info || ChatModel.generateChatInfo("0", "0", Colors.Standard));
|
);
|
||||||
this.storeInfo();
|
this.info =
|
||||||
|
info || ChatModel.generateChatInfo("0", "0", Colors.Standard);
|
||||||
|
this.storeInfo();
|
||||||
};
|
};
|
||||||
|
|
||||||
readonly handleInfo = (info: ChatInfoFileContent): void => {
|
readonly handleInfo = (info: ChatInfoFileContent): void => {
|
||||||
this.info = info;
|
this.info = info;
|
||||||
this.changeHandlerManager.trigger(null);
|
this.changeHandlerManager.trigger(null);
|
||||||
this.storeInfo();
|
this.storeInfo();
|
||||||
}
|
};
|
||||||
|
|
||||||
get messages(): ChatMessage[] {
|
get messages(): ChatMessage[] {
|
||||||
const messageIds: string[] = this.storageModel.list(
|
const messageIds: string[] = this.storageModel.list(
|
||||||
this.getMessageDirPath(),
|
this.getMessageDirPath(),
|
||||||
);
|
);
|
||||||
if (!Array.isArray(messageIds)) return [];
|
if (!Array.isArray(messageIds)) return [];
|
||||||
|
|
||||||
const chatMessages: ChatMessage[] = [];
|
const chatMessages: ChatMessage[] = [];
|
||||||
for (const messageId of messageIds) {
|
for (const messageId of messageIds) {
|
||||||
const messagePath: string[] = this.getMessagePath(messageId);
|
const messagePath: string[] = this.getMessagePath(messageId);
|
||||||
const chatMessage: ChatMessage | null =
|
const chatMessage: ChatMessage | null =
|
||||||
this.storageModel.readStringifiable(
|
this.storageModel.readStringifiable(
|
||||||
messagePath,
|
messagePath,
|
||||||
ChatMessageReference,
|
ChatMessageReference,
|
||||||
);
|
);
|
||||||
if (chatMessage == null) continue;
|
if (chatMessage == null) continue;
|
||||||
chatMessages.push(chatMessage);
|
chatMessages.push(chatMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sorted = chatMessages.sort((a, b) =>
|
const sorted = chatMessages.sort((a, b) =>
|
||||||
a.dateSent.localeCompare(b.dateSent),
|
a.dateSent.localeCompare(b.dateSent),
|
||||||
);
|
);
|
||||||
return sorted;
|
return sorted;
|
||||||
}
|
}
|
||||||
|
|
||||||
get reactions(): ChatMessageReaction[] {
|
get reactions(): ChatMessageReaction[] {
|
||||||
const reactionIds: string[] = this.storageModel.list(
|
const reactionIds: string[] = this.storageModel.list(
|
||||||
this.getReactionDirPath(),
|
this.getReactionDirPath(),
|
||||||
);
|
);
|
||||||
if (!Array.isArray(reactionIds)) return [];
|
if (!Array.isArray(reactionIds)) return [];
|
||||||
|
|
||||||
const reactions: ChatMessageReaction[] = [];
|
const reactions: ChatMessageReaction[] = [];
|
||||||
for (const reactionId of reactionIds) {
|
for (const reactionId of reactionIds) {
|
||||||
const reactionPath: string[] = this.getReactionPath(reactionId);
|
const reactionPath: string[] = this.getReactionPath(reactionId);
|
||||||
const reaction: ChatMessageReaction | null =
|
const reaction: ChatMessageReaction | null =
|
||||||
this.storageModel.readStringifiable(
|
this.storageModel.readStringifiable(
|
||||||
reactionPath,
|
reactionPath,
|
||||||
ChatMessageReactionReference,
|
ChatMessageReactionReference,
|
||||||
);
|
);
|
||||||
if (reaction == null) continue;
|
if (reaction == null) continue;
|
||||||
reactions.push(reaction);
|
reactions.push(reaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
return reactions;
|
return reactions;
|
||||||
}
|
}
|
||||||
|
|
||||||
// init
|
// init
|
||||||
constructor(
|
constructor(
|
||||||
storageModel: StorageModel,
|
storageModel: StorageModel,
|
||||||
connectionModel: ConnectionModel,
|
connectionModel: ConnectionModel,
|
||||||
settingsModel: SettingsModel,
|
settingsModel: SettingsModel,
|
||||||
chatListModel: ChatListModel,
|
chatListModel: ChatListModel,
|
||||||
chatId: string,
|
chatId: string,
|
||||||
) {
|
) {
|
||||||
this.id = chatId;
|
this.id = chatId;
|
||||||
this.connectionModel = connectionModel;
|
this.connectionModel = connectionModel;
|
||||||
this.settingsModel = settingsModel;
|
this.settingsModel = settingsModel;
|
||||||
this.storageModel = storageModel;
|
this.storageModel = storageModel;
|
||||||
this.chatListModel = chatListModel;
|
this.chatListModel = chatListModel;
|
||||||
|
|
||||||
this.loadInfo();
|
this.loadInfo();
|
||||||
this.subscribe();
|
this.subscribe();
|
||||||
|
|
||||||
this.fileModel = new FileModel(
|
this.fileModel = new FileModel(
|
||||||
this.storageModel,
|
this.storageModel,
|
||||||
this.settingsModel,
|
this.settingsModel,
|
||||||
this,
|
this,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// utility
|
// utility
|
||||||
static splitChannel(channelString: string): string[] {
|
static splitChannel(channelString: string): string[] {
|
||||||
return channelString.split("/");
|
return channelString.split("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
static generateChatInfo = (name: string, id: string, color: Colors): ChatInfoFileContent => {
|
static generateChatInfo = (
|
||||||
const file = FileModel.createFileContent(id, "chat-info");
|
name: string,
|
||||||
|
id: string,
|
||||||
|
color: Colors,
|
||||||
|
): ChatInfoFileContent => {
|
||||||
|
const file = FileModel.createFileContent(id, "chat-info");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...file,
|
...file,
|
||||||
|
|
||||||
name,
|
name,
|
||||||
secondaryChannels: [],
|
secondaryChannels: [],
|
||||||
encryptionKey: "",
|
encryptionKey: "",
|
||||||
color,
|
color,
|
||||||
hasUnreadMessages: false,
|
hasUnreadMessages: false,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
static createChatMessage = async (
|
static createChatMessage = async (
|
||||||
channel: string,
|
channel: string,
|
||||||
sender: string,
|
sender: string,
|
||||||
encryptionKey: string,
|
encryptionKey: string,
|
||||||
body: string,
|
body: string,
|
||||||
inlineReplyId?: string,
|
inlineReplyId?: string,
|
||||||
fileContent?: FileContent<string>,
|
fileContent?: FileContent<string>,
|
||||||
): Promise<ChatMessage> => {
|
): Promise<ChatMessage> => {
|
||||||
const chatMessage: ChatMessage = {
|
const chatMessage: ChatMessage = {
|
||||||
dataVersion: DATA_VERSION,
|
dataVersion: DATA_VERSION,
|
||||||
|
|
||||||
id: v4(),
|
id: v4(),
|
||||||
|
|
||||||
channel,
|
channel,
|
||||||
sender,
|
sender,
|
||||||
body,
|
body,
|
||||||
dateSent: createTimestamp(),
|
dateSent: createTimestamp(),
|
||||||
inlineReplyId,
|
inlineReplyId,
|
||||||
|
|
||||||
status: ChatMessageStatuses.Outbox,
|
status: ChatMessageStatuses.Outbox,
|
||||||
stringifiedFile: "",
|
stringifiedFile: "",
|
||||||
};
|
};
|
||||||
if (fileContent != undefined) {
|
if (fileContent != undefined) {
|
||||||
const stringifiedFile: string = stringify(fileContent);
|
const stringifiedFile: string = stringify(fileContent);
|
||||||
chatMessage.stringifiedFile = stringifiedFile;
|
chatMessage.stringifiedFile = stringifiedFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (encryptionKey != "") {
|
if (encryptionKey != "") {
|
||||||
chatMessage.body = await encryptString(
|
chatMessage.body = await encryptString(
|
||||||
chatMessage.body,
|
chatMessage.body,
|
||||||
encryptionKey,
|
encryptionKey,
|
||||||
);
|
);
|
||||||
chatMessage.stringifiedFile = await encryptString(
|
chatMessage.stringifiedFile = await encryptString(
|
||||||
chatMessage.stringifiedFile,
|
chatMessage.stringifiedFile,
|
||||||
encryptionKey,
|
encryptionKey,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return chatMessage;
|
return chatMessage;
|
||||||
};
|
};
|
||||||
|
|
||||||
static createMessageReaction = (
|
static createMessageReaction = (
|
||||||
messageId: string,
|
messageId: string,
|
||||||
sender: string,
|
sender: string,
|
||||||
content: ReactionSymbols,
|
content: ReactionSymbols,
|
||||||
isDeleting: boolean,
|
isDeleting: boolean,
|
||||||
): ChatMessageReaction => {
|
): ChatMessageReaction => {
|
||||||
const fileContent: FileContent<"reaction"> =
|
const fileContent: FileContent<"reaction"> =
|
||||||
FileModel.createFileContent(v4(), "reaction");
|
FileModel.createFileContent(v4(), "reaction");
|
||||||
const reaction: ChatMessageReaction = {
|
const reaction: ChatMessageReaction = {
|
||||||
...fileContent,
|
...fileContent,
|
||||||
fileId: ChatModel.createMessageReactionId(messageId, sender),
|
fileId: ChatModel.createMessageReactionId(messageId, sender),
|
||||||
|
|
||||||
messageId,
|
messageId,
|
||||||
sender,
|
sender,
|
||||||
content,
|
content,
|
||||||
isDeleting,
|
isDeleting,
|
||||||
};
|
};
|
||||||
return reaction;
|
return reaction;
|
||||||
};
|
};
|
||||||
|
|
||||||
static createMessageReactionId = (
|
static createMessageReactionId = (
|
||||||
messageId: string,
|
messageId: string,
|
||||||
sender: string,
|
sender: string,
|
||||||
): string => {
|
): string => {
|
||||||
return messageId + sender;
|
return messageId + sender;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// types
|
// types
|
||||||
export enum ReactionSymbols {
|
export enum ReactionSymbols {
|
||||||
ThumbsUp = "👍",
|
ThumbsUp = "👍",
|
||||||
Check = "✅",
|
Check = "✅",
|
||||||
Stop = "🛑",
|
Stop = "🛑",
|
||||||
Attention = "❗️",
|
Attention = "❗️",
|
||||||
DoubleAttention = "‼️",
|
DoubleAttention = "‼️",
|
||||||
Question = "❓",
|
Question = "❓",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChatInfoFileContent extends FileContent<"chat-info"> {
|
export interface ChatInfoFileContent extends FileContent<"chat-info"> {
|
||||||
@@ -478,9 +487,9 @@ export interface ChatInfoFileContent extends FileContent<"chat-info"> {
|
|||||||
|
|
||||||
export enum ChatMessageStatuses {
|
export enum ChatMessageStatuses {
|
||||||
Outbox = "outbox",
|
Outbox = "outbox",
|
||||||
Sent = "sent",
|
Sent = "sent",
|
||||||
Received = "received",
|
Received = "received",
|
||||||
Other = "other",
|
Other = "other",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChatMessage extends ValidObject {
|
export interface ChatMessage extends ValidObject {
|
||||||
@@ -498,7 +507,7 @@ export interface ChatMessage extends ValidObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ChatMessageReaction
|
export interface ChatMessageReaction
|
||||||
extends ValidObject, FileContent<"reaction"> {
|
extends ValidObject, FileContent<"reaction"> {
|
||||||
readonly messageId: string;
|
readonly messageId: string;
|
||||||
readonly sender: string;
|
readonly sender: string;
|
||||||
readonly content: ReactionSymbols | string;
|
readonly content: ReactionSymbols | string;
|
||||||
@@ -552,4 +561,4 @@ export const ChatMessageReactionReference: ChatMessageReaction = {
|
|||||||
content: "",
|
content: "",
|
||||||
|
|
||||||
isDeleting: false,
|
isDeleting: false,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
import FileModel, { FileModelSubPath } from "./fileModel";
|
import FileModel, { FileModelSubPath } from "./fileModel";
|
||||||
import { TaskFileContent } from "./boardsAndTasksModel";
|
import { TaskFileContent } from "./boardsAndTasksModel";
|
||||||
|
import { isoToMonthString, padZero } from "../Utility/utility";
|
||||||
import StorageModel from "../Global/storageModel";
|
import StorageModel from "../Global/storageModel";
|
||||||
import SettingsModel from "../Global/settingsModel";
|
import SettingsModel from "../Global/settingsModel";
|
||||||
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
|
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
|
||||||
import {isoToMonthString, padZero} from "../Utility/utility";
|
|
||||||
|
|
||||||
export default class CalendarModel {
|
export default class CalendarModel {
|
||||||
readonly storageModel: StorageModel;
|
readonly storageModel: StorageModel;
|
||||||
@@ -35,9 +35,7 @@ export default class CalendarModel {
|
|||||||
readonly storeTaskReference = (taskFileContent: TaskFileContent): void => {
|
readonly storeTaskReference = (taskFileContent: TaskFileContent): void => {
|
||||||
if (taskFileContent.date == undefined) return;
|
if (taskFileContent.date == undefined) return;
|
||||||
|
|
||||||
const monthString: string = isoToMonthString(
|
const monthString: string = isoToMonthString(taskFileContent.date);
|
||||||
taskFileContent.date,
|
|
||||||
);
|
|
||||||
|
|
||||||
const monthPath: string[] = this.getMonthPath(monthString);
|
const monthPath: string[] = this.getMonthPath(monthString);
|
||||||
const referencePath: string[] = [...monthPath, taskFileContent.fileId];
|
const referencePath: string[] = [...monthPath, taskFileContent.fileId];
|
||||||
@@ -103,9 +101,7 @@ export default class CalendarModel {
|
|||||||
};
|
};
|
||||||
|
|
||||||
for (let i = 0; i < daysInMonth; i++) {
|
for (let i = 0; i < daysInMonth; i++) {
|
||||||
const paddedDate: string = padZero(
|
const paddedDate: string = padZero((i + 1).toString(), 2);
|
||||||
(i + 1).toString(), 2
|
|
||||||
);
|
|
||||||
grid.days[paddedDate] = defaultValueCreator();
|
grid.days[paddedDate] = defaultValueCreator();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+108
-96
@@ -8,13 +8,21 @@ import {
|
|||||||
parseValidObject,
|
parseValidObject,
|
||||||
stringify,
|
stringify,
|
||||||
} from "../Utility/utility";
|
} from "../Utility/utility";
|
||||||
import { checkIsValidObject, checkMatchesObjectStructure, DATA_VERSION, ValidObject } from "../Utility/typeSafety";
|
import {
|
||||||
|
checkIsValidObject,
|
||||||
|
checkMatchesObjectStructure,
|
||||||
|
DATA_VERSION,
|
||||||
|
ValidObject,
|
||||||
|
} from "../Utility/typeSafety";
|
||||||
import StorageModel, {
|
import StorageModel, {
|
||||||
StorageModelSubPaths,
|
StorageModelSubPaths,
|
||||||
filePaths,
|
filePaths,
|
||||||
} from "../Global/storageModel";
|
} from "../Global/storageModel";
|
||||||
import SettingsModel from "../Global/settingsModel";
|
import SettingsModel from "../Global/settingsModel";
|
||||||
import ChatModel, {ChatInfoFileContent, ChatInfoReference} from "../Chat/chatModel";
|
import ChatModel, {
|
||||||
|
ChatInfoFileContent,
|
||||||
|
ChatInfoReference,
|
||||||
|
} from "../Chat/chatModel";
|
||||||
|
|
||||||
export default class FileModel {
|
export default class FileModel {
|
||||||
readonly storageModel: StorageModel;
|
readonly storageModel: StorageModel;
|
||||||
@@ -67,161 +75,165 @@ export default class FileModel {
|
|||||||
};
|
};
|
||||||
|
|
||||||
readonly handleFileContent = (fileContent: FileContent<string>): void => {
|
readonly handleFileContent = (fileContent: FileContent<string>): void => {
|
||||||
console.log(fileContent.type);
|
console.log(fileContent.type);
|
||||||
|
|
||||||
if (fileContent.type == "chat-info") {
|
if (fileContent.type == "chat-info") {
|
||||||
if (checkMatchesObjectStructure(fileContent, ChatInfoReference) == false) return;
|
if (
|
||||||
this.chatModel.handleInfo(fileContent as ChatInfoFileContent);
|
checkMatchesObjectStructure(fileContent, ChatInfoReference) ==
|
||||||
}
|
false
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
this.chatModel.handleInfo(fileContent as ChatInfoFileContent);
|
||||||
|
}
|
||||||
|
|
||||||
const didStore: boolean = this.storeFileContent(fileContent);
|
const didStore: boolean = this.storeFileContent(fileContent);
|
||||||
if (didStore == false) return;
|
if (didStore == false) return;
|
||||||
|
|
||||||
switch (fileContent.type) {
|
switch (fileContent.type) {
|
||||||
case "board-info":
|
case "board-info":
|
||||||
case "task":
|
case "task":
|
||||||
this.boardsAndTasksModel.handleFileContent(fileContent);
|
this.boardsAndTasksModel.handleFileContent(fileContent);
|
||||||
break;
|
break;
|
||||||
case "reaction":
|
case "reaction":
|
||||||
this.chatModel.handleReaction(fileContent);
|
this.chatModel.handleReaction(fileContent);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// methods
|
// methods
|
||||||
readonly addFileContentAndSend = (
|
readonly addFileContentAndSend = (
|
||||||
fileContent: FileContent<string>,
|
fileContent: FileContent<string>,
|
||||||
): void => {
|
): void => {
|
||||||
this.handleFileContent(fileContent);
|
this.handleFileContent(fileContent);
|
||||||
this.chatModel.sendMessage("", undefined, fileContent);
|
this.chatModel.sendMessage("", undefined, fileContent);
|
||||||
};
|
};
|
||||||
|
|
||||||
// storage
|
// storage
|
||||||
readonly storeFileContent = (fileContent: FileContent<string>): boolean => {
|
readonly storeFileContent = (fileContent: FileContent<string>): boolean => {
|
||||||
const fileContentPath: string[] = this.getFileContentPath(
|
const fileContentPath: string[] = this.getFileContentPath(
|
||||||
fileContent.fileId,
|
fileContent.fileId,
|
||||||
fileContent.fileContentId,
|
fileContent.fileContentId,
|
||||||
);
|
);
|
||||||
|
|
||||||
// check if fileContent already exists
|
// check if fileContent already exists
|
||||||
const existingFileContent: string | null =
|
const existingFileContent: string | null =
|
||||||
this.storageModel.read(fileContentPath);
|
this.storageModel.read(fileContentPath);
|
||||||
if (existingFileContent != null) return false;
|
if (existingFileContent != null) return false;
|
||||||
|
|
||||||
const stringifiedContent: string = stringify(fileContent);
|
const stringifiedContent: string = stringify(fileContent);
|
||||||
this.storageModel.write(fileContentPath, stringifiedContent);
|
this.storageModel.write(fileContentPath, stringifiedContent);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
readonly listFileIds = (): string[] => {
|
readonly listFileIds = (): string[] => {
|
||||||
return this.storageModel.list(this.basePath);
|
return this.storageModel.list(this.basePath);
|
||||||
};
|
};
|
||||||
|
|
||||||
readonly listFileContentIds = (fileId: string): string[] => {
|
readonly listFileContentIds = (fileId: string): string[] => {
|
||||||
const filePath: string[] = this.getFilePath(fileId);
|
const filePath: string[] = this.getFilePath(fileId);
|
||||||
return this.storageModel.list(filePath);
|
return this.storageModel.list(filePath);
|
||||||
};
|
};
|
||||||
|
|
||||||
readonly selectLatestFileContentId = (
|
readonly selectLatestFileContentId = (
|
||||||
fileContentIds: string[],
|
fileContentIds: string[],
|
||||||
): string | undefined => {
|
): string | undefined => {
|
||||||
return fileContentIds[fileContentIds.length - 1];
|
return fileContentIds[fileContentIds.length - 1];
|
||||||
};
|
};
|
||||||
|
|
||||||
readonly getFileContent = <T extends FileContent<string>>(
|
readonly getFileContent = <T extends FileContent<string>>(
|
||||||
fileId: string,
|
fileId: string,
|
||||||
fileContentName: string,
|
fileContentName: string,
|
||||||
reference: T,
|
reference: T,
|
||||||
): T | null => {
|
): T | null => {
|
||||||
const filePath: string[] = this.getFileContentPath(
|
const filePath: string[] = this.getFileContentPath(
|
||||||
fileId,
|
fileId,
|
||||||
fileContentName,
|
fileContentName,
|
||||||
);
|
);
|
||||||
const fileContentOrNull: T | null = this.storageModel.readStringifiable(
|
const fileContentOrNull: T | null = this.storageModel.readStringifiable(
|
||||||
filePath,
|
filePath,
|
||||||
reference,
|
reference,
|
||||||
);
|
);
|
||||||
return fileContentOrNull;
|
return fileContentOrNull;
|
||||||
};
|
};
|
||||||
|
|
||||||
readonly getLatestFileContent = <T extends FileContent<string>>(
|
readonly getLatestFileContent = <T extends FileContent<string>>(
|
||||||
fileId: string,
|
fileId: string,
|
||||||
reference: T,
|
reference: T,
|
||||||
): T | null => {
|
): T | null => {
|
||||||
const fileContentsIds: string[] = this.listFileContentIds(fileId);
|
const fileContentsIds: string[] = this.listFileContentIds(fileId);
|
||||||
const latestFileContentId: string | undefined =
|
const latestFileContentId: string | undefined =
|
||||||
this.selectLatestFileContentId(fileContentsIds);
|
this.selectLatestFileContentId(fileContentsIds);
|
||||||
if (latestFileContentId == undefined) return null;
|
if (latestFileContentId == undefined) return null;
|
||||||
|
|
||||||
const fileContent: T | null = this.getFileContent(
|
const fileContent: T | null = this.getFileContent(
|
||||||
fileId,
|
fileId,
|
||||||
latestFileContentId,
|
latestFileContentId,
|
||||||
reference,
|
reference,
|
||||||
);
|
);
|
||||||
return fileContent;
|
return fileContent;
|
||||||
};
|
};
|
||||||
|
|
||||||
// init
|
// init
|
||||||
constructor(
|
constructor(
|
||||||
storageModel: StorageModel,
|
storageModel: StorageModel,
|
||||||
settingsModel: SettingsModel,
|
settingsModel: SettingsModel,
|
||||||
chatModel: ChatModel,
|
chatModel: ChatModel,
|
||||||
) {
|
) {
|
||||||
this.chatModel = chatModel;
|
this.chatModel = chatModel;
|
||||||
this.settingsModel = settingsModel;
|
this.settingsModel = settingsModel;
|
||||||
this.storageModel = storageModel;
|
this.storageModel = storageModel;
|
||||||
|
|
||||||
this.boardsAndTasksModel = new BoardsAndTasksModel(
|
this.boardsAndTasksModel = new BoardsAndTasksModel(
|
||||||
this.storageModel,
|
this.storageModel,
|
||||||
this.settingsModel,
|
this.settingsModel,
|
||||||
chatModel,
|
chatModel,
|
||||||
this,
|
this,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// utility
|
// utility
|
||||||
static generateFileContentId = (creationDate: string): string => {
|
static generateFileContentId = (creationDate: string): string => {
|
||||||
return creationDate + v4();
|
return creationDate + v4();
|
||||||
};
|
};
|
||||||
|
|
||||||
static createFileContent = <T extends string>(
|
static createFileContent = <T extends string>(
|
||||||
fileId: string,
|
fileId: string,
|
||||||
type: T,
|
type: T,
|
||||||
): FileContent<T> => {
|
): FileContent<T> => {
|
||||||
const creationDate: string = createTimestamp();
|
const creationDate: string = createTimestamp();
|
||||||
const fileContentId: string =
|
const fileContentId: string =
|
||||||
FileModel.generateFileContentId(creationDate);
|
FileModel.generateFileContentId(creationDate);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dataVersion: DATA_VERSION,
|
dataVersion: DATA_VERSION,
|
||||||
|
|
||||||
fileId,
|
fileId,
|
||||||
fileContentId,
|
fileContentId,
|
||||||
creationDate,
|
creationDate,
|
||||||
type,
|
type,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// paths
|
// paths
|
||||||
export enum FileModelSubPath {
|
export enum FileModelSubPath {
|
||||||
Data = "data",
|
Data = "data",
|
||||||
Model = "model",
|
Model = "model",
|
||||||
Chat = "chat",
|
Chat = "chat",
|
||||||
ModelView = "view",
|
ModelView = "view",
|
||||||
ModelTask = "tasks",
|
ModelTask = "tasks",
|
||||||
ModelCalendar = "calendar",
|
ModelCalendar = "calendar",
|
||||||
}
|
}
|
||||||
|
|
||||||
// types
|
// types
|
||||||
export interface FileContent<T extends string>
|
export interface FileContent<T extends string>
|
||||||
extends ValidObject, StringEntryObject {
|
extends ValidObject, StringEntryObject {
|
||||||
readonly fileId: string;
|
readonly fileId: string;
|
||||||
readonly fileContentId: string;
|
readonly fileContentId: string;
|
||||||
readonly creationDate: string;
|
readonly creationDate: string;
|
||||||
|
|
||||||
readonly type: T;
|
readonly type: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
// references
|
// references
|
||||||
export const FileContentReference: FileContent<string> = {
|
export const FileContentReference: FileContent<string> = {
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export default class FileTransferModel {
|
|||||||
};
|
};
|
||||||
|
|
||||||
readonly handleDecryptedFile = (data: string): void => {
|
readonly handleDecryptedFile = (data: string): void => {
|
||||||
const parsed: any = parse(data);
|
const parsed: any = parse(data);
|
||||||
const isFileData: boolean = checkMatchesObjectStructure(
|
const isFileData: boolean = checkMatchesObjectStructure(
|
||||||
parsed,
|
parsed,
|
||||||
FileDataReference,
|
FileDataReference,
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
|
import {
|
||||||
import { bytesToMB, localeCompare, parseValidObject, stringify } from "../Utility/utility";
|
bytesToMB,
|
||||||
|
localeCompare,
|
||||||
|
parseValidObject,
|
||||||
|
stringify,
|
||||||
|
} from "../Utility/utility";
|
||||||
import { DATA_VERSION, ValidObject } from "../Utility/typeSafety";
|
import { DATA_VERSION, ValidObject } from "../Utility/typeSafety";
|
||||||
|
|
||||||
export const PATH_COMPONENT_SEPARATOR = "\\";
|
export const PATH_COMPONENT_SEPARATOR = "\\";
|
||||||
@@ -152,36 +156,36 @@ export default class StorageModel {
|
|||||||
};
|
};
|
||||||
|
|
||||||
readonly determineCapacity = (): number => {
|
readonly determineCapacity = (): number => {
|
||||||
const KEY = "storage-capacity-check";
|
const KEY = "storage-capacity-check";
|
||||||
|
|
||||||
let chunk = "0";
|
let chunk = "0";
|
||||||
for (let i = 0; i < 1024*256; i++) {
|
for (let i = 0; i < 1024 * 256; i++) {
|
||||||
chunk += "0";
|
chunk += "0";
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem(KEY, "");
|
localStorage.setItem(KEY, "");
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
const current = localStorage.getItem(KEY);
|
const current = localStorage.getItem(KEY);
|
||||||
console.log(current);
|
console.log(current);
|
||||||
localStorage.setItem(KEY, current + chunk);
|
localStorage.setItem(KEY, current + chunk);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const capacity = this.calculateUsage();
|
const capacity = this.calculateUsage();
|
||||||
localStorage.removeItem(KEY);
|
localStorage.removeItem(KEY);
|
||||||
return capacity;
|
return capacity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
readonly calculateUsage = (): number => {
|
readonly calculateUsage = (): number => {
|
||||||
let data = JSON.stringify(localStorage);
|
let data = JSON.stringify(localStorage);
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
const encoded = encoder.encode(data);
|
const encoded = encoder.encode(data);
|
||||||
const bytes = encoded.byteLength;
|
const bytes = encoded.byteLength;
|
||||||
const mb = bytesToMB(bytes);
|
const mb = bytesToMB(bytes);
|
||||||
const rounded = Math.round(mb * 100)/100;
|
const rounded = Math.round(mb * 100) / 100;
|
||||||
return rounded;
|
return rounded;
|
||||||
}
|
};
|
||||||
|
|
||||||
// tree
|
// tree
|
||||||
readonly initializeTree = (): void => {
|
readonly initializeTree = (): void => {
|
||||||
@@ -285,7 +289,10 @@ export const filePaths = {
|
|||||||
...filePaths.chat.chatBase(id),
|
...filePaths.chat.chatBase(id),
|
||||||
"reactions",
|
"reactions",
|
||||||
],
|
],
|
||||||
previousFilter: (id: string) => [...filePaths.chat.chatBase(id), "previous-filter"],
|
previousFilter: (id: string) => [
|
||||||
|
...filePaths.chat.chatBase(id),
|
||||||
|
"previous-filter",
|
||||||
|
],
|
||||||
lastUsedPage: (id: string) => [
|
lastUsedPage: (id: string) => [
|
||||||
...filePaths.chat.chatBase(id),
|
...filePaths.chat.chatBase(id),
|
||||||
"last-used-page",
|
"last-used-page",
|
||||||
|
|||||||
+237
-218
@@ -1,7 +1,7 @@
|
|||||||
import { checkIsValidObject, checkMatchesObjectStructure } from "./typeSafety";
|
import { checkIsValidObject, checkMatchesObjectStructure } from "./typeSafety";
|
||||||
import * as React from "../../react";
|
|
||||||
import {CalendarPage} from "../../View/ChatPages/calendarPage";
|
|
||||||
import CalendarModel from "../Files/calendarModel";
|
import CalendarModel from "../Files/calendarModel";
|
||||||
|
import * as React from "../../react";
|
||||||
|
import { CalendarPage } from "../../View/ChatPages/calendarPage";
|
||||||
|
|
||||||
export interface Stringifiable {
|
export interface Stringifiable {
|
||||||
toString(): string;
|
toString(): string;
|
||||||
@@ -20,37 +20,48 @@ export function createTimestamp(): string {
|
|||||||
return new Date().toISOString();
|
return new Date().toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isoToDateString (dateISOString: string): string {
|
export function isoToDateString(dateISOString: string): string {
|
||||||
const [year, month, date] = dateISOString.split("-");
|
const [year, month, date] = dateISOString.split("-");
|
||||||
const paddedDate = padZero(date ?? "", 2);
|
const paddedDate = padZero(date ?? "", 2);
|
||||||
return paddedDate;
|
return paddedDate;
|
||||||
};
|
}
|
||||||
|
|
||||||
export function isoToMonthString (dateISOString: string): string {
|
export function isoToMonthString(dateISOString: string): string {
|
||||||
const [year, month, _] = dateISOString.split("-");
|
const [year, month, _] = dateISOString.split("-");
|
||||||
return formatMonthStringFromParts(year, month);
|
return formatMonthStringFromParts(year, month);
|
||||||
};
|
}
|
||||||
|
|
||||||
export function formatISO(date: Date): string {
|
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]
|
return [year, month, date]
|
||||||
.map((x, i) => i == 0 ? x : padZero(x.toString(), 2))
|
.map((x, i) => (i == 0 ? x : padZero(x.toString(), 2)))
|
||||||
.join("-");
|
.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 {
|
export function formatTime(date: Date): string {
|
||||||
return [date.getHours(), date.getMinutes()]
|
return [date.getHours(), date.getMinutes()]
|
||||||
.map(x => padZero(x.toString(), 2))
|
.map((x) => padZero(x.toString(), 2))
|
||||||
.join(":");
|
.join(":");
|
||||||
}
|
}
|
||||||
|
|
||||||
// filters
|
// filters
|
||||||
@@ -64,11 +75,11 @@ export function filterObjectsByStringEntries<T>(
|
|||||||
const matches: Set<T> = new Set();
|
const matches: Set<T> = new Set();
|
||||||
|
|
||||||
for (const object of objects) {
|
for (const object of objects) {
|
||||||
const doesMatch: boolean = checkDoesObjectMatchReference(
|
const doesMatch: boolean = checkDoesObjectMatchReference(
|
||||||
reference,
|
reference,
|
||||||
converter(object),
|
converter(object),
|
||||||
);
|
);
|
||||||
if (doesMatch) matches.add(object);
|
if (doesMatch) matches.add(object);
|
||||||
}
|
}
|
||||||
|
|
||||||
return matches;
|
return matches;
|
||||||
@@ -82,12 +93,12 @@ export function filterObjectsByWords<T>(
|
|||||||
const matches: Set<T> = new Set();
|
const matches: Set<T> = new Set();
|
||||||
|
|
||||||
for (const object of objects) {
|
for (const object of objects) {
|
||||||
const doesMatch: boolean = checkDoesObjectMatchSearch(
|
const doesMatch: boolean = checkDoesObjectMatchSearch(
|
||||||
query,
|
query,
|
||||||
getStringsOfObject,
|
getStringsOfObject,
|
||||||
object,
|
object,
|
||||||
);
|
);
|
||||||
if (doesMatch) matches.add(object);
|
if (doesMatch) matches.add(object);
|
||||||
}
|
}
|
||||||
|
|
||||||
return matches;
|
return matches;
|
||||||
@@ -103,50 +114,55 @@ export function checkDoesObjectMatchSearch<T>(
|
|||||||
const stringsInObject: string[] = getStringsOfObject(object);
|
const stringsInObject: string[] = getStringsOfObject(object);
|
||||||
const wordsInObject: string[] = [];
|
const wordsInObject: string[] = [];
|
||||||
for (const string of stringsInObject) {
|
for (const string of stringsInObject) {
|
||||||
const lowercaseWordsInString = string
|
const lowercaseWordsInString = string
|
||||||
.toString()
|
.toString()
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.split(" ")
|
.split(" ")
|
||||||
.filter((word) => word != "");
|
.filter((word) => word != "");
|
||||||
wordsInObject.push(...lowercaseWordsInString);
|
wordsInObject.push(...lowercaseWordsInString);
|
||||||
}
|
}
|
||||||
|
|
||||||
const lowercaseWordsInQuery = query
|
const lowercaseWordsInQuery = query
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.split(" ")
|
.split(" ")
|
||||||
.filter((word) => word != "");
|
.filter((word) => word != "");
|
||||||
for (const queryWord of lowercaseWordsInQuery) {
|
for (const queryWord of lowercaseWordsInQuery) {
|
||||||
if (queryWord[0] == "-") {
|
if (queryWord[0] == "-") {
|
||||||
// exclusion
|
// exclusion
|
||||||
const wordContent = queryWord.substring(1);
|
const wordContent = queryWord.substring(1);
|
||||||
if (wordsInObject.includes(wordContent)) {
|
if (wordsInObject.includes(wordContent)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (wordsInObject.includes(queryWord) == false) {
|
if (wordsInObject.includes(queryWord) == false) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function implementFilter<T>(
|
export default function implementFilter<T>(
|
||||||
allItems: React.ListState<T>|React.MapState<T>,
|
allItems: React.ListState<T> | React.MapState<T>,
|
||||||
matches: React.ListState<T>,
|
matches: React.ListState<T>,
|
||||||
query: React.State<string>,
|
query: React.State<string>,
|
||||||
itemToString: (item: T) => string,
|
itemToString: (item: T) => string,
|
||||||
) {
|
) {
|
||||||
function update() {
|
function update() {
|
||||||
matches.clear();
|
matches.clear();
|
||||||
allItems.value.forEach((item: T) => {
|
allItems.value.forEach((item: T) => {
|
||||||
if (!itemToString(item).toLowerCase().includes(query.value.toLowerCase())) return;
|
if (
|
||||||
matches.add(item);
|
!itemToString(item)
|
||||||
allItems.handleRemoval(item, () => {
|
.toLowerCase()
|
||||||
matches.remove(item);
|
.includes(query.value.toLowerCase())
|
||||||
});
|
)
|
||||||
});
|
return;
|
||||||
|
matches.add(item);
|
||||||
|
allItems.handleRemoval(item, () => {
|
||||||
|
matches.remove(item);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
query.subscribe(update);
|
query.subscribe(update);
|
||||||
allItems.handleAddition(update);
|
allItems.handleAddition(update);
|
||||||
@@ -160,16 +176,16 @@ export class HandlerManager<T> {
|
|||||||
|
|
||||||
// manage
|
// manage
|
||||||
setHandler = (id: string, handler: Handler<T>): void => {
|
setHandler = (id: string, handler: Handler<T>): void => {
|
||||||
this.handlers.set(id, handler);
|
this.handlers.set(id, handler);
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteHandler = (id: string): void => {
|
deleteHandler = (id: string): void => {
|
||||||
this.handlers.delete(id);
|
this.handlers.delete(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
// trigger
|
// trigger
|
||||||
trigger = (item: T): void => {
|
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
|
// methods
|
||||||
update = (items: T[]): void => {
|
update = (items: T[]): void => {
|
||||||
this.sortedStrings = [];
|
this.sortedStrings = [];
|
||||||
|
|
||||||
let strings: string[] = [];
|
let strings: string[] = [];
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const string: string = this.itemToString(item);
|
const string: string = this.itemToString(item);
|
||||||
strings.push(string);
|
strings.push(string);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sortedStrings = strings.sort(localeCompare);
|
this.sortedStrings = strings.sort(localeCompare);
|
||||||
};
|
};
|
||||||
|
|
||||||
getIndex = (item: T): number => {
|
getIndex = (item: T): number => {
|
||||||
const string: string = this.itemToString(item);
|
const string: string = this.itemToString(item);
|
||||||
const index: number = this.sortedStrings.indexOf(string);
|
const index: number = this.sortedStrings.indexOf(string);
|
||||||
return index;
|
return index;
|
||||||
};
|
};
|
||||||
|
|
||||||
// init
|
// init
|
||||||
constructor(itemToString: (item: T) => string) {
|
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 {
|
export function bytesToMB(bytes: number): number {
|
||||||
return bytes / (1024*1024);
|
return bytes / (1024 * 1024);
|
||||||
}
|
}
|
||||||
|
|
||||||
// string & parsing
|
// string & parsing
|
||||||
@@ -227,9 +243,9 @@ export function padZero(string: string | undefined, length: number): string {
|
|||||||
|
|
||||||
export function parse(string: string): any {
|
export function parse(string: string): any {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(string);
|
return JSON.parse(string);
|
||||||
} catch {
|
} catch {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,8 +254,8 @@ export function parseValidObject<T>(string: string, reference: T): T | null {
|
|||||||
if (checkIsValidObject(parsed) == false) return null;
|
if (checkIsValidObject(parsed) == false) return null;
|
||||||
|
|
||||||
const doesMatchReference: boolean = checkMatchesObjectStructure(
|
const doesMatchReference: boolean = checkMatchesObjectStructure(
|
||||||
parsed,
|
parsed,
|
||||||
reference,
|
reference,
|
||||||
);
|
);
|
||||||
if (doesMatchReference == false) return null;
|
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 {
|
export function parseOrFallback(inputString: string): any {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(inputString);
|
return JSON.parse(inputString);
|
||||||
} catch {
|
} catch {
|
||||||
return inputString;
|
return inputString;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,27 +287,30 @@ export interface PinchToZoomData {
|
|||||||
x: number;
|
x: number;
|
||||||
y: 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 pinching = false;
|
||||||
let dragging = false;
|
let dragging = false;
|
||||||
let lastElement: HTMLElement | undefined = undefined;
|
let lastElement: HTMLElement | undefined = undefined;
|
||||||
|
|
||||||
let initialDistance: number,
|
let initialDistance: number,
|
||||||
initialZoom: number,
|
initialZoom: number,
|
||||||
initialX: number,
|
initialX: number,
|
||||||
initialY: number,
|
initialY: number,
|
||||||
initialTouchX: number,
|
initialTouchX: number,
|
||||||
initialTouchY: number;
|
initialTouchY: number;
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
initialDistance = 0;
|
initialDistance = 0;
|
||||||
|
|
||||||
initialZoom = 1;
|
initialZoom = 1;
|
||||||
|
|
||||||
initialX = 0;
|
initialX = 0;
|
||||||
initialY = 0;
|
initialY = 0;
|
||||||
initialTouchX = 0;
|
initialTouchX = 0;
|
||||||
initialTouchY = 0;
|
initialTouchY = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
reset();
|
reset();
|
||||||
@@ -301,117 +320,117 @@ export function implementPinchZoom(canvas: HTMLElement, data: React.State<PinchT
|
|||||||
|
|
||||||
const element = (): HTMLElement | null => canvas.querySelector(".zoom");
|
const element = (): HTMLElement | null => canvas.querySelector(".zoom");
|
||||||
const distance = (e: TouchEvent) =>
|
const distance = (e: TouchEvent) =>
|
||||||
Math.hypot(
|
Math.hypot(
|
||||||
e.touches[0].pageX - e.touches[1].pageX,
|
e.touches[0].pageX - e.touches[1].pageX,
|
||||||
e.touches[0].pageY - e.touches[1].pageY,
|
e.touches[0].pageY - e.touches[1].pageY,
|
||||||
);
|
);
|
||||||
const point = (e: TouchEvent, direction: "x" | "y", i: number) =>
|
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") =>
|
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]) {
|
function apply(zoom: number, _offset?: [number, number]) {
|
||||||
if (zoom < MIN) return apply(MIN, _offset);
|
if (zoom < MIN) return apply(MIN, _offset);
|
||||||
if (zoom > MAX) return apply(5, _offset);
|
if (zoom > MAX) return apply(5, _offset);
|
||||||
|
|
||||||
let {x, y} = data.value;
|
let { x, y } = data.value;
|
||||||
if (_offset) [x, y] = _offset;
|
if (_offset) [x, y] = _offset;
|
||||||
|
|
||||||
data.value = {
|
data.value = {
|
||||||
zoom, x, y
|
zoom,
|
||||||
}
|
x,
|
||||||
|
y,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
data.subscribe((data) => {
|
data.subscribe((data) => {
|
||||||
const el = element();
|
const el = element();
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
el.style.transform = `scale(${data.zoom.toString()}) translate(${data.x}px, ${data.y}px)`;
|
el.style.transform = `scale(${data.zoom.toString()}) translate(${data.x}px, ${data.y}px)`;
|
||||||
})
|
});
|
||||||
|
|
||||||
canvas.addEventListener("wheel", (event: WheelEvent) => {
|
canvas.addEventListener("wheel", (event: WheelEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
initialZoom = data.value.zoom;
|
initialZoom = data.value.zoom;
|
||||||
initialX = data.value.x;
|
initialX = data.value.x;
|
||||||
initialY = data.value.y;
|
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) => {
|
canvas.addEventListener("scroll", (event: Event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
canvas.addEventListener("mousedown", (event: MouseEvent) => {
|
canvas.addEventListener("mousedown", (event: MouseEvent) => {
|
||||||
const target = event.target as HTMLElement|undefined;
|
const target = event.target as HTMLElement | undefined;
|
||||||
console.log(target);
|
console.log(target);
|
||||||
if (!target || !target.classList.contains("allow-drag-move")) return;
|
if (!target || !target.classList.contains("allow-drag-move")) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
initialZoom = data.value.zoom;
|
initialZoom = data.value.zoom;
|
||||||
initialX = data.value.x;
|
initialX = data.value.x;
|
||||||
initialY = data.value.y;
|
initialY = data.value.y;
|
||||||
initialTouchX = event.clientX;
|
initialTouchX = event.clientX;
|
||||||
initialTouchY = event.clientY;
|
initialTouchY = event.clientY;
|
||||||
dragging = true;
|
dragging = true;
|
||||||
})
|
});
|
||||||
canvas.addEventListener("mouseup", () => {
|
canvas.addEventListener("mouseup", () => {
|
||||||
dragging = false;
|
dragging = false;
|
||||||
})
|
});
|
||||||
canvas.addEventListener("mousemove", (event: MouseEvent) => {
|
canvas.addEventListener("mousemove", (event: MouseEvent) => {
|
||||||
if (!dragging) return;
|
if (!dragging) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
apply(data.value.zoom, [
|
apply(data.value.zoom, [
|
||||||
initialX +
|
initialX + (event.clientX - initialTouchX) / initialZoom,
|
||||||
(event.clientX - initialTouchX) / initialZoom,
|
initialY + (event.clientY - initialTouchY) / initialZoom,
|
||||||
initialY +
|
]);
|
||||||
(event.clientY - initialTouchY) / initialZoom,
|
});
|
||||||
]);
|
|
||||||
})
|
|
||||||
|
|
||||||
canvas.addEventListener("touchstart", (event: TouchEvent) => {
|
canvas.addEventListener("touchstart", (event: TouchEvent) => {
|
||||||
(document.activeElement as HTMLElement)?.blur();
|
(document.activeElement as HTMLElement)?.blur();
|
||||||
|
|
||||||
const el = element();
|
const el = element();
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
if (lastElement != undefined && lastElement != el) reset();
|
if (lastElement != undefined && lastElement != el) reset();
|
||||||
lastElement = el;
|
lastElement = el;
|
||||||
|
|
||||||
initialZoom = data.value.zoom;
|
initialZoom = data.value.zoom;
|
||||||
initialX = data.value.x;
|
initialX = data.value.x;
|
||||||
initialY = data.value.y;
|
initialY = data.value.y;
|
||||||
|
|
||||||
if (event.touches.length != 2) {
|
if (event.touches.length != 2) {
|
||||||
pinching = false;
|
pinching = false;
|
||||||
initialTouchX = event.touches[0].clientX;
|
initialTouchX = event.touches[0].clientX;
|
||||||
initialTouchY = event.touches[0].clientY;
|
initialTouchY = event.touches[0].clientY;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pinching = true;
|
pinching = true;
|
||||||
initialDistance = distance(event);
|
initialDistance = distance(event);
|
||||||
initialTouchX = midpoint(event, "x");
|
initialTouchX = midpoint(event, "x");
|
||||||
initialTouchY = midpoint(event, "y");
|
initialTouchY = midpoint(event, "y");
|
||||||
});
|
});
|
||||||
canvas.addEventListener("touchmove", (event: TouchEvent) => {
|
canvas.addEventListener("touchmove", (event: TouchEvent) => {
|
||||||
if (!pinching) {
|
if (!pinching) {
|
||||||
apply(data.value.zoom, [
|
apply(data.value.zoom, [
|
||||||
initialX +
|
initialX +
|
||||||
(event.touches[0].clientX - initialTouchX) / initialZoom,
|
(event.touches[0].clientX - initialTouchX) / initialZoom,
|
||||||
initialY +
|
initialY +
|
||||||
(event.touches[0].clientY - initialTouchY) / initialZoom,
|
(event.touches[0].clientY - initialTouchY) / initialZoom,
|
||||||
]);
|
]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.touches.length < 2) return;
|
if (event.touches.length < 2) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const currentDistance = distance(event);
|
const currentDistance = distance(event);
|
||||||
const midX = midpoint(event, "x");
|
const midX = midpoint(event, "x");
|
||||||
const midY = midpoint(event, "y");
|
const midY = midpoint(event, "y");
|
||||||
const ratio = currentDistance / initialDistance;
|
const ratio = currentDistance / initialDistance;
|
||||||
const difference = currentDistance - initialDistance;
|
const difference = currentDistance - initialDistance;
|
||||||
apply(initialZoom * ratio, [
|
apply(initialZoom * ratio, [
|
||||||
initialX + (midX - difference - initialTouchX) / initialZoom / 2,
|
initialX + (midX - difference - initialTouchX) / initialZoom / 2,
|
||||||
initialY + (midY - difference - initialTouchY) / initialZoom / 2,
|
initialY + (midY - difference - initialTouchY) / initialZoom / 2,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -424,12 +443,12 @@ export function collectObjectValuesForKey<T>(
|
|||||||
const values: Set<string> = new Set();
|
const values: Set<string> = new Set();
|
||||||
|
|
||||||
for (const object of objects) {
|
for (const object of objects) {
|
||||||
const stringEntryObject: StringEntryObject = converter(object);
|
const stringEntryObject: StringEntryObject = converter(object);
|
||||||
const stringEntryObjectValue: Stringifiable | undefined =
|
const stringEntryObjectValue: Stringifiable | undefined =
|
||||||
stringEntryObject[key];
|
stringEntryObject[key];
|
||||||
if (stringEntryObjectValue == undefined) continue;
|
if (stringEntryObjectValue == undefined) continue;
|
||||||
|
|
||||||
values.add(stringEntryObjectValue.toString());
|
values.add(stringEntryObjectValue.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
return [...values.values()];
|
return [...values.values()];
|
||||||
@@ -442,50 +461,50 @@ export function checkDoesObjectMatchReference(
|
|||||||
explicitEmptyValue: boolean = false,
|
explicitEmptyValue: boolean = false,
|
||||||
): boolean {
|
): boolean {
|
||||||
reference_entry_loop: for (const referenceEntry of Object.entries(
|
reference_entry_loop: for (const referenceEntry of Object.entries(
|
||||||
reference,
|
reference,
|
||||||
)) {
|
)) {
|
||||||
const [referenceKey, referenceValue] = referenceEntry;
|
const [referenceKey, referenceValue] = referenceEntry;
|
||||||
const stringEntryObjectValue: Stringifiable | undefined =
|
const stringEntryObjectValue: Stringifiable | undefined =
|
||||||
stringEntryObject[referenceKey];
|
stringEntryObject[referenceKey];
|
||||||
|
|
||||||
if (referenceValue == undefined) return false;
|
if (referenceValue == undefined) return false;
|
||||||
|
|
||||||
if (referenceValue[0] == "-") {
|
if (referenceValue[0] == "-") {
|
||||||
const strippedReferenceValue: string = referenceValue
|
const strippedReferenceValue: string = referenceValue
|
||||||
.toString()
|
.toString()
|
||||||
.substring(1);
|
.substring(1);
|
||||||
// property may not exist
|
// property may not exist
|
||||||
if (
|
if (
|
||||||
strippedReferenceValue == "" &&
|
strippedReferenceValue == "" &&
|
||||||
stringEntryObjectValue != undefined &&
|
stringEntryObjectValue != undefined &&
|
||||||
stringEntryObjectValue != ""
|
stringEntryObjectValue != ""
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// property may not match
|
// property may not match
|
||||||
if (stringEntryObjectValue == strippedReferenceValue) {
|
if (stringEntryObjectValue == strippedReferenceValue) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (explicitEmptyValue == false) {
|
if (explicitEmptyValue == false) {
|
||||||
// property must exist but be anything
|
// property must exist but be anything
|
||||||
if (
|
if (
|
||||||
referenceValue == "" &&
|
referenceValue == "" &&
|
||||||
(stringEntryObjectValue == undefined ||
|
(stringEntryObjectValue == undefined ||
|
||||||
stringEntryObjectValue == "")
|
stringEntryObjectValue == "")
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
} else if (referenceValue == "") {
|
} else if (referenceValue == "") {
|
||||||
continue reference_entry_loop;
|
continue reference_entry_loop;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// property must match
|
// property must match
|
||||||
if (stringEntryObjectValue != referenceValue) {
|
if (stringEntryObjectValue != referenceValue) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,10 @@ export function BoardPage(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const wrapper = (
|
const wrapper = (
|
||||||
<div class="content main-content allow-drag-move" children:set={pages}></div>
|
<div
|
||||||
|
class="content main-content allow-drag-move"
|
||||||
|
children:set={pages}
|
||||||
|
></div>
|
||||||
);
|
);
|
||||||
Utility.implementPinchZoom(wrapper, boardViewModel.pinchToZoomData);
|
Utility.implementPinchZoom(wrapper, boardViewModel.pinchToZoomData);
|
||||||
|
|
||||||
|
|||||||
@@ -36,11 +36,11 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
padding-left: var(--content-padding);
|
padding-left: var(--content-padding);
|
||||||
gap: var(--gap-in-controls);
|
gap: var(--gap-in-controls);
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,7 +285,8 @@
|
|||||||
padding: var(--gap-between-tiles);
|
padding: var(--gap-between-tiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
#message-page[reply-view] :is(.reply-link, .inline-reply, #composer .reply-preview-wrapper) {
|
#message-page[reply-view]
|
||||||
|
:is(.reply-link, .inline-reply, #composer .reply-preview-wrapper) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+169
-148
@@ -15,156 +15,177 @@ export function MessagePage(
|
|||||||
messagePageViewModel.loadData();
|
messagePageViewModel.loadData();
|
||||||
|
|
||||||
const ChatMessageViewModelToView: React.StateItemConverter<
|
const ChatMessageViewModelToView: React.StateItemConverter<
|
||||||
ChatMessageViewModel
|
ChatMessageViewModel
|
||||||
> = (chatMessageViewModel: ChatMessageViewModel) => {
|
> = (chatMessageViewModel: ChatMessageViewModel) => {
|
||||||
return ChatMessage(coreViewModel, chatMessageViewModel);
|
return ChatMessage(coreViewModel, chatMessageViewModel);
|
||||||
};
|
};
|
||||||
|
|
||||||
const messageContainer = (
|
const messageContainer = (
|
||||||
<div
|
<div
|
||||||
id="message-container"
|
id="message-container"
|
||||||
children:append={[
|
children:append={[
|
||||||
messagePageViewModel.filteredMessageViewModels,
|
messagePageViewModel.filteredMessageViewModels,
|
||||||
ChatMessageViewModelToView,
|
ChatMessageViewModelToView,
|
||||||
]}
|
]}
|
||||||
></div>
|
></div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const persistenceId = messagePageViewModel.chatViewModel.chatModel.id;
|
const persistenceId = messagePageViewModel.chatViewModel.chatModel.id;
|
||||||
const replyPreview = ViewController.inlineReplies.setState(
|
const replyPreview = ViewController.inlineReplies.setState(
|
||||||
persistenceId,
|
persistenceId,
|
||||||
() =>
|
() =>
|
||||||
React.createProxyState(
|
React.createProxyState(
|
||||||
[messagePageViewModel.replyingMessage],
|
[messagePageViewModel.replyingMessage],
|
||||||
() => {
|
() => {
|
||||||
if (messagePageViewModel.replyingMessage.value == undefined)
|
if (messagePageViewModel.replyingMessage.value == undefined)
|
||||||
return <div></div>;
|
return <div></div>;
|
||||||
return ReplyPreview(
|
return ReplyPreview(
|
||||||
coreViewModel,
|
coreViewModel,
|
||||||
messagePageViewModel.replyingMessage.value,
|
messagePageViewModel.replyingMessage.value,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
function scrollDown(hard: boolean = false) {
|
function scrollDown(hard: boolean = false) {
|
||||||
if (hard) {
|
if (hard) {
|
||||||
messageContainer.setAttribute("scroll-hard", "");
|
messageContainer.setAttribute("scroll-hard", "");
|
||||||
|
}
|
||||||
|
messageContainer.scrollTop = messageContainer.scrollHeight;
|
||||||
|
messageContainer.removeAttribute("scroll-hard");
|
||||||
}
|
}
|
||||||
messageContainer.scrollTop = messageContainer.scrollHeight;
|
function scrollDownIfApplicable() {
|
||||||
messageContainer.removeAttribute("scroll-hard");
|
const scrollFromBottom =
|
||||||
}
|
messageContainer.scrollHeight -
|
||||||
function scrollDownIfApplicable() {
|
(messageContainer.scrollTop + messageContainer.offsetHeight);
|
||||||
const scrollFromBottom =
|
if (scrollFromBottom > 400) return;
|
||||||
messageContainer.scrollHeight -
|
|
||||||
(messageContainer.scrollTop + messageContainer.offsetHeight);
|
scrollDown();
|
||||||
if (scrollFromBottom > 400) return;
|
}
|
||||||
|
messagePageViewModel.filteredMessageViewModels.subscribeSilent(
|
||||||
scrollDown();
|
scrollDownIfApplicable,
|
||||||
}
|
);
|
||||||
messagePageViewModel.filteredMessageViewModels.subscribeSilent(
|
React.bulkSubscribe(
|
||||||
scrollDownIfApplicable,
|
[
|
||||||
);
|
messagePageViewModel.reactionFilter,
|
||||||
React.bulkSubscribe([messagePageViewModel.reactionFilter, messagePageViewModel.searchViewModel.appliedQuery], scrollDown)
|
messagePageViewModel.searchViewModel.appliedQuery,
|
||||||
messagePageViewModel.replyViewSelectedMessage.subscribeSilent(selectedMessage => {
|
],
|
||||||
if (selectedMessage != undefined) return;
|
scrollDown,
|
||||||
setTimeout(scrollDown, 100);
|
);
|
||||||
})
|
messagePageViewModel.replyViewSelectedMessage.subscribeSilent(
|
||||||
setTimeout(() => scrollDown(true), 100);
|
(selectedMessage) => {
|
||||||
|
if (selectedMessage != undefined) return;
|
||||||
messagePageViewModel.focusSetter.subscribeSilent(() => {
|
setTimeout(scrollDown, 100);
|
||||||
ViewController.setFocusWithDelay();
|
},
|
||||||
});
|
);
|
||||||
|
setTimeout(() => scrollDown(true), 100);
|
||||||
const isInReplyView = React.createProxyState([messagePageViewModel.replyViewSelectedMessage], ()=>messagePageViewModel.replyViewSelectedMessage.value != undefined);
|
|
||||||
|
messagePageViewModel.focusSetter.subscribeSilent(() => {
|
||||||
const title = React.createProxyState([messagePageViewModel.isReplyViewActive], ()=>messagePageViewModel.isReplyViewActive.value ? coreViewModel.translations.chatPage.message.replyHeaderLabel((messagePageViewModel.replyViewSelectedMessage.value!).body.value) : coreViewModel.translations.chatPage.message.messagesHeadline);
|
ViewController.setFocusWithDelay();
|
||||||
|
});
|
||||||
return (
|
|
||||||
<div id="message-page" toggle:reply-view={isInReplyView}>
|
const isInReplyView = React.createProxyState(
|
||||||
<div class="pane-wrapper">
|
[messagePageViewModel.replyViewSelectedMessage],
|
||||||
<div class="pane">
|
() => messagePageViewModel.replyViewSelectedMessage.value != undefined,
|
||||||
<div class="toolbar">
|
);
|
||||||
<button
|
|
||||||
class="ghost"
|
const title = React.createProxyState(
|
||||||
on:click={messagePageViewModel.resetReplyView}
|
[messagePageViewModel.isReplyViewActive],
|
||||||
toggle:hidden={messagePageViewModel.isReplyViewInactive}
|
() =>
|
||||||
>
|
messagePageViewModel.isReplyViewActive.value
|
||||||
<span class="icon">arrow_back</span>
|
? coreViewModel.translations.chatPage.message.replyHeaderLabel(
|
||||||
</button>
|
messagePageViewModel.replyViewSelectedMessage.value!.body
|
||||||
<span
|
.value,
|
||||||
class="title ellipsis width-100 flex"
|
)
|
||||||
subscribe:innerText={title}
|
: coreViewModel.translations.chatPage.message.messagesHeadline,
|
||||||
>
|
);
|
||||||
</span>
|
|
||||||
<span>
|
return (
|
||||||
<button
|
<div id="message-page" toggle:reply-view={isInReplyView}>
|
||||||
class="ghost inset-outline"
|
<div class="pane-wrapper">
|
||||||
on:click={messagePageViewModel.showFilterModal}
|
<div class="pane">
|
||||||
aria-label={
|
<div class="toolbar">
|
||||||
coreViewModel.translations.chatPage.message
|
<button
|
||||||
.filterMessagesButtonAudioLabel
|
class="ghost"
|
||||||
}
|
on:click={messagePageViewModel.resetReplyView}
|
||||||
toggle:selected={
|
toggle:hidden={
|
||||||
messagePageViewModel.isFilterActive
|
messagePageViewModel.isReplyViewInactive
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span class="icon">filter_alt</span>
|
<span class="icon">arrow_back</span>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
<span
|
||||||
</div>
|
class="title ellipsis width-100 flex"
|
||||||
<div class="content">
|
subscribe:innerText={title}
|
||||||
{messageContainer}
|
></span>
|
||||||
<div id="composer">
|
<span>
|
||||||
<div class="content-width-constraint">
|
<button
|
||||||
<div
|
class="ghost inset-outline"
|
||||||
class="reply-preview-wrapper"
|
on:click={messagePageViewModel.showFilterModal}
|
||||||
children:set={replyPreview}
|
aria-label={
|
||||||
></div>
|
coreViewModel.translations.chatPage.message
|
||||||
<div class="input-width-constraint">
|
.filterMessagesButtonAudioLabel
|
||||||
<input
|
}
|
||||||
id="focused"
|
toggle:selected={
|
||||||
class="blur"
|
messagePageViewModel.isFilterActive
|
||||||
bind:value={
|
}
|
||||||
messagePageViewModel.composingMessage
|
>
|
||||||
}
|
<span class="icon">filter_alt</span>
|
||||||
on:enter={
|
</button>
|
||||||
messagePageViewModel.sendMessage
|
</span>
|
||||||
}
|
</div>
|
||||||
placeholder={
|
<div class="content">
|
||||||
coreViewModel.translations.chatPage
|
{messageContainer}
|
||||||
.message
|
<div id="composer">
|
||||||
.composerInputPlaceholder
|
<div class="content-width-constraint">
|
||||||
}
|
<div
|
||||||
></input>
|
class="reply-preview-wrapper"
|
||||||
<button
|
children:set={replyPreview}
|
||||||
class="primary blur"
|
></div>
|
||||||
aria-label={
|
<div class="input-width-constraint">
|
||||||
coreViewModel.translations.chatPage
|
<input
|
||||||
.message
|
id="focused"
|
||||||
.sendMessageButtonAudioLabel
|
class="blur"
|
||||||
}
|
bind:value={
|
||||||
on:click={
|
messagePageViewModel.composingMessage
|
||||||
messagePageViewModel.sendMessage
|
}
|
||||||
}
|
on:enter={
|
||||||
toggle:disabled={
|
messagePageViewModel.sendMessage
|
||||||
messagePageViewModel.cannotSendMessage
|
}
|
||||||
}
|
placeholder={
|
||||||
>
|
coreViewModel.translations.chatPage
|
||||||
<span class="icon">send</span>
|
.message
|
||||||
</button>
|
.composerInputPlaceholder
|
||||||
</div>
|
}
|
||||||
</div>
|
></input>
|
||||||
</div>
|
<button
|
||||||
</div>
|
class="primary blur"
|
||||||
</div>
|
aria-label={
|
||||||
</div>
|
coreViewModel.translations.chatPage
|
||||||
|
.message
|
||||||
{MessageFilterModal(
|
.sendMessageButtonAudioLabel
|
||||||
coreViewModel,
|
}
|
||||||
messagePageViewModel,
|
on:click={
|
||||||
ChatMessageViewModelToView,
|
messagePageViewModel.sendMessage
|
||||||
)}
|
}
|
||||||
</div>
|
toggle:disabled={
|
||||||
);
|
messagePageViewModel.cannotSendMessage
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span class="icon">send</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{MessageFilterModal(
|
||||||
|
coreViewModel,
|
||||||
|
messagePageViewModel,
|
||||||
|
ChatMessageViewModelToView,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,12 +47,8 @@ export function SettingsPage(
|
|||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
bind:value={
|
bind:value={settingsPageViewModel.nameInput}
|
||||||
settingsPageViewModel.nameInput
|
on:enter={settingsPageViewModel.setName}
|
||||||
}
|
|
||||||
on:enter={
|
|
||||||
settingsPageViewModel.setName
|
|
||||||
}
|
|
||||||
></input>
|
></input>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
@@ -63,9 +59,7 @@ export function SettingsPage(
|
|||||||
coreViewModel.translations.chatPage.settings
|
coreViewModel.translations.chatPage.settings
|
||||||
.setNameButtonAudioLabel
|
.setNameButtonAudioLabel
|
||||||
}
|
}
|
||||||
on:click={
|
on:click={settingsPageViewModel.setName}
|
||||||
settingsPageViewModel.setName
|
|
||||||
}
|
|
||||||
toggle:disabled={
|
toggle:disabled={
|
||||||
settingsPageViewModel.cannotSetPrimaryChannel
|
settingsPageViewModel.cannotSetPrimaryChannel
|
||||||
}
|
}
|
||||||
@@ -177,7 +171,7 @@ export function SettingsPage(
|
|||||||
|
|
||||||
<hr></hr>
|
<hr></hr>
|
||||||
|
|
||||||
{ColorPicker(settingsPageViewModel.color)}
|
{ColorPicker(settingsPageViewModel.color)}
|
||||||
|
|
||||||
<div class="flex-row justify-end width-input">
|
<div class="flex-row justify-end width-input">
|
||||||
<button
|
<button
|
||||||
@@ -186,17 +180,16 @@ export function SettingsPage(
|
|||||||
coreViewModel.translations.chatPage.settings
|
coreViewModel.translations.chatPage.settings
|
||||||
.setColorButtonAudioLabel
|
.setColorButtonAudioLabel
|
||||||
}
|
}
|
||||||
on:click={
|
on:click={settingsPageViewModel.applyColor}
|
||||||
settingsPageViewModel.applyColor
|
toggle:disabled={
|
||||||
}
|
settingsPageViewModel.cannotSetColor
|
||||||
toggle:disabled={settingsPageViewModel.cannotSetColor}
|
}
|
||||||
>
|
>
|
||||||
{coreViewModel.translations.general.setButton}
|
{coreViewModel.translations.general.setButton}
|
||||||
<span class="icon">check</span>
|
<span class="icon">check</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<hr></hr>
|
<hr></hr>
|
||||||
|
|
||||||
<div class="width-input">
|
<div class="width-input">
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import "./taskPage.css";
|
|||||||
import { BoardPage } from "./boardPage";
|
import { BoardPage } from "./boardPage";
|
||||||
import { ViewController } from "../viewController";
|
import { ViewController } from "../viewController";
|
||||||
import { PlaceholderView } from "../Components/placeholderView";
|
import { PlaceholderView } from "../Components/placeholderView";
|
||||||
|
import { NewItemEntry } from "../Components/newItemEntry";
|
||||||
import { BoardViewModelToEntry } from "../Components/boardEntry";
|
import { BoardViewModelToEntry } from "../Components/boardEntry";
|
||||||
import * as React from "../../react";
|
import * as React from "../../react";
|
||||||
import TaskPageViewModel from "../../ViewModel/Pages/taskPageViewModel";
|
import TaskPageViewModel from "../../ViewModel/Pages/taskPageViewModel";
|
||||||
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
|
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
|
||||||
import {NewItemEntry} from "../Components/newItemEntry";
|
|
||||||
|
|
||||||
export function TaskPage(
|
export function TaskPage(
|
||||||
coreViewModel: CoreViewModel,
|
coreViewModel: CoreViewModel,
|
||||||
@@ -66,13 +66,18 @@ export function TaskPage(
|
|||||||
bind:value={taskPageViewModel.boardQuery}
|
bind:value={taskPageViewModel.boardQuery}
|
||||||
on:enter={taskPageViewModel.createBoard}
|
on:enter={taskPageViewModel.createBoard}
|
||||||
placeholder={
|
placeholder={
|
||||||
coreViewModel.translations.general.filterOrCreateLabel
|
coreViewModel.translations.general
|
||||||
|
.filterOrCreateLabel
|
||||||
}
|
}
|
||||||
></input>
|
></input>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="content gap">
|
<div class="content gap">
|
||||||
{NewItemEntry(coreViewModel, taskPageViewModel.boardQuery, taskPageViewModel.createBoard)}
|
{NewItemEntry(
|
||||||
|
coreViewModel,
|
||||||
|
taskPageViewModel.boardQuery,
|
||||||
|
taskPageViewModel.createBoard,
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
class="grid gap"
|
class="grid gap"
|
||||||
style="grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr))"
|
style="grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr))"
|
||||||
|
|||||||
@@ -12,14 +12,10 @@ export function ChatEntry(chatViewModel: ChatViewModel) {
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="shadow"
|
class="shadow"
|
||||||
subscribe:innerText={
|
subscribe:innerText={chatViewModel.settingsPageViewModel.name}
|
||||||
chatViewModel.settingsPageViewModel.name
|
|
||||||
}
|
|
||||||
></span>
|
></span>
|
||||||
<h2
|
<h2
|
||||||
subscribe:innerText={
|
subscribe:innerText={chatViewModel.settingsPageViewModel.name}
|
||||||
chatViewModel.settingsPageViewModel.name
|
|
||||||
}
|
|
||||||
></h2>
|
></h2>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ReplyLink } from "./replyLink";
|
||||||
import { MessageReactionButtonRow } from "./messageReactionButtonRow";
|
import { MessageReactionButtonRow } from "./messageReactionButtonRow";
|
||||||
import { InlineReply } from "./inlineReply";
|
import { InlineReply } from "./inlineReply";
|
||||||
import { ChatMessageInfoModal } from "../Modals/chatMessageInfoModal";
|
import { ChatMessageInfoModal } from "../Modals/chatMessageInfoModal";
|
||||||
@@ -5,7 +6,6 @@ import * as React from "../../react";
|
|||||||
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
|
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
|
||||||
import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel";
|
import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel";
|
||||||
import { ChatMessageStatuses } from "../../Model/Chat/chatModel";
|
import { ChatMessageStatuses } from "../../Model/Chat/chatModel";
|
||||||
import {ReplyLink} from "./replyLink";
|
|
||||||
|
|
||||||
export function ChatMessage(
|
export function ChatMessage(
|
||||||
coreViewModel: CoreViewModel,
|
coreViewModel: CoreViewModel,
|
||||||
@@ -69,8 +69,11 @@ export function ChatMessage(
|
|||||||
aria-label={
|
aria-label={
|
||||||
coreViewModel.translations.chatPage.message
|
coreViewModel.translations.chatPage.message
|
||||||
.replyToMessageButton
|
.replyToMessageButton
|
||||||
}
|
}
|
||||||
toggle:hidden={chatMessageViewModel.messagePageViewModel.isReplyViewActive}
|
toggle:hidden={
|
||||||
|
chatMessageViewModel.messagePageViewModel
|
||||||
|
.isReplyViewActive
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span class="icon">reply</span>
|
<span class="icon">reply</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
import * as React from "../../react";
|
import * as React from "../../react";
|
||||||
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
|
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
|
||||||
|
|
||||||
export function NewItemEntry(coreViewModel: CoreViewModel, query: React.State<string>, fn: ()=>void) {
|
export function NewItemEntry(
|
||||||
const isHidden = React.createProxyState([query],()=>query.value == "");
|
coreViewModel: CoreViewModel,
|
||||||
const label = React.createProxyState([query],()=>coreViewModel.translations.general.createLabel(query.value));
|
query: React.State<string>,
|
||||||
|
fn: () => void,
|
||||||
|
) {
|
||||||
|
const isHidden = React.createProxyState([query], () => query.value == "");
|
||||||
|
const label = React.createProxyState([query], () =>
|
||||||
|
coreViewModel.translations.general.createLabel(query.value),
|
||||||
|
);
|
||||||
|
|
||||||
return <button
|
return (
|
||||||
class="standard slide-up"
|
<button
|
||||||
toggle:hidden={isHidden}
|
class="standard slide-up"
|
||||||
on:click={fn}
|
toggle:hidden={isHidden}
|
||||||
>
|
on:click={fn}
|
||||||
<span subscribe:innerText={label}></span>
|
>
|
||||||
<span class="icon">add</span>
|
<span subscribe:innerText={label}></span>
|
||||||
</button>
|
<span class="icon">add</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,31 @@
|
|||||||
import * as React from "../../react";
|
import * as React from "../../react";
|
||||||
import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel";
|
|
||||||
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
|
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
|
||||||
|
import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel";
|
||||||
|
|
||||||
export function ReplyLink(coreViewModel: CoreViewModel, chatMessageViewModel: ChatMessageViewModel) {
|
export function ReplyLink(
|
||||||
const isHidden = React.createProxyState([chatMessageViewModel.replyCount], ()=>chatMessageViewModel.replyCount.value == 0);
|
coreViewModel: CoreViewModel,
|
||||||
|
chatMessageViewModel: ChatMessageViewModel,
|
||||||
|
) {
|
||||||
|
const isHidden = React.createProxyState(
|
||||||
|
[chatMessageViewModel.replyCount],
|
||||||
|
() => chatMessageViewModel.replyCount.value == 0,
|
||||||
|
);
|
||||||
|
|
||||||
function select() {
|
function select() {
|
||||||
chatMessageViewModel.messagePageViewModel.setReplyView(chatMessageViewModel);
|
chatMessageViewModel.messagePageViewModel.setReplyView(
|
||||||
|
chatMessageViewModel,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div class="reply-link" toggle:hidden={isHidden} on:click={select}>
|
||||||
class="reply-link"
|
<span>
|
||||||
toggle:hidden={isHidden}
|
{coreViewModel.translations.chatPage.message.replyPrefixLabel}
|
||||||
on:click={select}
|
</span>
|
||||||
>
|
<b
|
||||||
<span>{coreViewModel.translations.chatPage.message.replyPrefixLabel}</span>
|
class="ellipsis"
|
||||||
<b class="ellipsis" subscribe:innerText={chatMessageViewModel.replyCount}></b>
|
subscribe:innerText={chatMessageViewModel.replyCount}
|
||||||
|
></b>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,48 @@
|
|||||||
import * as React from "../../react";
|
import * as React from "../../react";
|
||||||
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
|
|
||||||
import MessagePageViewModel from "../../ViewModel/Pages/messagePageViewModel";
|
import MessagePageViewModel from "../../ViewModel/Pages/messagePageViewModel";
|
||||||
|
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
|
||||||
|
|
||||||
export function ReplyViewHeader(coreViewModel: CoreViewModel, messagePageViewModel: MessagePageViewModel) {
|
export function ReplyViewHeader(
|
||||||
const isHidden = React.createProxyState([messagePageViewModel.isReplyViewActive], ()=>!messagePageViewModel.isReplyViewActive.value);
|
coreViewModel: CoreViewModel,
|
||||||
|
messagePageViewModel: MessagePageViewModel,
|
||||||
|
) {
|
||||||
|
const isHidden = React.createProxyState(
|
||||||
|
[messagePageViewModel.isReplyViewActive],
|
||||||
|
() => !messagePageViewModel.isReplyViewActive.value,
|
||||||
|
);
|
||||||
|
|
||||||
const label = React.createProxyState([messagePageViewModel.replyViewSelectedMessage], ()=>messagePageViewModel.replyViewSelectedMessage.value == undefined ? "" : coreViewModel.translations.chatPage.message.replyHeaderLabel(messagePageViewModel.replyViewSelectedMessage.value.sender));
|
const label = React.createProxyState(
|
||||||
const message = React.createProxyState([messagePageViewModel.replyViewSelectedMessage], ()=>messagePageViewModel.replyViewSelectedMessage.value == undefined ? "" : messagePageViewModel.replyViewSelectedMessage.value.body.value);
|
[messagePageViewModel.replyViewSelectedMessage],
|
||||||
|
() =>
|
||||||
|
messagePageViewModel.replyViewSelectedMessage.value == undefined
|
||||||
|
? ""
|
||||||
|
: coreViewModel.translations.chatPage.message.replyHeaderLabel(
|
||||||
|
messagePageViewModel.replyViewSelectedMessage.value
|
||||||
|
.sender,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const message = React.createProxyState(
|
||||||
|
[messagePageViewModel.replyViewSelectedMessage],
|
||||||
|
() =>
|
||||||
|
messagePageViewModel.replyViewSelectedMessage.value == undefined
|
||||||
|
? ""
|
||||||
|
: messagePageViewModel.replyViewSelectedMessage.value.body
|
||||||
|
.value,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div class="reply-header" toggle:hidden={isHidden}>
|
||||||
class="reply-header"
|
<b class="ellipsis" subscribe:innerText={label}></b>
|
||||||
toggle:hidden={isHidden}
|
<span
|
||||||
>
|
class="secondary ellipsis"
|
||||||
<b class="ellipsis" subscribe:innerText={label}></b>
|
subscribe:innerText={message}
|
||||||
<span class="secondary ellipsis" subscribe:innerText={message}></span>
|
></span>
|
||||||
<button
|
<button
|
||||||
class="standard square"
|
class="standard square"
|
||||||
on:click={messagePageViewModel.resetReplyView}
|
on:click={messagePageViewModel.resetReplyView}
|
||||||
>
|
>
|
||||||
<span class="icon">close</span>
|
<span class="icon">close</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ export function TaskEntry(taskViewModel: TaskViewModel) {
|
|||||||
schedule: taskViewModel.time.value || "---",
|
schedule: taskViewModel.time.value || "---",
|
||||||
};
|
};
|
||||||
|
|
||||||
const className = React.createProxyState([taskViewModel.isNotNext], ()=>
|
const className = React.createProxyState(
|
||||||
`${taskViewModel.isNotNext.value ? "standard" : "primary"} tile flex-no`
|
[taskViewModel.isNotNext],
|
||||||
)
|
() =>
|
||||||
|
`${taskViewModel.isNotNext.value ? "standard" : "primary"} tile flex-no`,
|
||||||
|
);
|
||||||
|
|
||||||
const view = (
|
const view = (
|
||||||
<button
|
<button
|
||||||
@@ -22,45 +24,45 @@ export function TaskEntry(taskViewModel: TaskViewModel) {
|
|||||||
style="user-select: none; -webkit-user-select: none"
|
style="user-select: none; -webkit-user-select: none"
|
||||||
on:click={taskViewModel.open}
|
on:click={taskViewModel.open}
|
||||||
on:dragstart={taskViewModel.dragStart}
|
on:dragstart={taskViewModel.dragStart}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<span
|
<span class="secondary" toggle:hidden={taskViewModel.isNotNext}>
|
||||||
class="secondary"
|
{
|
||||||
toggle:hidden={taskViewModel.isNotNext}
|
taskViewModel.coreViewModel.translations.chatPage
|
||||||
>
|
.calendar.eventNext
|
||||||
{taskViewModel.coreViewModel.translations.chatPage.calendar.eventNext}
|
}
|
||||||
<hr></hr>
|
<hr></hr>
|
||||||
</span>
|
</span>
|
||||||
<b
|
<b
|
||||||
class="ellipsis"
|
class="ellipsis"
|
||||||
subscribe:innerText={taskViewModel.name}
|
subscribe:innerText={taskViewModel.name}
|
||||||
></b>
|
></b>
|
||||||
<hr></hr>
|
<hr></hr>
|
||||||
<div
|
<div
|
||||||
class="grid secondary"
|
class="grid secondary"
|
||||||
style="grid-template-columns: repeat(2, 1fr); column-gap: 1rem; row-gap: .5rem"
|
style="grid-template-columns: repeat(2, 1fr); column-gap: 1rem; row-gap: .5rem"
|
||||||
>
|
>
|
||||||
{...Object.entries(details).map((entry) => (
|
{...Object.entries(details).map((entry) => (
|
||||||
<span
|
<span
|
||||||
class="flex-row align-center width-100 flex-no clip"
|
class="flex-row align-center width-100 flex-no clip"
|
||||||
style="gap: 1rem"
|
style="gap: 1rem"
|
||||||
>
|
>
|
||||||
<span class="icon" style="font-size: 1.1rem">
|
<span class="icon" style="font-size: 1.1rem">
|
||||||
{entry[0]}
|
{entry[0]}
|
||||||
</span>
|
</span>
|
||||||
<span class="ellipsis">{entry[1]}</span>
|
<span class="ellipsis">{entry[1]}</span>
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
||||||
taskViewModel.index.subscribe((newIndex) => {
|
taskViewModel.index.subscribe((newIndex) => {
|
||||||
view.style.order = newIndex;
|
view.style.order = newIndex;
|
||||||
});
|
});
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TaskViewModelToEntry: React.StateItemConverter<TaskViewModel> = (
|
export const TaskViewModelToEntry: React.StateItemConverter<TaskViewModel> = (
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
import * as React from "../../react";
|
import * as React from "../../react";
|
||||||
|
|
||||||
export function UsageBar(label: string, valueLabel: React.State<string>, value: React.State<number>, maximum: React.State<number>) {
|
export function UsageBar(
|
||||||
const style = React.createProxyState([value, maximum], ()=>`width: ${100*(value.value/maximum.value)}%`)
|
label: string,
|
||||||
|
valueLabel: React.State<string>,
|
||||||
|
value: React.State<number>,
|
||||||
|
maximum: React.State<number>,
|
||||||
|
) {
|
||||||
|
const style = React.createProxyState(
|
||||||
|
[value, maximum],
|
||||||
|
() => `width: ${100 * (value.value / maximum.value)}%`,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div class="surface flex-column padding gap">
|
||||||
class="surface flex-column padding gap"
|
<b>{label}</b>
|
||||||
>
|
<div class="width-100 surface-alt" style="height: 18px">
|
||||||
<b>{label}</b>
|
<div
|
||||||
<div
|
class="height-100 background-primary"
|
||||||
class="width-100 surface-alt"
|
set:style={style}
|
||||||
style="height: 18px"
|
></div>
|
||||||
>
|
</div>
|
||||||
<div
|
<span class="secondary" subscribe:innerText={valueLabel}></span>
|
||||||
class="height-100 background-primary"
|
</div>
|
||||||
set:style={style}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
class="secondary"
|
|
||||||
subscribe:innerText={valueLabel}
|
|
||||||
></span>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,10 +72,13 @@ export function ChatMessageInfoModal(
|
|||||||
}
|
}
|
||||||
<span class="icon">key</span>
|
<span class="icon">key</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
on:click={chatMessageViewModel.reply}
|
on:click={chatMessageViewModel.reply}
|
||||||
toggle:disabled={chatMessageViewModel.messagePageViewModel.isReplyViewActive}
|
toggle:disabled={
|
||||||
>
|
chatMessageViewModel.messagePageViewModel
|
||||||
|
.isReplyViewActive
|
||||||
|
}
|
||||||
|
>
|
||||||
{
|
{
|
||||||
coreViewModel.translations.chatPage.message
|
coreViewModel.translations.chatPage.message
|
||||||
.replyToMessageButton
|
.replyToMessageButton
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { v4 } from "uuid";
|
import { v4 } from "uuid";
|
||||||
import { ViewController } from "../viewController";
|
import { ViewController } from "../viewController";
|
||||||
import { StringToOption } from "../Components/option";
|
import { StringToOption } from "../Components/option";
|
||||||
|
import { DeletableListItem } from "../Components/deletableListItem";
|
||||||
import * as React from "../../react";
|
import * as React from "../../react";
|
||||||
import SearchViewModel from "../../ViewModel/Utility/searchViewModel";
|
import SearchViewModel from "../../ViewModel/Utility/searchViewModel";
|
||||||
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
|
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
|
||||||
import {DeletableListItem} from "../Components/deletableListItem";
|
|
||||||
|
|
||||||
export function SearchModal<T>(
|
export function SearchModal<T>(
|
||||||
coreViewModel: CoreViewModel,
|
coreViewModel: CoreViewModel,
|
||||||
@@ -13,92 +13,92 @@ export function SearchModal<T>(
|
|||||||
isOpen: React.State<boolean>,
|
isOpen: React.State<boolean>,
|
||||||
) {
|
) {
|
||||||
function close() {
|
function close() {
|
||||||
isOpen.value = false;
|
isOpen.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const suggestionId = v4();
|
const suggestionId = v4();
|
||||||
|
|
||||||
isOpen.subscribe((isOpen) => {
|
isOpen.subscribe((isOpen) => {
|
||||||
if (!isOpen) return;
|
if (!isOpen) return;
|
||||||
ViewController.setFocusWithDelay();
|
ViewController.setFocusWithDelay();
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="modal" toggle:open={isOpen} extended>
|
<div class="modal" toggle:open={isOpen} extended>
|
||||||
<div>
|
<div>
|
||||||
<main>
|
<main>
|
||||||
<h2>{headline}</h2>
|
<h2>{headline}</h2>
|
||||||
<div class="flex-row">
|
<div class="flex-row">
|
||||||
<input
|
<input
|
||||||
id="focused"
|
id="focused"
|
||||||
style="max-width: unset"
|
style="max-width: unset"
|
||||||
placeholder={
|
placeholder={
|
||||||
coreViewModel.translations.general.searchLabel
|
coreViewModel.translations.general.searchLabel
|
||||||
}
|
}
|
||||||
bind:value={searchViewModel.searchInput}
|
bind:value={searchViewModel.searchInput}
|
||||||
on:enter={searchViewModel.applySearch}
|
on:enter={searchViewModel.applySearch}
|
||||||
list={suggestionId}
|
list={suggestionId}
|
||||||
></input>
|
></input>
|
||||||
<datalist
|
<datalist
|
||||||
hidden
|
hidden
|
||||||
id={suggestionId}
|
id={suggestionId}
|
||||||
children:append={[
|
children:append={[
|
||||||
searchViewModel.suggestions,
|
searchViewModel.suggestions,
|
||||||
StringToOption,
|
StringToOption,
|
||||||
]}
|
]}
|
||||||
></datalist>
|
></datalist>
|
||||||
<button
|
<button
|
||||||
class="standard"
|
class="standard"
|
||||||
aria-label={
|
aria-label={
|
||||||
coreViewModel.translations.general
|
coreViewModel.translations.general
|
||||||
.searchButtonClearAudioLabel
|
.searchButtonClearAudioLabel
|
||||||
}
|
}
|
||||||
on:click={searchViewModel.clear}
|
on:click={searchViewModel.clear}
|
||||||
toggle:disabled={searchViewModel.cannotClear}
|
toggle:disabled={searchViewModel.cannotClear}
|
||||||
>
|
>
|
||||||
<span class="icon">close</span>
|
<span class="icon">close</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="primary"
|
class="primary"
|
||||||
aria-label={
|
aria-label={
|
||||||
coreViewModel.translations.general
|
coreViewModel.translations.general
|
||||||
.searchButtonAudioLabel
|
.searchButtonAudioLabel
|
||||||
}
|
}
|
||||||
on:click={searchViewModel.applySearch}
|
on:click={searchViewModel.applySearch}
|
||||||
toggle:disabled={searchViewModel.cannotApplySearch}
|
toggle:disabled={searchViewModel.cannotApplySearch}
|
||||||
>
|
>
|
||||||
<span class="icon">search</span>
|
<span class="icon">search</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div toggle:hidden={searchViewModel.hasNoSuggestions}>
|
<div toggle:hidden={searchViewModel.hasNoSuggestions}>
|
||||||
<hr></hr>
|
<hr></hr>
|
||||||
<h3>
|
<h3>
|
||||||
{
|
{
|
||||||
coreViewModel.translations.general
|
coreViewModel.translations.general
|
||||||
.searchSuggestionsLabel
|
.searchSuggestionsLabel
|
||||||
}
|
}
|
||||||
</h3>
|
</h3>
|
||||||
<div
|
<div
|
||||||
class="flex-column gap"
|
class="flex-column gap"
|
||||||
children:append={[
|
children:append={[
|
||||||
searchViewModel.suggestions,
|
searchViewModel.suggestions,
|
||||||
(suggestion: string) =>
|
(suggestion: string) =>
|
||||||
SuggestionView(
|
SuggestionView(
|
||||||
suggestion,
|
suggestion,
|
||||||
searchViewModel,
|
searchViewModel,
|
||||||
coreViewModel,
|
coreViewModel,
|
||||||
),
|
),
|
||||||
]}
|
]}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<button on:click={close}>
|
<button on:click={close}>
|
||||||
{coreViewModel.translations.general.closeButton}
|
{coreViewModel.translations.general.closeButton}
|
||||||
<span class="icon">close</span>
|
<span class="icon">close</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,26 +108,29 @@ function SuggestionView<T>(
|
|||||||
coreViewModel: CoreViewModel,
|
coreViewModel: CoreViewModel,
|
||||||
) {
|
) {
|
||||||
const isApplied = React.createProxyState(
|
const isApplied = React.createProxyState(
|
||||||
[searchViewModel.appliedQuery],
|
[searchViewModel.appliedQuery],
|
||||||
() => searchViewModel.appliedQuery.value == suggestion,
|
() => searchViewModel.appliedQuery.value == suggestion,
|
||||||
);
|
);
|
||||||
|
|
||||||
function deleteSuggestion() {
|
function deleteSuggestion() {
|
||||||
searchViewModel.deleteSuggestion(suggestion);
|
searchViewModel.deleteSuggestion(suggestion);
|
||||||
}
|
}
|
||||||
|
|
||||||
function applySuggestion() {
|
function applySuggestion() {
|
||||||
searchViewModel.search(suggestion);
|
searchViewModel.search(suggestion);
|
||||||
}
|
}
|
||||||
|
|
||||||
return DeletableListItem(coreViewModel, suggestion,
|
return DeletableListItem(
|
||||||
<button
|
coreViewModel,
|
||||||
class="primary"
|
suggestion,
|
||||||
aria-label={coreViewModel.translations.general.applyButton}
|
<button
|
||||||
on:click={applySuggestion}
|
class="primary"
|
||||||
toggle:disabled={isApplied}
|
aria-label={coreViewModel.translations.general.applyButton}
|
||||||
>
|
on:click={applySuggestion}
|
||||||
<span class="icon">check</span>
|
toggle:disabled={isApplied}
|
||||||
</button>
|
>
|
||||||
, deleteSuggestion)
|
<span class="icon">check</span>
|
||||||
|
</button>,
|
||||||
|
deleteSuggestion,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,25 +125,20 @@ function SettingsInfoPane(
|
|||||||
settingsViewModel.coreViewModel.version,
|
settingsViewModel.coreViewModel.version,
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="primary"
|
class="primary"
|
||||||
on:click={coreViewModel.update}
|
on:click={coreViewModel.update}
|
||||||
toggle:hidden={coreViewModel.noUpdateAvailable}
|
toggle:hidden={coreViewModel.noUpdateAvailable}
|
||||||
>
|
>
|
||||||
<span
|
<span subscribe:innerText={coreViewModel.updateText}></span>
|
||||||
subscribe:innerText={coreViewModel.updateText}
|
<span class="icon">update</span>
|
||||||
></span>
|
</button>
|
||||||
<span class="icon">update</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
<button class="standard" on:click={coreViewModel.checkUpdates}>
|
||||||
class="standard"
|
{coreViewModel.translations.settings.checkUpdatesButton}
|
||||||
on:click={coreViewModel.checkUpdates}
|
</button>
|
||||||
>
|
</div>
|
||||||
{coreViewModel.translations.settings.checkUpdatesButton}
|
</div>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,30 +147,30 @@ function SettingsRegionalPane(
|
|||||||
settingsViewModel: SettingsViewModel,
|
settingsViewModel: SettingsViewModel,
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<div class="slide-up">
|
<div class="slide-up">
|
||||||
<h2>{coreViewModel.translations.settings.pages.regional}</h2>
|
<h2>{coreViewModel.translations.settings.pages.regional}</h2>
|
||||||
|
|
||||||
<hr></hr>
|
<hr></hr>
|
||||||
<h3>{coreViewModel.translations.settings.language}</h3>
|
<h3>{coreViewModel.translations.settings.language}</h3>
|
||||||
{OptionButtonList(
|
{OptionButtonList(
|
||||||
new React.ListState<[string, string]>(
|
new React.ListState<[string, string]>(
|
||||||
Object.values(Languages).map((x) => [languageNames[x], x]),
|
Object.values(Languages).map((x) => [languageNames[x], x]),
|
||||||
),
|
),
|
||||||
settingsViewModel.language,
|
settingsViewModel.language,
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<hr></hr>
|
<hr></hr>
|
||||||
<h3>{coreViewModel.translations.settings.firstDayOfWeekLabel}</h3>
|
<h3>{coreViewModel.translations.settings.firstDayOfWeekLabel}</h3>
|
||||||
{OptionButtonList(
|
{OptionButtonList(
|
||||||
new React.ListState<[string, string]>(
|
new React.ListState<[string, string]>(
|
||||||
coreViewModel.translations.regional.weekdays.full.map(
|
coreViewModel.translations.regional.weekdays.full.map(
|
||||||
(x, i) => [x, i.toString()],
|
(x, i) => [x, i.toString()],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
settingsViewModel.firstDayOfWeek,
|
settingsViewModel.firstDayOfWeek,
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SettingsAppearancePane(
|
function SettingsAppearancePane(
|
||||||
@@ -183,34 +178,34 @@ function SettingsAppearancePane(
|
|||||||
settingsViewModel: SettingsViewModel,
|
settingsViewModel: SettingsViewModel,
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<div class="slide-up">
|
<div class="slide-up">
|
||||||
<h2>{coreViewModel.translations.settings.pages.appearance}</h2>
|
<h2>{coreViewModel.translations.settings.pages.appearance}</h2>
|
||||||
<hr></hr>
|
<hr></hr>
|
||||||
{OptionButtonList(
|
{OptionButtonList(
|
||||||
new React.ListState<[string, string]>([
|
new React.ListState<[string, string]>([
|
||||||
[
|
[
|
||||||
coreViewModel.translations.settings.themes.dynamic,
|
coreViewModel.translations.settings.themes.dynamic,
|
||||||
ThemeSettings.Dynamic,
|
ThemeSettings.Dynamic,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
coreViewModel.translations.settings.themes.system,
|
coreViewModel.translations.settings.themes.system,
|
||||||
ThemeSettings.System,
|
ThemeSettings.System,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
coreViewModel.translations.settings.themes.dark,
|
coreViewModel.translations.settings.themes.dark,
|
||||||
ThemeSettings.Dark,
|
ThemeSettings.Dark,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
coreViewModel.translations.settings.themes.light,
|
coreViewModel.translations.settings.themes.light,
|
||||||
ThemeSettings.Light,
|
ThemeSettings.Light,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
coreViewModel.translations.settings.themes.black,
|
coreViewModel.translations.settings.themes.black,
|
||||||
ThemeSettings.Black,
|
ThemeSettings.Black,
|
||||||
],
|
],
|
||||||
]),
|
]),
|
||||||
settingsViewModel.theme,
|
settingsViewModel.theme,
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,94 +1,105 @@
|
|||||||
import { SplitModal } from "./splitModal";
|
import { SplitModal } from "./splitModal";
|
||||||
|
import { UsageBar } from "../Components/usageBar";
|
||||||
import { DirectoryItemList } from "../Components/directoryItemList";
|
import { DirectoryItemList } from "../Components/directoryItemList";
|
||||||
import { DangerousActionButton } from "../Components/dangerousActionButton";
|
import { DangerousActionButton } from "../Components/dangerousActionButton";
|
||||||
import * as React from "../../react";
|
import * as React from "../../react";
|
||||||
import StorageViewModel from "../../ViewModel/Global/storageViewModel";
|
import StorageViewModel from "../../ViewModel/Global/storageViewModel";
|
||||||
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
|
import CoreViewModel from "../../ViewModel/Global/coreViewModel";
|
||||||
import { PATH_COMPONENT_SEPARATOR } from "../../Model/Global/storageModel";
|
import { PATH_COMPONENT_SEPARATOR } from "../../Model/Global/storageModel";
|
||||||
import {UsageBar} from "../Components/usageBar";
|
|
||||||
|
|
||||||
export function StorageModal(
|
export function StorageModal(
|
||||||
coreViewModel: CoreViewModel,
|
coreViewModel: CoreViewModel,
|
||||||
storageViewModel: StorageViewModel,
|
storageViewModel: StorageViewModel,
|
||||||
) {
|
) {
|
||||||
const usageValueLabel = React.createProxyState([storageViewModel.occupiedSpaceMB, storageViewModel.maximumSpaceMB], ()=>
|
const usageValueLabel = React.createProxyState(
|
||||||
coreViewModel.translations.storage.usageVauleLabel(storageViewModel.occupiedSpaceMB.value, storageViewModel.maximumSpaceMB.value));
|
[storageViewModel.occupiedSpaceMB, storageViewModel.maximumSpaceMB],
|
||||||
|
() =>
|
||||||
|
coreViewModel.translations.storage.usageVauleLabel(
|
||||||
|
storageViewModel.occupiedSpaceMB.value,
|
||||||
|
storageViewModel.maximumSpaceMB.value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const detailView = React.createProxyState(
|
const detailView = React.createProxyState(
|
||||||
[storageViewModel.selectedPath],
|
[storageViewModel.selectedPath],
|
||||||
() => {
|
() => {
|
||||||
if (storageViewModel.selectedPath.value == PATH_COMPONENT_SEPARATOR)
|
if (storageViewModel.selectedPath.value == PATH_COMPONENT_SEPARATOR)
|
||||||
return (
|
return (
|
||||||
<div class="flex-column gap">
|
<div class="flex-column gap">
|
||||||
<span class="secondary">
|
<span class="secondary">
|
||||||
{coreViewModel.translations.storage.noItemSelected}
|
{coreViewModel.translations.storage.noItemSelected}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<hr></hr>
|
<hr></hr>
|
||||||
|
|
||||||
{UsageBar(coreViewModel.translations.storage.usageLabel, usageValueLabel, storageViewModel.occupiedSpaceMB, storageViewModel.maximumSpaceMB)}
|
{UsageBar(
|
||||||
|
coreViewModel.translations.storage.usageLabel,
|
||||||
|
usageValueLabel,
|
||||||
|
storageViewModel.occupiedSpaceMB,
|
||||||
|
storageViewModel.maximumSpaceMB,
|
||||||
|
)}
|
||||||
|
|
||||||
<hr></hr>
|
<hr></hr>
|
||||||
|
|
||||||
{DangerousActionButton(
|
{DangerousActionButton(
|
||||||
coreViewModel,
|
coreViewModel,
|
||||||
coreViewModel.translations.storage.removeJunkButton,
|
coreViewModel.translations.storage.removeJunkButton,
|
||||||
"delete_forever",
|
"delete_forever",
|
||||||
storageViewModel.removeJunk,
|
storageViewModel.removeJunk,
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex-column gap">
|
<div class="flex-column gap">
|
||||||
<div class="tile flex-no">
|
<div class="tile flex-no">
|
||||||
<div>
|
<div>
|
||||||
<b>{coreViewModel.translations.storage.path}</b>
|
<b>{coreViewModel.translations.storage.path}</b>
|
||||||
<span
|
<span
|
||||||
class="break-all"
|
class="break-all"
|
||||||
subscribe:innerText={
|
subscribe:innerText={
|
||||||
storageViewModel.selectedPath
|
storageViewModel.selectedPath
|
||||||
}
|
}
|
||||||
></span>
|
></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tile flex-no">
|
<div class="tile flex-no">
|
||||||
<div>
|
<div>
|
||||||
<b>{coreViewModel.translations.storage.content}</b>
|
<b>{coreViewModel.translations.storage.content}</b>
|
||||||
<code
|
<code
|
||||||
subscribe:innerText={
|
subscribe:innerText={
|
||||||
storageViewModel.selectedFileContent
|
storageViewModel.selectedFileContent
|
||||||
}
|
}
|
||||||
></code>
|
></code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{DangerousActionButton(
|
{DangerousActionButton(
|
||||||
coreViewModel,
|
coreViewModel,
|
||||||
coreViewModel.translations.storage.deleteItem,
|
coreViewModel.translations.storage.deleteItem,
|
||||||
"delete_forever",
|
"delete_forever",
|
||||||
storageViewModel.deleteSelectedItem,
|
storageViewModel.deleteSelectedItem,
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div class="modal" toggle:open={storageViewModel.isShowingStorageModal}>
|
<div class="modal" toggle:open={storageViewModel.isShowingStorageModal}>
|
||||||
<div>
|
<div>
|
||||||
<main class="padding-0">
|
<main class="padding-0">
|
||||||
{SplitModal(
|
{SplitModal(
|
||||||
coreViewModel,
|
coreViewModel,
|
||||||
new React.State(DirectoryItemList(storageViewModel)),
|
new React.State(DirectoryItemList(storageViewModel)),
|
||||||
detailView,
|
detailView,
|
||||||
true,
|
true,
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
<button on:click={storageViewModel.close}>
|
<button on:click={storageViewModel.close}>
|
||||||
{coreViewModel.translations.general.closeButton}
|
{coreViewModel.translations.general.closeButton}
|
||||||
<span class="icon">close</span>
|
<span class="icon">close</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+174
-176
@@ -20,195 +20,193 @@ export function HomePage(
|
|||||||
) {
|
) {
|
||||||
// sections
|
// sections
|
||||||
const overviewSection = (
|
const overviewSection = (
|
||||||
<div id="overview-section">
|
<div id="overview-section">
|
||||||
<h2>{coreViewModel.translations.homePage.overviewHeadline}</h2>
|
<h2>{coreViewModel.translations.homePage.overviewHeadline}</h2>
|
||||||
|
|
||||||
<label class="tile flex-no">
|
<label class="tile flex-no">
|
||||||
<span class="icon">cell_tower</span>
|
<span class="icon">cell_tower</span>
|
||||||
<div>
|
<div>
|
||||||
<span>
|
<span>
|
||||||
{coreViewModel.translations.homePage.serverAddress}
|
{coreViewModel.translations.homePage.serverAddress}
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
list="previous-connection-list"
|
list="previous-connection-list"
|
||||||
placeholder={
|
placeholder={
|
||||||
coreViewModel.translations.homePage
|
coreViewModel.translations.homePage
|
||||||
.serverAddressPlaceholder
|
.serverAddressPlaceholder
|
||||||
}
|
}
|
||||||
bind:value={connectionViewModel.serverAddressInput}
|
bind:value={connectionViewModel.serverAddressInput}
|
||||||
on:enter={connectionViewModel.connect}
|
on:enter={connectionViewModel.connect}
|
||||||
></input>
|
></input>
|
||||||
<datalist
|
<datalist
|
||||||
hidden
|
hidden
|
||||||
id="previous-connection-list"
|
id="previous-connection-list"
|
||||||
children:append={[
|
children:append={[
|
||||||
connectionViewModel.previousAddresses,
|
connectionViewModel.previousAddresses,
|
||||||
StringToOption,
|
StringToOption,
|
||||||
]}
|
]}
|
||||||
></datalist>
|
></datalist>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="flex-row">
|
<div class="flex-row">
|
||||||
<button
|
<button
|
||||||
class="danger flex justify-center"
|
class="danger flex justify-center"
|
||||||
aria-label={
|
aria-label={
|
||||||
coreViewModel.translations.homePage.disconnectAudioLabel
|
coreViewModel.translations.homePage.disconnectAudioLabel
|
||||||
}
|
}
|
||||||
on:click={connectionViewModel.disconnect}
|
on:click={connectionViewModel.disconnect}
|
||||||
toggle:disabled={connectionViewModel.cannotDisonnect}
|
toggle:disabled={connectionViewModel.cannotDisonnect}
|
||||||
>
|
>
|
||||||
<span class="icon">link_off</span>
|
<span class="icon">link_off</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="flex justify-center"
|
class="flex justify-center"
|
||||||
aria-label={
|
aria-label={
|
||||||
coreViewModel.translations.homePage
|
coreViewModel.translations.homePage
|
||||||
.manageConnectionsAudioLabel
|
.manageConnectionsAudioLabel
|
||||||
}
|
}
|
||||||
on:click={connectionViewModel.showConnectionModal}
|
on:click={connectionViewModel.showConnectionModal}
|
||||||
toggle:disabled={
|
toggle:disabled={
|
||||||
connectionViewModel.hasNoPreviousConnections
|
connectionViewModel.hasNoPreviousConnections
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span class="icon">history</span>
|
<span class="icon">history</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="primary flex justify-center"
|
class="primary flex justify-center"
|
||||||
aria-label={
|
aria-label={
|
||||||
coreViewModel.translations.homePage.connectAudioLabel
|
coreViewModel.translations.homePage.connectAudioLabel
|
||||||
}
|
}
|
||||||
on:click={connectionViewModel.connect}
|
on:click={connectionViewModel.connect}
|
||||||
toggle:disabled={connectionViewModel.cannotConnect}
|
toggle:disabled={connectionViewModel.cannotConnect}
|
||||||
>
|
>
|
||||||
<span class="icon">link</span>
|
<span class="icon">link</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr></hr>
|
<hr></hr>
|
||||||
|
|
||||||
<label class="tile flex-no">
|
<label class="tile flex-no">
|
||||||
<span class="icon">account_circle</span>
|
<span class="icon">account_circle</span>
|
||||||
<div>
|
<div>
|
||||||
<span>
|
<span>
|
||||||
{coreViewModel.translations.homePage.yourNameLabel}
|
{coreViewModel.translations.homePage.yourNameLabel}
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
placeholder={
|
placeholder={
|
||||||
coreViewModel.translations.homePage
|
coreViewModel.translations.homePage
|
||||||
.yourNamePlaceholder
|
.yourNamePlaceholder
|
||||||
}
|
}
|
||||||
bind:value={settingsViewModel.usernameInput}
|
bind:value={settingsViewModel.usernameInput}
|
||||||
on:enter={settingsViewModel.setName}
|
on:enter={settingsViewModel.setName}
|
||||||
></input>
|
></input>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<div class="flex-row justify-end">
|
<div class="flex-row justify-end">
|
||||||
<button
|
<button
|
||||||
class="width-50"
|
class="width-50"
|
||||||
on:click={settingsViewModel.setName}
|
on:click={settingsViewModel.setName}
|
||||||
toggle:disabled={settingsViewModel.cannotSetName}
|
toggle:disabled={settingsViewModel.cannotSetName}
|
||||||
aria-label={
|
aria-label={
|
||||||
coreViewModel.translations.homePage
|
coreViewModel.translations.homePage
|
||||||
.setNameButtonAudioLabel
|
.setNameButtonAudioLabel
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{coreViewModel.translations.general.setButton}
|
{coreViewModel.translations.general.setButton}
|
||||||
<span class="icon">check</span>
|
<span class="icon">check</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr></hr>
|
<hr></hr>
|
||||||
|
|
||||||
{HomePageButton(
|
{HomePageButton(
|
||||||
settingsViewModel.showSettingsModal,
|
settingsViewModel.showSettingsModal,
|
||||||
coreViewModel.translations.homePage.settingsButton,
|
coreViewModel.translations.homePage.settingsButton,
|
||||||
"settings",
|
"settings",
|
||||||
)}
|
)}
|
||||||
{HomePageButton(
|
{HomePageButton(
|
||||||
fileTransferViewModel.showDirectionSelectionModal,
|
fileTransferViewModel.showDirectionSelectionModal,
|
||||||
coreViewModel.translations.homePage.transferDataButton,
|
coreViewModel.translations.homePage.transferDataButton,
|
||||||
"sync_alt",
|
"sync_alt",
|
||||||
)}
|
)}
|
||||||
{HomePageButton(
|
{HomePageButton(
|
||||||
storageViewModel.showStorageModal,
|
storageViewModel.showStorageModal,
|
||||||
coreViewModel.translations.homePage.manageStorageButton,
|
coreViewModel.translations.homePage.manageStorageButton,
|
||||||
"hard_drive",
|
"hard_drive",
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="primary"
|
class="primary"
|
||||||
on:click={coreViewModel.update}
|
on:click={coreViewModel.update}
|
||||||
toggle:hidden={coreViewModel.noUpdateAvailable}
|
toggle:hidden={coreViewModel.noUpdateAvailable}
|
||||||
>
|
>
|
||||||
<span
|
<span subscribe:innerText={coreViewModel.updateText}></span>
|
||||||
subscribe:innerText={coreViewModel.updateText}
|
<span class="icon">update</span>
|
||||||
></span>
|
</button>
|
||||||
<span class="icon">update</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="mobile-only">
|
<div class="mobile-only">
|
||||||
<hr></hr>
|
<hr></hr>
|
||||||
|
|
||||||
<div class="flex-row justify-end">
|
<div class="flex-row justify-end">
|
||||||
<button class="ghost width-50" on:click={scrollToChat}>
|
<button class="ghost width-50" on:click={scrollToChat}>
|
||||||
{coreViewModel.translations.homePage.scrollToChatButton}
|
{coreViewModel.translations.homePage.scrollToChatButton}
|
||||||
<span class="icon">arrow_forward</span>
|
<span class="icon">arrow_forward</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const chatSection = (
|
const chatSection = (
|
||||||
<div id="chat-section">
|
<div id="chat-section">
|
||||||
<h2>{coreViewModel.translations.homePage.chatsHeadline}</h2>
|
<h2>{coreViewModel.translations.homePage.chatsHeadline}</h2>
|
||||||
|
|
||||||
<div class="flex-row width-input">
|
<div class="flex-row width-input">
|
||||||
<input
|
<input
|
||||||
placeholder={
|
placeholder={
|
||||||
coreViewModel.translations.homePage.addChatPlaceholder
|
coreViewModel.translations.homePage.addChatPlaceholder
|
||||||
}
|
}
|
||||||
aria-label={
|
aria-label={
|
||||||
coreViewModel.translations.homePage.addChatAudioLabel
|
coreViewModel.translations.homePage.addChatAudioLabel
|
||||||
}
|
}
|
||||||
bind:value={chatListViewModel.newChatPrimaryChannel}
|
bind:value={chatListViewModel.newChatPrimaryChannel}
|
||||||
on:enter={chatListViewModel.createChat}
|
on:enter={chatListViewModel.createChat}
|
||||||
></input>
|
></input>
|
||||||
<button
|
<button
|
||||||
class="primary"
|
class="primary"
|
||||||
aria-label={
|
aria-label={
|
||||||
coreViewModel.translations.homePage.addChatButton
|
coreViewModel.translations.homePage.addChatButton
|
||||||
}
|
}
|
||||||
on:click={chatListViewModel.createChat}
|
on:click={chatListViewModel.createChat}
|
||||||
toggle:disabled={chatListViewModel.cannotCreateChat}
|
toggle:disabled={chatListViewModel.cannotCreateChat}
|
||||||
>
|
>
|
||||||
<span class="icon">add</span>
|
<span class="icon">add</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
id="chat-grid"
|
id="chat-grid"
|
||||||
children:append={[
|
children:append={[
|
||||||
chatListViewModel.chatViewModels,
|
chatListViewModel.chatViewModels,
|
||||||
ChatViewModelToChatEntry,
|
ChatViewModelToChatEntry,
|
||||||
]}
|
]}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// methods
|
// methods
|
||||||
function scrollToChat() {
|
function scrollToChat() {
|
||||||
chatSection.scrollIntoView();
|
chatSection.scrollIntoView();
|
||||||
}
|
}
|
||||||
|
|
||||||
// final
|
// final
|
||||||
return (
|
return (
|
||||||
<article id="home-page">
|
<article id="home-page">
|
||||||
<div>
|
<div>
|
||||||
{overviewSection}
|
{overviewSection}
|
||||||
{chatSection}
|
{chatSection}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+790
-786
File diff suppressed because it is too large
Load Diff
@@ -105,11 +105,11 @@ export default class ChatListViewModel {
|
|||||||
|
|
||||||
// utility
|
// utility
|
||||||
getDisplayName = (fullChannel: string): string => {
|
getDisplayName = (fullChannel: string): string => {
|
||||||
for (const vm of this.chatViewModels.value) {
|
for (const vm of this.chatViewModels.value) {
|
||||||
const fullReference = vm.chatModel.id;
|
const fullReference = vm.chatModel.id;
|
||||||
if (fullReference == fullChannel) return vm.chatModel.info.name;
|
if (fullReference == fullChannel) return vm.chatModel.info.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
return fullChannel;
|
return fullChannel;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ export default class ChatMessageViewModel {
|
|||||||
body: React.State<string> = new React.State("");
|
body: React.State<string> = new React.State("");
|
||||||
inlineReply: ChatMessageViewModel | undefined = undefined;
|
inlineReply: ChatMessageViewModel | undefined = undefined;
|
||||||
replies: React.MapState<ChatMessageViewModel> = new React.MapState();
|
replies: React.MapState<ChatMessageViewModel> = new React.MapState();
|
||||||
replyCount: React.State<number> = React.createProxyState([this.replies], ()=>this.replies.value.size);
|
replyCount: React.State<number> = React.createProxyState(
|
||||||
|
[this.replies],
|
||||||
|
() => this.replies.value.size,
|
||||||
|
);
|
||||||
status: React.State<ChatMessageStatuses | any> = new React.State<any>(
|
status: React.State<ChatMessageStatuses | any> = new React.State<any>(
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
@@ -100,106 +103,119 @@ export default class ChatMessageViewModel {
|
|||||||
};
|
};
|
||||||
|
|
||||||
toggleHiding = (): void => {
|
toggleHiding = (): void => {
|
||||||
let hideForReactions: boolean = false;
|
let hideForReactions: boolean = false;
|
||||||
let hideForReplyView: boolean = false;
|
let hideForReplyView: boolean = false;
|
||||||
|
|
||||||
// reactions
|
// reactions
|
||||||
const reactionFilter = this.messagePageViewModel.reactionFilter.value;
|
const reactionFilter = this.messagePageViewModel.reactionFilter.value;
|
||||||
if (reactionFilter == undefined) {
|
if (reactionFilter == undefined) {
|
||||||
hideForReactions = false;
|
hideForReactions = false;
|
||||||
} else {
|
} else {
|
||||||
let count: number = 0;
|
let count: number = 0;
|
||||||
switch (reactionFilter) {
|
switch (reactionFilter) {
|
||||||
case ReactionSymbols.ThumbsUp: {
|
case ReactionSymbols.ThumbsUp: {
|
||||||
count = this.reactionsThumbsUpCount.value;
|
count = this.reactionsThumbsUpCount.value;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ReactionSymbols.Check: {
|
case ReactionSymbols.Check: {
|
||||||
count = this.reactionsCheckCount.value;
|
count = this.reactionsCheckCount.value;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ReactionSymbols.Stop: {
|
case ReactionSymbols.Stop: {
|
||||||
count = this.reactionsStopCount.value;
|
count = this.reactionsStopCount.value;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ReactionSymbols.Attention: {
|
case ReactionSymbols.Attention: {
|
||||||
count = this.reactionsAttentionCount.value;
|
count = this.reactionsAttentionCount.value;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ReactionSymbols.DoubleAttention: {
|
case ReactionSymbols.DoubleAttention: {
|
||||||
count = this.reactionsDoubleAttentionCount.value;
|
count = this.reactionsDoubleAttentionCount.value;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ReactionSymbols.Question: {
|
case ReactionSymbols.Question: {
|
||||||
count = this.reactionsQuestionCount.value;
|
count = this.reactionsQuestionCount.value;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
hideForReactions = count == 0;
|
hideForReactions = count == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// reply
|
// reply
|
||||||
const selectedMessage = this.messagePageViewModel.replyViewSelectedMessage.value;
|
const selectedMessage =
|
||||||
if (selectedMessage == undefined) hideForReplyView = false;
|
this.messagePageViewModel.replyViewSelectedMessage.value;
|
||||||
else if (selectedMessage.chatMessage.id == this.chatMessage.id) hideForReplyView = false;
|
if (selectedMessage == undefined) hideForReplyView = false;
|
||||||
else if (this.inlineReply != undefined && selectedMessage.chatMessage.id == this.inlineReply.chatMessage.id) hideForReplyView = false;
|
else if (selectedMessage.chatMessage.id == this.chatMessage.id)
|
||||||
else hideForReplyView = true;
|
hideForReplyView = false;
|
||||||
|
else if (
|
||||||
|
this.inlineReply != undefined &&
|
||||||
|
selectedMessage.chatMessage.id == this.inlineReply.chatMessage.id
|
||||||
|
)
|
||||||
|
hideForReplyView = false;
|
||||||
|
else hideForReplyView = true;
|
||||||
|
|
||||||
this.isHidden.value = hideForReactions || hideForReplyView;
|
this.isHidden.value = hideForReactions || hideForReplyView;
|
||||||
}
|
};
|
||||||
|
|
||||||
// reactions
|
// reactions
|
||||||
handleReaction = (reaction: ChatMessageReaction): void => {
|
handleReaction = (reaction: ChatMessageReaction): void => {
|
||||||
if (reaction.isDeleting) {
|
if (reaction.isDeleting) {
|
||||||
this.allReactions.remove(reaction.sender);
|
this.allReactions.remove(reaction.sender);
|
||||||
} else {
|
} else {
|
||||||
this.allReactions.set(reaction.sender, reaction);
|
this.allReactions.set(reaction.sender, reaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reaction.sender != this.coreViewModel.settingsModel.username)
|
if (reaction.sender != this.coreViewModel.settingsModel.username)
|
||||||
return;
|
return;
|
||||||
this.userReaction.value = reaction.isDeleting
|
this.userReaction.value = reaction.isDeleting
|
||||||
? undefined
|
? undefined
|
||||||
: reaction.content;
|
: reaction.content;
|
||||||
};
|
};
|
||||||
|
|
||||||
sendReaction = (content: ReactionSymbols, isDeleting: boolean): void => {
|
sendReaction = (content: ReactionSymbols, isDeleting: boolean): void => {
|
||||||
this.messagePageViewModel.sendReaction(
|
this.messagePageViewModel.sendReaction(
|
||||||
this.chatMessage.id,
|
this.chatMessage.id,
|
||||||
content,
|
content,
|
||||||
isDeleting,
|
isDeleting,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// load
|
// load
|
||||||
loadData = (): void => {
|
loadData = (): void => {
|
||||||
this.channel = this.chatMessage.channel;
|
this.channel = this.chatMessage.channel;
|
||||||
this.sender = this.chatMessage.sender;
|
this.sender = this.chatMessage.sender;
|
||||||
this.dateSent = new Date(this.chatMessage.dateSent).toLocaleString();
|
this.dateSent = new Date(this.chatMessage.dateSent).toLocaleString();
|
||||||
this.body.value = this.chatMessage.body;
|
this.body.value = this.chatMessage.body;
|
||||||
this.status.value = this.chatMessage.status;
|
this.status.value = this.chatMessage.status;
|
||||||
|
|
||||||
if (this.chatMessage.inlineReplyId) {
|
if (this.chatMessage.inlineReplyId) {
|
||||||
this.inlineReply =
|
this.inlineReply =
|
||||||
this.messagePageViewModel.chatMessageViewModels.value.get(
|
this.messagePageViewModel.chatMessageViewModels.value.get(
|
||||||
this.chatMessage.inlineReplyId,
|
this.chatMessage.inlineReplyId,
|
||||||
);
|
);
|
||||||
this.inlineReply?.replies.set(this.chatMessage.id, this);
|
this.inlineReply?.replies.set(this.chatMessage.id, this);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// init
|
// init
|
||||||
constructor(
|
constructor(
|
||||||
public readonly coreViewModel: CoreViewModel,
|
public readonly coreViewModel: CoreViewModel,
|
||||||
public readonly messagePageViewModel: MessagePageViewModel,
|
public readonly messagePageViewModel: MessagePageViewModel,
|
||||||
chatMessage: ChatMessage,
|
chatMessage: ChatMessage,
|
||||||
sentByUser: boolean,
|
sentByUser: boolean,
|
||||||
) {
|
) {
|
||||||
this.chatMessage = chatMessage;
|
this.chatMessage = chatMessage;
|
||||||
this.sentByUser = sentByUser;
|
this.sentByUser = sentByUser;
|
||||||
this.loadData();
|
this.loadData();
|
||||||
|
|
||||||
React.bulkSubscribe([this.messagePageViewModel.reactionFilter, this.messagePageViewModel.replyViewSelectedMessage, this.allReactions], this.toggleHiding);
|
React.bulkSubscribe(
|
||||||
this.toggleHiding();
|
[
|
||||||
|
this.messagePageViewModel.reactionFilter,
|
||||||
|
this.messagePageViewModel.replyViewSelectedMessage,
|
||||||
|
this.allReactions,
|
||||||
|
],
|
||||||
|
this.toggleHiding,
|
||||||
|
);
|
||||||
|
this.toggleHiding();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+123
-120
@@ -31,7 +31,7 @@ export default class ChatViewModel extends ContextHost<ChatPageTypes> {
|
|||||||
// state
|
// state
|
||||||
displayedColor: React.State<Colors> = new React.State<any>(Colors.Standard);
|
displayedColor: React.State<Colors> = new React.State<any>(Colors.Standard);
|
||||||
selectedPage: React.State<ChatPageTypes> = new React.State<any>(
|
selectedPage: React.State<ChatPageTypes> = new React.State<any>(
|
||||||
ChatPageTypes.Messages,
|
ChatPageTypes.Messages,
|
||||||
);
|
);
|
||||||
pageContexts = new React.MapState<Context>();
|
pageContexts = new React.MapState<Context>();
|
||||||
|
|
||||||
@@ -42,186 +42,189 @@ export default class ChatViewModel extends ContextHost<ChatPageTypes> {
|
|||||||
|
|
||||||
// context
|
// context
|
||||||
get isOpen(): boolean {
|
get isOpen(): boolean {
|
||||||
return this.chatListViewModel.selectedChat.value == this;
|
return this.chatListViewModel.selectedChat.value == this;
|
||||||
}
|
}
|
||||||
|
|
||||||
get contextSelection(): ChatPageTypes {
|
get contextSelection(): ChatPageTypes {
|
||||||
return this.selectedPage.value;
|
return this.selectedPage.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// view
|
// view
|
||||||
open = (): void => {
|
open = (): void => {
|
||||||
this.coreViewModel.context = this;
|
this.coreViewModel.context = this;
|
||||||
this.chatListViewModel.openChat(this);
|
this.chatListViewModel.openChat(this);
|
||||||
//this.selectedPage.callSubscriptions();
|
//this.selectedPage.callSubscriptions();
|
||||||
};
|
};
|
||||||
|
|
||||||
openPage = (page: ChatPageTypes): void => {
|
openPage = (page: ChatPageTypes): void => {
|
||||||
this.closeCurrentContext();
|
this.closeCurrentContext();
|
||||||
this.selectedPage.value = page;
|
this.selectedPage.value = page;
|
||||||
};
|
};
|
||||||
|
|
||||||
setColor = (color: Colors): void => {
|
setColor = (color: Colors): void => {
|
||||||
this.setDisplayedColor(color);
|
this.setDisplayedColor(color);
|
||||||
this.chatModel.setColor(color);
|
this.chatModel.setColor(color);
|
||||||
};
|
};
|
||||||
|
|
||||||
setDisplayedColor = (color: Colors): void => {
|
setDisplayedColor = (color: Colors): void => {
|
||||||
this.displayedColor.value = color;
|
this.displayedColor.value = color;
|
||||||
};
|
};
|
||||||
|
|
||||||
resetColor = (): void => {
|
resetColor = (): void => {
|
||||||
this.displayedColor.value = this.settingsPageViewModel.color.value;
|
this.displayedColor.value = this.settingsPageViewModel.color.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
updateIndex = (): void => {
|
updateIndex = (): void => {
|
||||||
const index: number =
|
const index: number =
|
||||||
this.chatListViewModel.chatIndexManager.getIndex(this);
|
this.chatListViewModel.chatIndexManager.getIndex(this);
|
||||||
this.index.value = index;
|
this.index.value = index;
|
||||||
};
|
};
|
||||||
|
|
||||||
// load
|
// load
|
||||||
loadPageSelection = (): void => {
|
loadPageSelection = (): void => {
|
||||||
const path: string[] = StorageModel.getPath(
|
const path: string[] = StorageModel.getPath(
|
||||||
StorageModelSubPaths.Chat,
|
StorageModelSubPaths.Chat,
|
||||||
filePaths.chat.lastUsedPage(this.chatModel.id),
|
filePaths.chat.lastUsedPage(this.chatModel.id),
|
||||||
);
|
);
|
||||||
const lastUsedPage: string | null =
|
const lastUsedPage: string | null =
|
||||||
this.coreViewModel.storageModel.read(path);
|
this.coreViewModel.storageModel.read(path);
|
||||||
if (lastUsedPage != null) {
|
if (lastUsedPage != null) {
|
||||||
this.openPage(lastUsedPage as ChatPageTypes);
|
this.openPage(lastUsedPage as ChatPageTypes);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.selectedPage.subscribeSilent((newPage) => {
|
this.selectedPage.subscribeSilent((newPage) => {
|
||||||
this.coreViewModel.storageModel.write(path, newPage);
|
this.coreViewModel.storageModel.write(path, newPage);
|
||||||
this.resetColor();
|
this.resetColor();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
loadInfo = (): void => {
|
loadInfo = (): void => {
|
||||||
this.updateReadStatus();
|
this.updateReadStatus();
|
||||||
this.taskBoardSuggestions.set(CALENDAR_EVENT_BOARD_ID, [
|
this.taskBoardSuggestions.set(CALENDAR_EVENT_BOARD_ID, [
|
||||||
CALENDAR_EVENT_BOARD_ID,
|
CALENDAR_EVENT_BOARD_ID,
|
||||||
this.coreViewModel.translations.chatPage.calendar.eventsBoard,
|
this.coreViewModel.translations.chatPage.calendar.eventsBoard,
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
updateReadStatus = (): void => {
|
updateReadStatus = (): void => {
|
||||||
if (
|
if (
|
||||||
this.chatListViewModel.selectedChat.value == this &&
|
this.chatListViewModel.selectedChat.value == this &&
|
||||||
this.selectedPage.value == ChatPageTypes.Messages
|
this.selectedPage.value == ChatPageTypes.Messages
|
||||||
) {
|
) {
|
||||||
this.chatModel.setReadStatus(false);
|
this.chatModel.setReadStatus(false);
|
||||||
}
|
}
|
||||||
this.hasUnreadMessages.value = this.chatModel.info.hasUnreadMessages;
|
this.hasUnreadMessages.value = this.chatModel.info.hasUnreadMessages;
|
||||||
};
|
};
|
||||||
|
|
||||||
setReadStatus = (hasUnreadMessages: boolean): void => {
|
setReadStatus = (hasUnreadMessages: boolean): void => {
|
||||||
this.chatModel.setReadStatus(hasUnreadMessages);
|
this.chatModel.setReadStatus(hasUnreadMessages);
|
||||||
this.hasUnreadMessages.value = hasUnreadMessages;
|
this.hasUnreadMessages.value = hasUnreadMessages;
|
||||||
};
|
};
|
||||||
|
|
||||||
subscribeReadStatus = (): void => {
|
subscribeReadStatus = (): void => {
|
||||||
React.createProxyState(
|
React.createProxyState(
|
||||||
[this.selectedPage, this.chatListViewModel.selectedChat],
|
[this.selectedPage, this.chatListViewModel.selectedChat],
|
||||||
() => {
|
() => {
|
||||||
if (this.chatListViewModel.selectedChat.value != this) return;
|
if (this.chatListViewModel.selectedChat.value != this) return;
|
||||||
if (this.selectedPage.value != ChatPageTypes.Messages) return;
|
if (this.selectedPage.value != ChatPageTypes.Messages) return;
|
||||||
this.setReadStatus(false);
|
this.setReadStatus(false);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// exit
|
// exit
|
||||||
close = (): void => {
|
close = (): void => {
|
||||||
this.coreViewModel.closeContext(this.contextId);
|
this.coreViewModel.closeContext(this.contextId);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleContextClose = (): void => {
|
handleContextClose = (): void => {
|
||||||
this.chatListViewModel.closeChat();
|
this.chatListViewModel.closeChat();
|
||||||
};
|
};
|
||||||
|
|
||||||
// init
|
// init
|
||||||
constructor(
|
constructor(
|
||||||
readonly coreViewModel: CoreViewModel,
|
readonly coreViewModel: CoreViewModel,
|
||||||
public readonly chatModel: ChatModel,
|
public readonly chatModel: ChatModel,
|
||||||
public readonly settingsViewModel: SettingsViewModel,
|
public readonly settingsViewModel: SettingsViewModel,
|
||||||
public readonly notificationViewModel: NotificationViewModel,
|
public readonly notificationViewModel: NotificationViewModel,
|
||||||
public readonly connectionViewModel: ConnectionViewModel,
|
public readonly connectionViewModel: ConnectionViewModel,
|
||||||
public readonly chatListViewModel: ChatListViewModel,
|
public readonly chatListViewModel: ChatListViewModel,
|
||||||
) {
|
) {
|
||||||
super("chat", coreViewModel);
|
super("chat", coreViewModel);
|
||||||
|
|
||||||
// page viewModels
|
// page viewModels
|
||||||
this.calendarViewModel = new CalendarPageViewModel(
|
this.calendarViewModel = new CalendarPageViewModel(
|
||||||
coreViewModel,
|
coreViewModel,
|
||||||
this,
|
this,
|
||||||
this.chatModel.fileModel.boardsAndTasksModel.calendarModel,
|
this.chatModel.fileModel.boardsAndTasksModel.calendarModel,
|
||||||
this.chatModel.fileModel.boardsAndTasksModel,
|
this.chatModel.fileModel.boardsAndTasksModel,
|
||||||
);
|
);
|
||||||
this.taskPageViewModel = new TaskPageViewModel(
|
this.taskPageViewModel = new TaskPageViewModel(
|
||||||
this.coreViewModel,
|
this.coreViewModel,
|
||||||
this,
|
this,
|
||||||
this.chatModel.fileModel.boardsAndTasksModel,
|
this.chatModel.fileModel.boardsAndTasksModel,
|
||||||
);
|
);
|
||||||
this.messagePageViewModel = new MessagePageViewModel(
|
this.messagePageViewModel = new MessagePageViewModel(
|
||||||
this.coreViewModel,
|
this.coreViewModel,
|
||||||
this,
|
this,
|
||||||
);
|
);
|
||||||
this.settingsPageViewModel = new SettingsPageViewModel(
|
this.settingsPageViewModel = new SettingsPageViewModel(
|
||||||
this.coreViewModel,
|
this.coreViewModel,
|
||||||
this,
|
this,
|
||||||
);
|
);
|
||||||
|
|
||||||
// handlers
|
// handlers
|
||||||
chatModel.chatMessageHandlerManager.setHandler(
|
chatModel.chatMessageHandlerManager.setHandler(
|
||||||
this.chatModel.id,
|
this.chatModel.id,
|
||||||
(chatMessage: ChatMessage) => {
|
(chatMessage: ChatMessage) => {
|
||||||
this.messagePageViewModel.showChatMessage(chatMessage);
|
this.messagePageViewModel.showChatMessage(chatMessage);
|
||||||
this.updateReadStatus();
|
this.updateReadStatus();
|
||||||
|
|
||||||
this.notificationViewModel.showNotification(chatMessage);
|
this.notificationViewModel.showNotification(chatMessage);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
chatModel.reactionHandlerManager.setHandler(
|
chatModel.reactionHandlerManager.setHandler(
|
||||||
this.chatModel.id,
|
this.chatModel.id,
|
||||||
(reaction: ChatMessageReaction) => {
|
(reaction: ChatMessageReaction) => {
|
||||||
this.messagePageViewModel.handleReaction(reaction);
|
this.messagePageViewModel.handleReaction(reaction);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
chatModel.changeHandlerManager.setHandler(this.chatModel.id, this.settingsPageViewModel.updateData);
|
chatModel.changeHandlerManager.setHandler(
|
||||||
|
this.chatModel.id,
|
||||||
|
this.settingsPageViewModel.updateData,
|
||||||
|
);
|
||||||
|
|
||||||
// load
|
// load
|
||||||
this.loadPageSelection();
|
this.loadPageSelection();
|
||||||
this.resetColor();
|
this.resetColor();
|
||||||
this.loadInfo();
|
this.loadInfo();
|
||||||
this.subscribeReadStatus();
|
this.subscribeReadStatus();
|
||||||
|
|
||||||
// keystrokes
|
// keystrokes
|
||||||
this.registerKeyStroke(CommonKeys.Home, this.close);
|
this.registerKeyStroke(CommonKeys.Home, this.close);
|
||||||
this.registerKeyStroke("u", () =>
|
this.registerKeyStroke("u", () =>
|
||||||
this.openPage(ChatPageTypes.Messages),
|
this.openPage(ChatPageTypes.Messages),
|
||||||
);
|
);
|
||||||
this.registerKeyStroke("i", () => this.openPage(ChatPageTypes.Tasks));
|
this.registerKeyStroke("i", () => this.openPage(ChatPageTypes.Tasks));
|
||||||
this.registerKeyStroke("o", () =>
|
this.registerKeyStroke("o", () =>
|
||||||
this.openPage(ChatPageTypes.Calendar),
|
this.openPage(ChatPageTypes.Calendar),
|
||||||
);
|
);
|
||||||
this.registerKeyStroke(CommonKeys.Settings, () =>
|
this.registerKeyStroke(CommonKeys.Settings, () =>
|
||||||
this.openPage(ChatPageTypes.Settings),
|
this.openPage(ChatPageTypes.Settings),
|
||||||
);
|
);
|
||||||
|
|
||||||
// context
|
// context
|
||||||
this.chatListViewModel.selectedChat.subscribeSilent(
|
this.chatListViewModel.selectedChat.subscribeSilent(
|
||||||
this.updateContexts,
|
this.updateContexts,
|
||||||
);
|
);
|
||||||
this.selectedPage.subscribeSilent(this.updateContexts);
|
this.selectedPage.subscribeSilent(this.updateContexts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// types
|
// types
|
||||||
export enum ChatPageTypes {
|
export enum ChatPageTypes {
|
||||||
Settings = "settings",
|
Settings = "settings",
|
||||||
Messages = "messages",
|
Messages = "messages",
|
||||||
Tasks = "tasks",
|
Tasks = "tasks",
|
||||||
Calendar = "calendar",
|
Calendar = "calendar",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,19 @@ import ConnectionModel from "../../Model/Global/connectionModel";
|
|||||||
import ChatListModel from "../../Model/Chat/chatListModel";
|
import ChatListModel from "../../Model/Chat/chatListModel";
|
||||||
|
|
||||||
export default class CoreViewModel {
|
export default class CoreViewModel {
|
||||||
swRegistration: ServiceWorkerRegistration|undefined = undefined;
|
swRegistration: ServiceWorkerRegistration | undefined = undefined;
|
||||||
|
|
||||||
version: React.State<string> = new React.State("");
|
version: React.State<string> = new React.State("");
|
||||||
latestVersion: React.State<string> = new React.State("");
|
latestVersion: React.State<string> = new React.State("");
|
||||||
noUpdateAvailable: React.State<boolean> = React.createProxyState([this.latestVersion], () => {
|
noUpdateAvailable: React.State<boolean> = React.createProxyState(
|
||||||
return (this.latestVersion.value == "" || this.latestVersion.value == this.version.value)
|
[this.latestVersion],
|
||||||
})
|
() => {
|
||||||
|
return (
|
||||||
|
this.latestVersion.value == "" ||
|
||||||
|
this.latestVersion.value == this.version.value
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
updateText: React.State<string> = new React.State("");
|
updateText: React.State<string> = new React.State("");
|
||||||
|
|
||||||
translations: Translations;
|
translations: Translations;
|
||||||
@@ -82,11 +88,11 @@ export default class CoreViewModel {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleChron = (): void => {
|
handleChron = (): void => {
|
||||||
const newDate = new Date();
|
const newDate = new Date();
|
||||||
if (this.unwrappedTodayDate.toDateString() != newDate.toDateString())
|
if (this.unwrappedTodayDate.toDateString() != newDate.toDateString())
|
||||||
this.todayDate.value = new Date();
|
this.todayDate.value = new Date();
|
||||||
if (formatTime(newDate) != this.time.value)
|
if (formatTime(newDate) != this.time.value)
|
||||||
this.time.value = formatTime(newDate);
|
this.time.value = formatTime(newDate);
|
||||||
};
|
};
|
||||||
|
|
||||||
// DRAG & DROP
|
// DRAG & DROP
|
||||||
@@ -94,75 +100,79 @@ export default class CoreViewModel {
|
|||||||
|
|
||||||
// OFFLINE MODE & UPDATE
|
// OFFLINE MODE & UPDATE
|
||||||
configureServiceWorker = async (): Promise<void> => {
|
configureServiceWorker = async (): Promise<void> => {
|
||||||
if ("serviceWorker" in navigator) {
|
if ("serviceWorker" in navigator) {
|
||||||
try {
|
try {
|
||||||
this.swRegistration = await navigator.serviceWorker.register(
|
this.swRegistration = await navigator.serviceWorker.register(
|
||||||
"sw.js",
|
"sw.js",
|
||||||
{
|
{
|
||||||
scope: "/",
|
scope: "/",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (this.swRegistration.active) {
|
if (this.swRegistration.active) {
|
||||||
console.log("Service worker active");
|
console.log("Service worker active");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Could not install service worker: ${error}`);
|
console.error(`Could not install service worker: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
checkUpdates = (): void => {
|
checkUpdates = (): void => {
|
||||||
fetch("/version").then(async response => {
|
fetch("/version").then(async (response) => {
|
||||||
this.version.value = await response.text();
|
this.version.value = await response.text();
|
||||||
});
|
});
|
||||||
fetch("/latestVersion").then(async response => {
|
fetch("/latestVersion").then(async (response) => {
|
||||||
this.latestVersion.value = (await response.text()).replace("\n", "");
|
this.latestVersion.value = (await response.text()).replace(
|
||||||
})
|
"\n",
|
||||||
}
|
"",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
update = async (): Promise<void> => {
|
update = async (): Promise<void> => {
|
||||||
if (!this.swRegistration) return;
|
if (!this.swRegistration) return;
|
||||||
await this.swRegistration.update();
|
await this.swRegistration.update();
|
||||||
setTimeout(()=>{
|
setTimeout(() => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
};
|
||||||
|
|
||||||
// init
|
// init
|
||||||
constructor(
|
constructor(
|
||||||
public readonly storageModel: StorageModel,
|
public readonly storageModel: StorageModel,
|
||||||
public readonly settingsModel: SettingsModel,
|
public readonly settingsModel: SettingsModel,
|
||||||
public readonly connectionModel: ConnectionModel,
|
public readonly connectionModel: ConnectionModel,
|
||||||
public readonly chatListModel: ChatListModel,
|
public readonly chatListModel: ChatListModel,
|
||||||
public readonly fileTransferModel: FileTransferModel,
|
public readonly fileTransferModel: FileTransferModel,
|
||||||
) {
|
) {
|
||||||
this.translations =
|
this.translations =
|
||||||
allTranslations[settingsModel.language] || allTranslations.en;
|
allTranslations[settingsModel.language] || allTranslations.en;
|
||||||
|
|
||||||
this.latestVersion.subscribe(latestVersion => {
|
this.latestVersion.subscribe((latestVersion) => {
|
||||||
this.updateText.value = this.translations.homePage.updateButton(latestVersion);
|
this.updateText.value =
|
||||||
});
|
this.translations.homePage.updateButton(latestVersion);
|
||||||
|
});
|
||||||
|
|
||||||
document.body.addEventListener("keydown", this.handleKeyDown);
|
document.body.addEventListener("keydown", this.handleKeyDown);
|
||||||
|
|
||||||
window.onpopstate = () => {
|
window.onpopstate = () => {
|
||||||
if (!this.context) return;
|
if (!this.context) return;
|
||||||
this.closeContext(this.context.contextId, true);
|
this.closeContext(this.context.contextId, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.configureServiceWorker();
|
this.configureServiceWorker();
|
||||||
this.checkUpdates();
|
this.checkUpdates();
|
||||||
|
|
||||||
this.startChron();
|
this.startChron();
|
||||||
this.chronHandlerManager.setHandler(
|
this.chronHandlerManager.setHandler(
|
||||||
"core-view-model",
|
"core-view-model",
|
||||||
this.handleChron,
|
this.handleChron,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// util
|
// util
|
||||||
static checkIsKeystroke(e: KeyboardEvent): boolean {
|
static checkIsKeystroke(e: KeyboardEvent): boolean {
|
||||||
return (e.metaKey || e.altKey) && e.ctrlKey;
|
return (e.metaKey || e.altKey) && e.ctrlKey;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,19 +181,19 @@ export class Context {
|
|||||||
keystrokes = new Map<string, () => void>();
|
keystrokes = new Map<string, () => void>();
|
||||||
|
|
||||||
handleKeystroke = (e: KeyboardEvent): boolean => {
|
handleKeystroke = (e: KeyboardEvent): boolean => {
|
||||||
const fn: (() => void) | undefined = this.keystrokes.get(
|
const fn: (() => void) | undefined = this.keystrokes.get(
|
||||||
e.key.toLowerCase(),
|
e.key.toLowerCase(),
|
||||||
);
|
);
|
||||||
if (!fn) return false;
|
if (!fn) return false;
|
||||||
fn();
|
fn();
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
close = (): void => {};
|
close = (): void => {};
|
||||||
handleContextClose = (fromHistoryEvent: boolean): void => {};
|
handleContextClose = (fromHistoryEvent: boolean): void => {};
|
||||||
|
|
||||||
registerKeyStroke = (key: string, fn: () => void): void => {
|
registerKeyStroke = (key: string, fn: () => void): void => {
|
||||||
this.keystrokes.set(key, fn);
|
this.keystrokes.set(key, fn);
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(public contextDebugDescription: string) {}
|
constructor(public contextDebugDescription: string) {}
|
||||||
@@ -194,45 +204,45 @@ export class ContextHost<T> extends Context {
|
|||||||
currentContext = new React.State<Context | undefined>(undefined);
|
currentContext = new React.State<Context | undefined>(undefined);
|
||||||
|
|
||||||
get isOpen(): boolean {
|
get isOpen(): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
get contextSelection(): T | undefined {
|
get contextSelection(): T | undefined {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
registerContext = (key: T, context: Context): void => {
|
registerContext = (key: T, context: Context): void => {
|
||||||
this.contexts.set(key, context);
|
this.contexts.set(key, context);
|
||||||
};
|
};
|
||||||
|
|
||||||
closeCurrentContext = (): void => {
|
closeCurrentContext = (): void => {
|
||||||
if (this.currentContext.value) {
|
if (this.currentContext.value) {
|
||||||
this.coreViewModel.closeContext(
|
this.coreViewModel.closeContext(
|
||||||
this.currentContext.value.contextId,
|
this.currentContext.value.contextId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.currentContext.value = undefined;
|
this.currentContext.value = undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
updateContexts = (): void => {
|
updateContexts = (): void => {
|
||||||
if (this.isOpen == false) return;
|
if (this.isOpen == false) return;
|
||||||
|
|
||||||
const selection = this.contextSelection;
|
const selection = this.contextSelection;
|
||||||
if (!selection) return;
|
if (!selection) return;
|
||||||
const selectedContext: Context | undefined =
|
const selectedContext: Context | undefined =
|
||||||
this.contexts.get(selection);
|
this.contexts.get(selection);
|
||||||
if (!selectedContext) return;
|
if (!selectedContext) return;
|
||||||
if (selectedContext != this.currentContext.value) {
|
if (selectedContext != this.currentContext.value) {
|
||||||
this.closeCurrentContext();
|
this.closeCurrentContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.coreViewModel.context = selectedContext;
|
this.coreViewModel.context = selectedContext;
|
||||||
this.currentContext.value = selectedContext;
|
this.currentContext.value = selectedContext;
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
contextDebugDescription: string,
|
contextDebugDescription: string,
|
||||||
public coreViewModel: CoreViewModel,
|
public coreViewModel: CoreViewModel,
|
||||||
) {
|
) {
|
||||||
super(contextDebugDescription);
|
super(contextDebugDescription);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import CoreViewModel, { Context } from "./coreViewModel";
|
import CoreViewModel, { Context } from "./coreViewModel";
|
||||||
import * as React from "../../react";
|
import * as React from "../../react";
|
||||||
import { CommonKeys } from "../../View/keystrokes";
|
import { CommonKeys } from "../../View/keystrokes";
|
||||||
|
import { formatISO } from "../../Model/Utility/utility";
|
||||||
import StorageModel, {
|
import StorageModel, {
|
||||||
StorageModelSubPaths,
|
StorageModelSubPaths,
|
||||||
filePaths,
|
filePaths,
|
||||||
} from "../../Model/Global/storageModel";
|
} from "../../Model/Global/storageModel";
|
||||||
import { TransferData } from "../../Model/Global/fileTransferModel";
|
import { TransferData } from "../../Model/Global/fileTransferModel";
|
||||||
import ChatModel from "../../Model/Chat/chatModel";
|
import ChatModel from "../../Model/Chat/chatModel";
|
||||||
import {formatISO} from "../../Model/Utility/utility";
|
|
||||||
|
|
||||||
export default class FileTransferViewModel extends Context {
|
export default class FileTransferViewModel extends Context {
|
||||||
// state
|
// state
|
||||||
@@ -143,7 +143,7 @@ export default class FileTransferViewModel extends Context {
|
|||||||
|
|
||||||
downloadFile = async (): Promise<void> => {
|
downloadFile = async (): Promise<void> => {
|
||||||
if (this.cannotExport.value == true) return;
|
if (this.cannotExport.value == true) return;
|
||||||
const date = formatISO(new Date());
|
const date = formatISO(new Date());
|
||||||
|
|
||||||
const backup: Blob =
|
const backup: Blob =
|
||||||
await this.coreViewModel.fileTransferModel.generateBackup(
|
await this.coreViewModel.fileTransferModel.generateBackup(
|
||||||
|
|||||||
@@ -16,11 +16,9 @@ export default class NotificationViewModel {
|
|||||||
if (this.seenMessageIds.has(message.id)) return;
|
if (this.seenMessageIds.has(message.id)) return;
|
||||||
if (this.chatListViewModel.selectedChat.value == undefined) return;
|
if (this.chatListViewModel.selectedChat.value == undefined) return;
|
||||||
|
|
||||||
const notification: Notification =
|
const notification: Notification = this.createNotification(message);
|
||||||
this.createNotification(message);
|
|
||||||
const currentChat =
|
const currentChat =
|
||||||
this.chatListViewModel.selectedChat.value.chatModel
|
this.chatListViewModel.selectedChat.value.chatModel.id;
|
||||||
.id;
|
|
||||||
const currentPage =
|
const currentPage =
|
||||||
this.chatListViewModel.selectedChat.value.selectedPage.value;
|
this.chatListViewModel.selectedChat.value.selectedPage.value;
|
||||||
if (
|
if (
|
||||||
@@ -39,10 +37,7 @@ export default class NotificationViewModel {
|
|||||||
|
|
||||||
const chat = [
|
const chat = [
|
||||||
...this.chatListViewModel.chatViewModels.value.values(),
|
...this.chatListViewModel.chatViewModels.value.values(),
|
||||||
].find(
|
].find((chat) => chat.chatModel.id == notification.fullChannel);
|
||||||
(chat) =>
|
|
||||||
chat.chatModel.id == notification.fullChannel,
|
|
||||||
);
|
|
||||||
if (!chat) return;
|
if (!chat) return;
|
||||||
chat.open();
|
chat.open();
|
||||||
chat.openPage(ChatPageTypes.Messages);
|
chat.openPage(ChatPageTypes.Messages);
|
||||||
@@ -86,12 +81,12 @@ export default class NotificationViewModel {
|
|||||||
|
|
||||||
// util
|
// util
|
||||||
createNotification(message: ChatMessage): Notification {
|
createNotification(message: ChatMessage): Notification {
|
||||||
const fullChannel = ChatModel.splitChannel(message.channel)[0];
|
const fullChannel = ChatModel.splitChannel(message.channel)[0];
|
||||||
const chat = this.chatListViewModel.getDisplayName(fullChannel);
|
const chat = this.chatListViewModel.getDisplayName(fullChannel);
|
||||||
return {
|
return {
|
||||||
messageId: message.id,
|
messageId: message.id,
|
||||||
chat,
|
chat,
|
||||||
fullChannel,
|
fullChannel,
|
||||||
sender: message.sender,
|
sender: message.sender,
|
||||||
body: message.body,
|
body: message.body,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -53,129 +53,129 @@ export default class SettingsViewModel extends Context {
|
|||||||
|
|
||||||
// view
|
// view
|
||||||
applyTheme = (): void => {
|
applyTheme = (): void => {
|
||||||
let theme: string = this.theme.value;
|
let theme: string = this.theme.value;
|
||||||
|
|
||||||
if (theme == ThemeSettings.System)
|
|
||||||
theme = SettingsViewModel.getSystemTheme();
|
|
||||||
|
|
||||||
switch (theme) {
|
if (theme == ThemeSettings.System)
|
||||||
case ThemeSettings.Dynamic:
|
theme = SettingsViewModel.getSystemTheme();
|
||||||
const scene = this.selectDynamicScene();
|
|
||||||
this.setScene(scene);
|
switch (theme) {
|
||||||
break;
|
case ThemeSettings.Dynamic:
|
||||||
case ThemeSettings.Dark:
|
const scene = this.selectDynamicScene();
|
||||||
this.setScene(DynamicSceneNight);
|
this.setScene(scene);
|
||||||
break;
|
break;
|
||||||
case ThemeSettings.Black:
|
case ThemeSettings.Dark:
|
||||||
this.setScene(DynamicSceneBlack);
|
this.setScene(DynamicSceneNight);
|
||||||
break;
|
break;
|
||||||
default:
|
case ThemeSettings.Black:
|
||||||
this.setScene(DynamicSceneDay);
|
this.setScene(DynamicSceneBlack);
|
||||||
}
|
break;
|
||||||
|
default:
|
||||||
|
this.setScene(DynamicSceneDay);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
selectDynamicScene = (): DynamicScene => {
|
selectDynamicScene = (): DynamicScene => {
|
||||||
let hour = new Date().getHours();
|
let hour = new Date().getHours();
|
||||||
|
|
||||||
if (hour >= 21 || hour <= 6) {
|
if (hour >= 21 || hour <= 6) {
|
||||||
return DynamicSceneNight;
|
return DynamicSceneNight;
|
||||||
} else if (hour > 6 && hour < 10) {
|
} else if (hour > 6 && hour < 10) {
|
||||||
return DynamicSceneSunrise;
|
return DynamicSceneSunrise;
|
||||||
} else if (hour < 14) {
|
} else if (hour < 14) {
|
||||||
return DynamicSceneDay;
|
return DynamicSceneDay;
|
||||||
} else if (hour < 17) {
|
} else if (hour < 17) {
|
||||||
return DynamicSceneAfternoon;
|
return DynamicSceneAfternoon;
|
||||||
} else {
|
} else {
|
||||||
return DynamicSceneSunset;
|
return DynamicSceneSunset;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
setScene = (scene: DynamicScene): void => {
|
setScene = (scene: DynamicScene): void => {
|
||||||
document.body.style.setProperty(
|
document.body.style.setProperty(
|
||||||
"--backdrop-grass-filter",
|
"--backdrop-grass-filter",
|
||||||
`brightness(${scene.brightness})`,
|
`brightness(${scene.brightness})`,
|
||||||
);
|
);
|
||||||
|
|
||||||
function setSkyColor(
|
function setSkyColor(
|
||||||
tone: 1 | 2,
|
tone: 1 | 2,
|
||||||
hue: number,
|
hue: number,
|
||||||
saturation: number,
|
saturation: number,
|
||||||
luma: number,
|
luma: number,
|
||||||
) {
|
) {
|
||||||
document.body.style.setProperty(
|
document.body.style.setProperty(
|
||||||
`--sky-${tone}`,
|
`--sky-${tone}`,
|
||||||
`hsl(${hue}, ${saturation}%, ${luma}%)`,
|
`hsl(${hue}, ${saturation}%, ${luma}%)`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
setSkyColor(1, scene.hue1, scene.saturation, scene.luma);
|
setSkyColor(1, scene.hue1, scene.saturation, scene.luma);
|
||||||
setSkyColor(2, scene.hue2, scene.saturation - 10, scene.luma - 10);
|
setSkyColor(2, scene.hue2, scene.saturation - 10, scene.luma - 10);
|
||||||
|
|
||||||
document.body.setAttribute("theme", scene.baseTheme);
|
document.body.setAttribute("theme", scene.baseTheme);
|
||||||
};
|
};
|
||||||
|
|
||||||
// exit
|
// exit
|
||||||
close = (): void => {
|
close = (): void => {
|
||||||
this.coreViewModel.closeContext(this.contextId);
|
this.coreViewModel.closeContext(this.contextId);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleContextClose = (): void => {
|
handleContextClose = (): void => {
|
||||||
this.isShowingSettingsModal.value = false;
|
this.isShowingSettingsModal.value = false;
|
||||||
if (this.requiresReload.value == true) {
|
if (this.requiresReload.value == true) {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// init
|
// init
|
||||||
constructor(public readonly coreViewModel: CoreViewModel) {
|
constructor(public readonly coreViewModel: CoreViewModel) {
|
||||||
super("settings");
|
super("settings");
|
||||||
|
|
||||||
this.username.value = coreViewModel.settingsModel.username;
|
this.username.value = coreViewModel.settingsModel.username;
|
||||||
this.usernameInput.value = coreViewModel.settingsModel.username;
|
this.usernameInput.value = coreViewModel.settingsModel.username;
|
||||||
this.firstDayOfWeek.value = coreViewModel.settingsModel.firstDayOfWeek;
|
this.firstDayOfWeek.value = coreViewModel.settingsModel.firstDayOfWeek;
|
||||||
this.language.value = coreViewModel.settingsModel.language;
|
this.language.value = coreViewModel.settingsModel.language;
|
||||||
this.theme.value = coreViewModel.settingsModel.theme;
|
this.theme.value = coreViewModel.settingsModel.theme;
|
||||||
|
|
||||||
// subscriptions
|
// subscriptions
|
||||||
this.firstDayOfWeek.subscribe(this.setFirstDayofWeek);
|
this.firstDayOfWeek.subscribe(this.setFirstDayofWeek);
|
||||||
this.language.subscribeSilent((newValue) => {
|
this.language.subscribeSilent((newValue) => {
|
||||||
this.coreViewModel.settingsModel.setLanguage(newValue);
|
this.coreViewModel.settingsModel.setLanguage(newValue);
|
||||||
this.requiresReload.value = true;
|
this.requiresReload.value = true;
|
||||||
});
|
});
|
||||||
this.theme.subscribeSilent((newValue) => {
|
this.theme.subscribeSilent((newValue) => {
|
||||||
this.coreViewModel.settingsModel.setTheme(newValue);
|
this.coreViewModel.settingsModel.setTheme(newValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
// set theme
|
// set theme
|
||||||
this.theme.subscribe(() => {
|
this.theme.subscribe(() => {
|
||||||
this.applyTheme();
|
this.applyTheme();
|
||||||
});
|
});
|
||||||
SettingsViewModel.generateThemeMedia().addEventListener("change", () =>
|
SettingsViewModel.generateThemeMedia().addEventListener("change", () =>
|
||||||
this.applyTheme(),
|
this.applyTheme(),
|
||||||
);
|
);
|
||||||
const seconds = new Date().getSeconds();
|
const seconds = new Date().getSeconds();
|
||||||
const secondsUntilNewMinute = 60 - seconds;
|
const secondsUntilNewMinute = 60 - seconds;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setInterval(this.applyTheme, 1000 * 60);
|
setInterval(this.applyTheme, 1000 * 60);
|
||||||
}, secondsUntilNewMinute);
|
}, secondsUntilNewMinute);
|
||||||
|
|
||||||
// keystrokes
|
// keystrokes
|
||||||
this.registerKeyStroke(CommonKeys.CloseOrCancel, this.close);
|
this.registerKeyStroke(CommonKeys.CloseOrCancel, this.close);
|
||||||
}
|
}
|
||||||
|
|
||||||
static generateThemeMedia(): MediaQueryList {
|
static generateThemeMedia(): MediaQueryList {
|
||||||
return window.matchMedia("(prefers-color-scheme: dark)");
|
return window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
}
|
}
|
||||||
|
|
||||||
static getSystemTheme(): string {
|
static getSystemTheme(): string {
|
||||||
const media = SettingsViewModel.generateThemeMedia();
|
const media = SettingsViewModel.generateThemeMedia();
|
||||||
return media.matches == true ? ThemeSettings.Dark : ThemeSettings.Light;
|
return media.matches == true ? ThemeSettings.Dark : ThemeSettings.Light;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SettingsModalPages {
|
export enum SettingsModalPages {
|
||||||
Appearance,
|
Appearance,
|
||||||
Regional,
|
Regional,
|
||||||
Info,
|
Info,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DynamicScene {
|
export interface DynamicScene {
|
||||||
|
|||||||
@@ -50,15 +50,17 @@ export default class StorageViewModel extends Context {
|
|||||||
};
|
};
|
||||||
|
|
||||||
calculateUsage = (): void => {
|
calculateUsage = (): void => {
|
||||||
this.occupiedSpaceMB.value = this.coreViewModel.storageModel.calculateUsage();
|
this.occupiedSpaceMB.value =
|
||||||
this.maximumSpaceMB.value = this.coreViewModel.storageModel.determineCapacity();
|
this.coreViewModel.storageModel.calculateUsage();
|
||||||
}
|
this.maximumSpaceMB.value =
|
||||||
|
this.coreViewModel.storageModel.determineCapacity();
|
||||||
|
};
|
||||||
|
|
||||||
// view
|
// view
|
||||||
showStorageModal = (): void => {
|
showStorageModal = (): void => {
|
||||||
this.coreViewModel.context = this;
|
this.coreViewModel.context = this;
|
||||||
this.isShowingStorageModal.value = true;
|
this.isShowingStorageModal.value = true;
|
||||||
this.calculateUsage();
|
this.calculateUsage();
|
||||||
};
|
};
|
||||||
|
|
||||||
// exit
|
// exit
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ import ChatViewModel, { ChatPageTypes } from "../Chat/chatViewModel";
|
|||||||
import * as React from "../../react";
|
import * as React from "../../react";
|
||||||
import { Colors } from "../../colors";
|
import { Colors } from "../../colors";
|
||||||
import { CommonKeys } from "../../View/keystrokes";
|
import { CommonKeys } from "../../View/keystrokes";
|
||||||
|
import { PinchToZoomData } from "../../Model/Utility/utility";
|
||||||
import BoardsAndTasksModel, {
|
import BoardsAndTasksModel, {
|
||||||
BoardInfoFileContent,
|
BoardInfoFileContent,
|
||||||
TaskFileContent,
|
TaskFileContent,
|
||||||
} from "../../Model/Files/boardsAndTasksModel";
|
} from "../../Model/Files/boardsAndTasksModel";
|
||||||
import {PinchToZoomData} from "../../Model/Utility/utility";
|
|
||||||
|
|
||||||
export default class BoardViewModel extends TaskContainingPageViewModel {
|
export default class BoardViewModel extends TaskContainingPageViewModel {
|
||||||
// state
|
// state
|
||||||
@@ -34,7 +34,11 @@ export default class BoardViewModel extends TaskContainingPageViewModel {
|
|||||||
new React.ListState();
|
new React.ListState();
|
||||||
isFilterActive: React.State<boolean>;
|
isFilterActive: React.State<boolean>;
|
||||||
|
|
||||||
pinchToZoomData: React.State<PinchToZoomData> = new React.State({zoom: 1, x: 0, y: 0});
|
pinchToZoomData: React.State<PinchToZoomData> = new React.State({
|
||||||
|
zoom: 1,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
});
|
||||||
|
|
||||||
// paths
|
// paths
|
||||||
getBasePath = (): string[] => {
|
getBasePath = (): string[] => {
|
||||||
|
|||||||
@@ -4,11 +4,19 @@ import CoreViewModel from "../Global/coreViewModel";
|
|||||||
import ChatViewModel, { ChatPageTypes } from "../Chat/chatViewModel";
|
import ChatViewModel, { ChatPageTypes } from "../Chat/chatViewModel";
|
||||||
import * as React from "../../react";
|
import * as React from "../../react";
|
||||||
import { CommonKeys } from "../../View/keystrokes";
|
import { CommonKeys } from "../../View/keystrokes";
|
||||||
|
import {
|
||||||
|
formatISO,
|
||||||
|
formatISOFromParts,
|
||||||
|
formatMonthStringFromParts,
|
||||||
|
formatTime,
|
||||||
|
isoToDateString,
|
||||||
|
isoToMonthString,
|
||||||
|
padZero,
|
||||||
|
} from "../../Model/Utility/utility";
|
||||||
import CalendarModel, { MonthGrid } from "../../Model/Files/calendarModel";
|
import CalendarModel, { MonthGrid } from "../../Model/Files/calendarModel";
|
||||||
import BoardsAndTasksModel, {
|
import BoardsAndTasksModel, {
|
||||||
TaskFileContent,
|
TaskFileContent,
|
||||||
} from "../../Model/Files/boardsAndTasksModel";
|
} from "../../Model/Files/boardsAndTasksModel";
|
||||||
import {formatISO, formatISOFromParts, formatMonthStringFromParts, formatTime, isoToDateString, isoToMonthString, padZero} from "../../Model/Utility/utility";
|
|
||||||
|
|
||||||
export const CALENDAR_EVENT_BOARD_ID = "events";
|
export const CALENDAR_EVENT_BOARD_ID = "events";
|
||||||
|
|
||||||
@@ -22,7 +30,11 @@ export default class CalendarPageViewModel extends TaskContainingPageViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get selectedDateString(): string {
|
get selectedDateString(): string {
|
||||||
return formatISOFromParts(this.selectedYear.value, this.selectedMonth.value, this.selectedDate.value);
|
return formatISOFromParts(
|
||||||
|
this.selectedYear.value,
|
||||||
|
this.selectedMonth.value,
|
||||||
|
this.selectedDate.value,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// paths
|
// paths
|
||||||
@@ -39,8 +51,8 @@ export default class CalendarPageViewModel extends TaskContainingPageViewModel {
|
|||||||
monthGrid: React.State<
|
monthGrid: React.State<
|
||||||
MonthGrid<React.MapState<TaskViewModel>> | undefined
|
MonthGrid<React.MapState<TaskViewModel>> | undefined
|
||||||
> = new React.State<any>(undefined);
|
> = new React.State<any>(undefined);
|
||||||
|
|
||||||
nextTask = new React.State<TaskViewModel|undefined>(undefined);
|
nextTask = new React.State<TaskViewModel | undefined>(undefined);
|
||||||
|
|
||||||
// methods
|
// methods
|
||||||
createEvent = (): void => {
|
createEvent = (): void => {
|
||||||
@@ -65,9 +77,7 @@ export default class CalendarPageViewModel extends TaskContainingPageViewModel {
|
|||||||
};
|
};
|
||||||
|
|
||||||
getEventsForDate = (): React.MapState<TaskViewModel> | undefined => {
|
getEventsForDate = (): React.MapState<TaskViewModel> | undefined => {
|
||||||
const paddedDate: string = padZero(
|
const paddedDate: string = padZero(this.selectedDate.toString(), 2);
|
||||||
this.selectedDate.toString(), 2
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.monthGrid.value == undefined) {
|
if (this.monthGrid.value == undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -82,9 +92,7 @@ export default class CalendarPageViewModel extends TaskContainingPageViewModel {
|
|||||||
): React.MapState<TaskViewModel> | null => {
|
): React.MapState<TaskViewModel> | null => {
|
||||||
if (this.monthGrid.value == null) return null;
|
if (this.monthGrid.value == null) return null;
|
||||||
|
|
||||||
const date: string = isoToDateString(
|
const date: string = isoToDateString(taskFileContent.date ?? "");
|
||||||
taskFileContent.date ?? "",
|
|
||||||
);
|
|
||||||
return this.monthGrid.value.days[date];
|
return this.monthGrid.value.days[date];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -152,11 +160,7 @@ export default class CalendarPageViewModel extends TaskContainingPageViewModel {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleDrop = (year: string, month: string, date: string): void => {
|
handleDrop = (year: string, month: string, date: string): void => {
|
||||||
const ISOString: string = formatISOFromParts(
|
const ISOString: string = formatISOFromParts(year, month, date);
|
||||||
year,
|
|
||||||
month,
|
|
||||||
date,
|
|
||||||
);
|
|
||||||
|
|
||||||
const draggedObject: any = this.coreViewModel.draggedObject.value;
|
const draggedObject: any = this.coreViewModel.draggedObject.value;
|
||||||
if (draggedObject instanceof TaskViewModel == false) return;
|
if (draggedObject instanceof TaskViewModel == false) return;
|
||||||
@@ -165,37 +169,34 @@ export default class CalendarPageViewModel extends TaskContainingPageViewModel {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleChron = (): void => {
|
handleChron = (): void => {
|
||||||
this.updateMonthGrid();
|
this.updateMonthGrid();
|
||||||
this.updateNextTask();
|
this.updateNextTask();
|
||||||
}
|
};
|
||||||
|
|
||||||
updateMonthGrid = (): void => {
|
updateMonthGrid = (): void => {
|
||||||
const today = formatISO(this.coreViewModel.todayDate.value)
|
const today = formatISO(this.coreViewModel.todayDate.value);
|
||||||
if (
|
if (this.currentTodayDate == today) return;
|
||||||
this.currentTodayDate == today
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
this.loadMonthTasks();
|
this.loadMonthTasks();
|
||||||
this.currentTodayDate = today;
|
this.currentTodayDate = today;
|
||||||
this.selectedDate.callSubscriptions();
|
this.selectedDate.callSubscriptions();
|
||||||
};
|
};
|
||||||
|
|
||||||
updateNextTask = (): void => {
|
updateNextTask = (): void => {
|
||||||
if (this.selectedDateString != this.currentTodayDate) {
|
if (this.selectedDateString != this.currentTodayDate) {
|
||||||
this.nextTask.value = undefined;
|
this.nextTask.value = undefined;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tasks = this.getEventsForDate();
|
const tasks = this.getEventsForDate();
|
||||||
if (tasks == undefined) return;
|
if (tasks == undefined) return;
|
||||||
const taskArray =[...tasks.value.values()];
|
const taskArray = [...tasks.value.values()];
|
||||||
const sorted = taskArray.sort((a, b) => a.index.value - b.index.value);
|
const sorted = taskArray.sort((a, b) => a.index.value - b.index.value);
|
||||||
for (const task of sorted) {
|
for (const task of sorted) {
|
||||||
if (task.time.value <= this.coreViewModel.time.value) continue;
|
if (task.time.value <= this.coreViewModel.time.value) continue;
|
||||||
this.nextTask.value = task;
|
this.nextTask.value = task;
|
||||||
break;
|
break;
|
||||||
};
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// load
|
// load
|
||||||
loadMonthTasks = (): void => {
|
loadMonthTasks = (): void => {
|
||||||
@@ -217,8 +218,7 @@ export default class CalendarPageViewModel extends TaskContainingPageViewModel {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadData = (): void => {
|
loadData = (): void => {};
|
||||||
};
|
|
||||||
|
|
||||||
// init
|
// init
|
||||||
constructor(
|
constructor(
|
||||||
@@ -237,9 +237,9 @@ export default class CalendarPageViewModel extends TaskContainingPageViewModel {
|
|||||||
React.bulkSubscribe([this.selectedYear, this.selectedMonth], () => {
|
React.bulkSubscribe([this.selectedYear, this.selectedMonth], () => {
|
||||||
this.loadMonthTasks();
|
this.loadMonthTasks();
|
||||||
});
|
});
|
||||||
this.selectedDate.subscribeSilent(()=>{
|
this.selectedDate.subscribeSilent(() => {
|
||||||
this.updateNextTask();
|
this.updateNextTask();
|
||||||
})
|
});
|
||||||
|
|
||||||
// handlers
|
// handlers
|
||||||
boardsAndTasksModel.taskHandlerManager.setHandler(
|
boardsAndTasksModel.taskHandlerManager.setHandler(
|
||||||
|
|||||||
@@ -16,9 +16,17 @@ export default class MessagePageViewModel extends Context {
|
|||||||
new React.MapState();
|
new React.MapState();
|
||||||
filteredMessageViewModels: React.ListState<ChatMessageViewModel> =
|
filteredMessageViewModels: React.ListState<ChatMessageViewModel> =
|
||||||
new React.ListState();
|
new React.ListState();
|
||||||
replyViewSelectedMessage = new React.State<ChatMessageViewModel|undefined>(undefined);
|
replyViewSelectedMessage = new React.State<
|
||||||
isReplyViewActive: React.State<boolean> = React.createProxyState([this.replyViewSelectedMessage], ()=>this.replyViewSelectedMessage.value != undefined);
|
ChatMessageViewModel | undefined
|
||||||
isReplyViewInactive = React.createProxyState([this.isReplyViewActive], ()=>!this.isReplyViewActive.value);
|
>(undefined);
|
||||||
|
isReplyViewActive: React.State<boolean> = React.createProxyState(
|
||||||
|
[this.replyViewSelectedMessage],
|
||||||
|
() => this.replyViewSelectedMessage.value != undefined,
|
||||||
|
);
|
||||||
|
isReplyViewInactive = React.createProxyState(
|
||||||
|
[this.isReplyViewActive],
|
||||||
|
() => !this.isReplyViewActive.value,
|
||||||
|
);
|
||||||
searchViewModel: SearchViewModel<ChatMessageViewModel>;
|
searchViewModel: SearchViewModel<ChatMessageViewModel>;
|
||||||
isFilterModalOpen = new React.State<boolean>(false);
|
isFilterModalOpen = new React.State<boolean>(false);
|
||||||
reactionFilter = new React.State<ReactionSymbols | undefined>(undefined);
|
reactionFilter = new React.State<ReactionSymbols | undefined>(undefined);
|
||||||
@@ -39,193 +47,194 @@ export default class MessagePageViewModel extends Context {
|
|||||||
if (this.cannotSendMessage.value == true) return;
|
if (this.cannotSendMessage.value == true) return;
|
||||||
|
|
||||||
const idPromise = this.sendMessageFromBody(this.composingMessage.value);
|
const idPromise = this.sendMessageFromBody(this.composingMessage.value);
|
||||||
this.composingMessage.value = "";
|
this.composingMessage.value = "";
|
||||||
|
|
||||||
const id = await idPromise;
|
const id = await idPromise;
|
||||||
if (id && this.reactionFilter.value != undefined) {
|
if (id && this.reactionFilter.value != undefined) {
|
||||||
this.sendReaction(id, this.reactionFilter.value, false);
|
this.sendReaction(id, this.reactionFilter.value, false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
sendMessageFromBody = (body: string): Promise<string|false>=> {
|
sendMessageFromBody = (body: string): Promise<string | false> => {
|
||||||
let replyId: string | undefined = undefined;
|
let replyId: string | undefined = undefined;
|
||||||
if (this.replyingMessage.value) {
|
if (this.replyingMessage.value) {
|
||||||
replyId = this.replyingMessage.value.chatMessage.id;
|
replyId = this.replyingMessage.value.chatMessage.id;
|
||||||
}
|
}
|
||||||
const id = this.chatViewModel.chatModel.sendMessage(body, replyId);
|
const id = this.chatViewModel.chatModel.sendMessage(body, replyId);
|
||||||
|
|
||||||
if (this.replyViewSelectedMessage.value == undefined)
|
if (this.replyViewSelectedMessage.value == undefined)
|
||||||
this.replyingMessage.value = undefined;
|
this.replyingMessage.value = undefined;
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
};
|
};
|
||||||
|
|
||||||
decryptMessage = async (
|
decryptMessage = async (
|
||||||
messageViewModel: ChatMessageViewModel,
|
messageViewModel: ChatMessageViewModel,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const chatMessage: ChatMessage = messageViewModel.chatMessage;
|
const chatMessage: ChatMessage = messageViewModel.chatMessage;
|
||||||
await this.chatViewModel.chatModel.decryptMessage(chatMessage);
|
await this.chatViewModel.chatModel.decryptMessage(chatMessage);
|
||||||
this.chatViewModel.chatModel.addMessage(chatMessage);
|
this.chatViewModel.chatModel.addMessage(chatMessage);
|
||||||
messageViewModel.loadData();
|
messageViewModel.loadData();
|
||||||
};
|
};
|
||||||
|
|
||||||
sendReaction = (
|
sendReaction = (
|
||||||
messageId: string,
|
messageId: string,
|
||||||
content: ReactionSymbols,
|
content: ReactionSymbols,
|
||||||
isDeleting: boolean,
|
isDeleting: boolean,
|
||||||
): void => {
|
): void => {
|
||||||
this.chatViewModel.chatModel.sendReaction(
|
this.chatViewModel.chatModel.sendReaction(
|
||||||
messageId,
|
messageId,
|
||||||
content,
|
content,
|
||||||
isDeleting,
|
isDeleting,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
setReply = (
|
setReply = (
|
||||||
chatMessageViewModel: ChatMessageViewModel | undefined,
|
chatMessageViewModel: ChatMessageViewModel | undefined,
|
||||||
setFocus: boolean = true,
|
setFocus: boolean = true,
|
||||||
): void => {
|
): void => {
|
||||||
this.replyingMessage.value = chatMessageViewModel;
|
this.replyingMessage.value = chatMessageViewModel;
|
||||||
if (setFocus == false || chatMessageViewModel == undefined) return;
|
if (setFocus == false || chatMessageViewModel == undefined) return;
|
||||||
this.setFocus();
|
this.setFocus();
|
||||||
};
|
};
|
||||||
|
|
||||||
resetReply = (setFocus: boolean = true): void => {
|
resetReply = (setFocus: boolean = true): void => {
|
||||||
this.replyingMessage.value = undefined;
|
this.replyingMessage.value = undefined;
|
||||||
if (setFocus == false) return;
|
if (setFocus == false) return;
|
||||||
this.setFocus();
|
this.setFocus();
|
||||||
};
|
};
|
||||||
|
|
||||||
// view
|
// view
|
||||||
showChatMessage = (chatMessage: ChatMessage): void => {
|
showChatMessage = (chatMessage: ChatMessage): void => {
|
||||||
const chatMessageViewModel = new ChatMessageViewModel(
|
const chatMessageViewModel = new ChatMessageViewModel(
|
||||||
this.coreViewModel,
|
this.coreViewModel,
|
||||||
this,
|
this,
|
||||||
chatMessage,
|
chatMessage,
|
||||||
chatMessage.sender ==
|
chatMessage.sender ==
|
||||||
this.chatViewModel.settingsViewModel.username.value,
|
this.chatViewModel.settingsViewModel.username.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
const existingChatMessageViewModel: ChatMessageViewModel | undefined =
|
const existingChatMessageViewModel: ChatMessageViewModel | undefined =
|
||||||
this.chatMessageViewModels.value.get(chatMessage.id);
|
this.chatMessageViewModels.value.get(chatMessage.id);
|
||||||
if (existingChatMessageViewModel != undefined) {
|
if (existingChatMessageViewModel != undefined) {
|
||||||
existingChatMessageViewModel.body.value = chatMessage.body;
|
existingChatMessageViewModel.body.value = chatMessage.body;
|
||||||
existingChatMessageViewModel.status.value = chatMessage.status;
|
existingChatMessageViewModel.status.value = chatMessage.status;
|
||||||
} else {
|
} else {
|
||||||
this.chatMessageViewModels.set(
|
this.chatMessageViewModels.set(
|
||||||
chatMessage.id,
|
chatMessage.id,
|
||||||
chatMessageViewModel,
|
chatMessageViewModel,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
handleReaction = (reaction: ChatMessageReaction): void => {
|
handleReaction = (reaction: ChatMessageReaction): void => {
|
||||||
const messageViewModel: ChatMessageViewModel | undefined =
|
const messageViewModel: ChatMessageViewModel | undefined =
|
||||||
this.chatMessageViewModels.value.get(reaction.messageId);
|
this.chatMessageViewModels.value.get(reaction.messageId);
|
||||||
if (messageViewModel == undefined) return;
|
if (messageViewModel == undefined) return;
|
||||||
messageViewModel.handleReaction(reaction);
|
messageViewModel.handleReaction(reaction);
|
||||||
};
|
};
|
||||||
|
|
||||||
showFilterModal = (): void => {
|
showFilterModal = (): void => {
|
||||||
this.isFilterModalOpen.value = true;
|
this.isFilterModalOpen.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
hideFilterModal = (): void => {
|
hideFilterModal = (): void => {
|
||||||
this.isFilterModalOpen.value = false;
|
this.isFilterModalOpen.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
revokeReactionFilter = (persist: boolean = true): void => {
|
revokeReactionFilter = (persist: boolean = true): void => {
|
||||||
this.reactionFilter.value = undefined;
|
this.reactionFilter.value = undefined;
|
||||||
if (persist) this.chatViewModel.chatModel.storeFilter("");
|
if (persist) this.chatViewModel.chatModel.storeFilter("");
|
||||||
};
|
};
|
||||||
|
|
||||||
setReactionFilter = (content: ReactionSymbols): void => {
|
setReactionFilter = (content: ReactionSymbols): void => {
|
||||||
this.reactionFilter.value = content;
|
this.reactionFilter.value = content;
|
||||||
this.chatViewModel.chatModel.storeFilter(content);
|
this.chatViewModel.chatModel.storeFilter(content);
|
||||||
};
|
};
|
||||||
|
|
||||||
resetFilter = (): void => {
|
resetFilter = (): void => {
|
||||||
this.revokeReactionFilter();
|
this.revokeReactionFilter();
|
||||||
this.searchViewModel.search("");
|
this.searchViewModel.search("");
|
||||||
};
|
};
|
||||||
|
|
||||||
setReplyView = (message: ChatMessageViewModel): void => {
|
setReplyView = (message: ChatMessageViewModel): void => {
|
||||||
this.replyViewSelectedMessage.value = message;
|
this.replyViewSelectedMessage.value = message;
|
||||||
this.revokeReactionFilter(false);
|
this.revokeReactionFilter(false);
|
||||||
this.setReply(message, false);
|
this.setReply(message, false);
|
||||||
}
|
};
|
||||||
|
|
||||||
resetReplyView = (): void => {
|
resetReplyView = (): void => {
|
||||||
this.replyViewSelectedMessage.value = undefined;
|
this.replyViewSelectedMessage.value = undefined;
|
||||||
this.resetReply(false);
|
this.resetReply(false);
|
||||||
this.restoreFilter();
|
this.restoreFilter();
|
||||||
}
|
};
|
||||||
|
|
||||||
setFocus = (): void => {
|
setFocus = (): void => {
|
||||||
this.focusSetter.callSubscriptions();
|
this.focusSetter.callSubscriptions();
|
||||||
};
|
};
|
||||||
|
|
||||||
// load
|
// load
|
||||||
loadData = (): void => {
|
loadData = (): void => {
|
||||||
this.chatMessageViewModels.clear();
|
this.chatMessageViewModels.clear();
|
||||||
for (const chatMessage of this.chatViewModel.chatModel.messages) {
|
for (const chatMessage of this.chatViewModel.chatModel.messages) {
|
||||||
this.showChatMessage(chatMessage);
|
this.showChatMessage(chatMessage);
|
||||||
}
|
}
|
||||||
for (const reaction of this.chatViewModel.chatModel.reactions) {
|
for (const reaction of this.chatViewModel.chatModel.reactions) {
|
||||||
this.handleReaction(reaction);
|
this.handleReaction(reaction);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
restoreFilter = (): void => {
|
restoreFilter = (): void => {
|
||||||
const previousFilter = this.chatViewModel.chatModel.getFilter() as any;
|
const previousFilter = this.chatViewModel.chatModel.getFilter() as any;
|
||||||
if (Object.values(ReactionSymbols).includes(previousFilter)) this.setReactionFilter(previousFilter);
|
if (Object.values(ReactionSymbols).includes(previousFilter))
|
||||||
}
|
this.setReactionFilter(previousFilter);
|
||||||
|
};
|
||||||
|
|
||||||
// init
|
// init
|
||||||
constructor(
|
constructor(
|
||||||
public readonly coreViewModel: CoreViewModel,
|
public readonly coreViewModel: CoreViewModel,
|
||||||
public readonly chatViewModel: ChatViewModel,
|
public readonly chatViewModel: ChatViewModel,
|
||||||
) {
|
) {
|
||||||
super("message-page");
|
super("message-page");
|
||||||
this.restoreFilter();
|
this.restoreFilter();
|
||||||
|
|
||||||
// states
|
// states
|
||||||
this.cannotSendMessage = React.createProxyState(
|
this.cannotSendMessage = React.createProxyState(
|
||||||
[
|
[
|
||||||
this.chatViewModel.settingsViewModel.username,
|
this.chatViewModel.settingsViewModel.username,
|
||||||
this.composingMessage,
|
this.composingMessage,
|
||||||
],
|
],
|
||||||
() =>
|
() =>
|
||||||
this.chatViewModel.settingsViewModel.username.value == "" ||
|
this.chatViewModel.settingsViewModel.username.value == "" ||
|
||||||
this.composingMessage.value == "",
|
this.composingMessage.value == "",
|
||||||
);
|
);
|
||||||
|
|
||||||
this.searchViewModel = new SearchViewModel(
|
this.searchViewModel = new SearchViewModel(
|
||||||
this.chatMessageViewModels,
|
this.chatMessageViewModels,
|
||||||
this.filteredMessageViewModels,
|
this.filteredMessageViewModels,
|
||||||
(chatMessageViewModel) => [chatMessageViewModel.body.value],
|
(chatMessageViewModel) => [chatMessageViewModel.body.value],
|
||||||
new React.ListState(),
|
new React.ListState(),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.isFilterActive = React.createProxyState(
|
this.isFilterActive = React.createProxyState(
|
||||||
[this.searchViewModel.appliedQuery, this.reactionFilter],
|
[this.searchViewModel.appliedQuery, this.reactionFilter],
|
||||||
() =>
|
() =>
|
||||||
this.searchViewModel.appliedQuery.value != "" ||
|
this.searchViewModel.appliedQuery.value != "" ||
|
||||||
this.reactionFilter.value != undefined,
|
this.reactionFilter.value != undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
// keystrokes
|
// keystrokes
|
||||||
this.registerKeyStroke(CommonKeys.Filter, this.showFilterModal);
|
this.registerKeyStroke(CommonKeys.Filter, this.showFilterModal);
|
||||||
this.registerKeyStroke(CommonKeys.CloseOrCancel, () => {
|
this.registerKeyStroke(CommonKeys.CloseOrCancel, () => {
|
||||||
if (this.isFilterModalOpen.value == true) {
|
if (this.isFilterModalOpen.value == true) {
|
||||||
this.hideFilterModal();
|
this.hideFilterModal();
|
||||||
} else {
|
} else {
|
||||||
this.replyingMessage.value = undefined;
|
this.replyingMessage.value = undefined;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.registerKeyStroke(CommonKeys.Reset, this.resetFilter);
|
this.registerKeyStroke(CommonKeys.Reset, this.resetFilter);
|
||||||
this.registerKeyStroke(CommonKeys.Create, this.setFocus);
|
this.registerKeyStroke(CommonKeys.Create, this.setFocus);
|
||||||
|
|
||||||
this.chatViewModel.registerContext(ChatPageTypes.Messages, this);
|
this.chatViewModel.registerContext(ChatPageTypes.Messages, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,8 +30,7 @@ export default class SettingsPageViewModel extends Context {
|
|||||||
);
|
);
|
||||||
cannotSetColor: React.State<boolean> = React.createProxyState(
|
cannotSetColor: React.State<boolean> = React.createProxyState(
|
||||||
[this.color, this.appliedColor],
|
[this.color, this.appliedColor],
|
||||||
() =>
|
() => this.color.value == this.appliedColor.value,
|
||||||
this.color.value == this.appliedColor.value,
|
|
||||||
);
|
);
|
||||||
cannotAddSecondaryChannel: React.State<boolean> = React.createProxyState(
|
cannotAddSecondaryChannel: React.State<boolean> = React.createProxyState(
|
||||||
[this.newSecondaryChannelInput],
|
[this.newSecondaryChannelInput],
|
||||||
@@ -41,11 +40,8 @@ export default class SettingsPageViewModel extends Context {
|
|||||||
|
|
||||||
// methods
|
// methods
|
||||||
setName = (): void => {
|
setName = (): void => {
|
||||||
this.chatViewModel.chatModel.setName(
|
this.chatViewModel.chatModel.setName(this.nameInput.value);
|
||||||
this.nameInput.value,
|
this.name.value = this.chatViewModel.chatModel.info.name;
|
||||||
);
|
|
||||||
this.name.value =
|
|
||||||
this.chatViewModel.chatModel.info.name;
|
|
||||||
|
|
||||||
this.chatViewModel.chatListViewModel.updateIndices();
|
this.chatViewModel.chatListViewModel.updateIndices();
|
||||||
};
|
};
|
||||||
@@ -79,7 +75,7 @@ export default class SettingsPageViewModel extends Context {
|
|||||||
|
|
||||||
applyColor = (): void => {
|
applyColor = (): void => {
|
||||||
this.chatViewModel.setColor(this.color.value);
|
this.chatViewModel.setColor(this.color.value);
|
||||||
this.appliedColor.value = this.color.value;
|
this.appliedColor.value = this.color.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
remove = (): void => {
|
remove = (): void => {
|
||||||
@@ -90,18 +86,17 @@ export default class SettingsPageViewModel extends Context {
|
|||||||
|
|
||||||
// load
|
// load
|
||||||
preloadData = (): void => {
|
preloadData = (): void => {
|
||||||
this.name.value =
|
this.name.value = this.chatViewModel.chatModel.info.name;
|
||||||
this.chatViewModel.chatModel.info.name;
|
|
||||||
|
|
||||||
this.color.value = this.chatViewModel.chatModel.color;
|
this.color.value = this.chatViewModel.chatModel.color;
|
||||||
this.appliedColor.value = this.color.value;
|
this.appliedColor.value = this.color.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
updateData = (): void => {
|
updateData = (): void => {
|
||||||
this.preloadData();
|
this.preloadData();
|
||||||
this.nameInput.value = this.name.value;
|
this.nameInput.value = this.name.value;
|
||||||
this.chatViewModel.resetColor();
|
this.chatViewModel.resetColor();
|
||||||
}
|
};
|
||||||
|
|
||||||
loadData = (): void => {
|
loadData = (): void => {
|
||||||
this.nameInput.value = this.name.value;
|
this.nameInput.value = this.name.value;
|
||||||
|
|||||||
@@ -178,8 +178,13 @@ export default class TaskPageViewModel extends ContextHost<string> {
|
|||||||
|
|
||||||
this.chatViewModel = chatViewModel;
|
this.chatViewModel = chatViewModel;
|
||||||
|
|
||||||
// filter
|
// filter
|
||||||
implementFilter(this.boardViewModels, this.boardMatches, this.boardQuery, (board) => board.name.value);
|
implementFilter(
|
||||||
|
this.boardViewModels,
|
||||||
|
this.boardMatches,
|
||||||
|
this.boardQuery,
|
||||||
|
(board) => board.name.value,
|
||||||
|
);
|
||||||
|
|
||||||
// context
|
// context
|
||||||
this.chatViewModel.registerContext(ChatPageTypes.Tasks, this);
|
this.chatViewModel.registerContext(ChatPageTypes.Tasks, this);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import TaskContainingPageViewModel from "./taskContainingPageViewModel";
|
import TaskContainingPageViewModel from "./taskContainingPageViewModel";
|
||||||
|
import CalendarPageViewModel from "./calendarPageViewModel";
|
||||||
import CoreViewModel, { Context } from "../Global/coreViewModel";
|
import CoreViewModel, { Context } from "../Global/coreViewModel";
|
||||||
import ChatViewModel from "../Chat/chatViewModel";
|
import ChatViewModel from "../Chat/chatViewModel";
|
||||||
import * as React from "../../react";
|
import * as React from "../../react";
|
||||||
@@ -8,7 +9,6 @@ import { localeCompare, padZero } from "../../Model/Utility/utility";
|
|||||||
import BoardsAndTasksModel, {
|
import BoardsAndTasksModel, {
|
||||||
TaskFileContent,
|
TaskFileContent,
|
||||||
} from "../../Model/Files/boardsAndTasksModel";
|
} from "../../Model/Files/boardsAndTasksModel";
|
||||||
import CalendarPageViewModel from "./calendarPageViewModel";
|
|
||||||
|
|
||||||
export default class TaskViewModel extends Context {
|
export default class TaskViewModel extends Context {
|
||||||
// util
|
// util
|
||||||
@@ -200,11 +200,11 @@ export default class TaskViewModel extends Context {
|
|||||||
this.loadTaskData();
|
this.loadTaskData();
|
||||||
this.loadVersionIds();
|
this.loadVersionIds();
|
||||||
|
|
||||||
if (this.containingViewModel instanceof CalendarPageViewModel) {
|
if (this.containingViewModel instanceof CalendarPageViewModel) {
|
||||||
this.containingViewModel.nextTask.subscribe((nextTask)=>{
|
this.containingViewModel.nextTask.subscribe((nextTask) => {
|
||||||
this.isNotNext.value = nextTask != this;
|
this.isNotNext.value = nextTask != this;
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadTaskData = (): void => {
|
loadTaskData = (): void => {
|
||||||
|
|||||||
+2
-3
@@ -10,7 +10,7 @@
|
|||||||
#background-wrapper {
|
#background-wrapper {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0px;
|
inset: 0px;
|
||||||
|
|
||||||
transform: scale(1.2);
|
transform: scale(1.2);
|
||||||
filter: blur(40px);
|
filter: blur(40px);
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ main {
|
|||||||
bottom: -50%;
|
bottom: -50%;
|
||||||
|
|
||||||
background-image: radial-gradient(var(--sky-1), var(--sky-2));
|
background-image: radial-gradient(var(--sky-1), var(--sky-2));
|
||||||
|
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,4 +113,3 @@ body[theme="dark"] #background-wrapper #grass-2 {
|
|||||||
|
|
||||||
padding: var(--content-padding);
|
padding: var(--content-padding);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -80,7 +80,8 @@ body:is([theme="dark"], [theme="black"]) [color]:not([color="standard"]).tile {
|
|||||||
background: hsl(var(--hue), var(--sat), var(--luma), 40%);
|
background: hsl(var(--hue), var(--sat), var(--luma), 40%);
|
||||||
}
|
}
|
||||||
|
|
||||||
body:is([theme="dark"], [theme="black"]) [color]:not([color="standard"]).background {
|
body:is([theme="dark"], [theme="black"])
|
||||||
|
[color]:not([color="standard"]).background {
|
||||||
--luma: var(--background-luma-dark);
|
--luma: var(--background-luma-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +101,8 @@ body [color]:not([color="standard"]).subtle-background {
|
|||||||
background: hsl(var(--hue), 100%, var(--luma), 40%);
|
background: hsl(var(--hue), 100%, var(--luma), 40%);
|
||||||
}
|
}
|
||||||
|
|
||||||
body:is([theme="dark"], [theme="black"]) [color]:not([color="standard"]).subtle-background {
|
body:is([theme="dark"], [theme="black"])
|
||||||
|
[color]:not([color="standard"]).subtle-background {
|
||||||
--luma: var(--subtle-luma-dark);
|
--luma: var(--subtle-luma-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user