This commit is contained in:
marlon-erler
2026-09-26 23:32:38 +02:00
parent ce9f6d89f0
commit 9622c196cd
40 changed files with 2866 additions and 2706 deletions
+283 -274
View File
@@ -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,
} };
+3 -7
View File
@@ -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
View File
@@ -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> = {
+1 -1
View File
@@ -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,
+36 -29
View File
@@ -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
View File
@@ -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;
} }
+4 -1
View File
@@ -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);
+4 -3
View File
@@ -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
View File
@@ -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>
);
} }
+8 -15
View File
@@ -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">
+9 -4
View File
@@ -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))"
+2 -6
View File
@@ -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>
); );
+6 -3
View File
@@ -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>
+19 -11
View File
@@ -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>
);
} }
+20 -11
View File
@@ -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>
); );
} }
+40 -18
View File
@@ -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>
); );
} }
+42 -40
View File
@@ -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> = (
+20 -20
View File
@@ -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>
); );
} }
+7 -4
View File
@@ -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
+95 -92
View File
@@ -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,
);
} }
+65 -70
View File
@@ -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>
); );
} }
+81 -70
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -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;
} };
} }
+97 -81
View File
@@ -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
View File
@@ -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",
} }
+103 -93
View File
@@ -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(
+6 -11
View File
@@ -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,
}; };
+87 -87
View File
@@ -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 {
+6 -4
View File
@@ -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
+6 -2
View File
@@ -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[] => {
+43 -43
View File
@@ -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(
+134 -125
View File
@@ -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);
} }
} }
+11 -16
View File
@@ -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;
+7 -2
View File
@@ -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);
+6 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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);
} }