implement auto reconnect

This commit is contained in:
marlon-erler
2026-04-01 08:29:39 +00:00
parent c52d7b7685
commit 9d400a9929
82 changed files with 8340 additions and 7641 deletions
+1 -1
View File
@@ -827,7 +827,7 @@ body[showing-chat] #chat-page-wrapper {
.file-browser code {
width: 100%;
height: 100%;
font-size: .9rem;
font-size: 0.9rem;
}
@media (min-width: 640px) {
.file-browser > div {
+389 -159
View File
@@ -532,7 +532,10 @@
this.initializeTree();
};
this.removeRecursively = (pathComponents) => {
this.recurse(pathComponents, (path) => this.remove(path, false));
this.recurse(
pathComponents,
(path) => this.remove(path, false)
);
this.initializeTree();
};
this.renameRecursively = (sourcePathComponents, destinationPathComponents) => {
@@ -658,7 +661,9 @@
constructor(storageModel2, settingsModel2, fileModel) {
// paths
this.getBasePath = () => {
return this.fileModel.getModelContainerPath("calendar" /* ModelCalendar */);
return this.fileModel.getModelContainerPath(
"calendar" /* ModelCalendar */
);
};
this.getViewPath = () => {
return [...this.getBasePath(), "view" /* ModelView */];
@@ -806,9 +811,15 @@
};
// handlers
this.handleFileContent = (fileContent) => {
if (checkMatchesObjectStructure(fileContent, BoardInfoFileContentReference) == true) {
if (checkMatchesObjectStructure(
fileContent,
BoardInfoFileContentReference
) == true) {
this.handleBoard(fileContent);
} else if (checkMatchesObjectStructure(fileContent, TaskFileContentReference) == true) {
} else if (checkMatchesObjectStructure(
fileContent,
TaskFileContentReference
) == true) {
this.handleTask(fileContent);
}
};
@@ -897,7 +908,10 @@
return versionIds;
};
this.getLatestTaskFileContent = (taskId) => {
const taskFileContentOrNull = this.fileModel.getLatestFileContent(taskId, TaskFileContentReference);
const taskFileContentOrNull = this.fileModel.getLatestFileContent(
taskId,
TaskFileContentReference
);
return taskFileContentOrNull;
};
this.getSpecificTaskFileContent = (taskId, versionId) => {
@@ -933,10 +947,7 @@
static {
// utility
this.createBoardInfoFileContent = (fileId, name, color) => {
const fileContent = FileModel2.createFileContent(
fileId,
"board-info"
);
const fileContent = FileModel2.createFileContent(fileId, "board-info");
return {
...fileContent,
name,
@@ -1043,7 +1054,10 @@
return fileContentIds[fileContentIds.length - 1];
};
this.getFileContent = (fileId, fileContentName, reference) => {
const filePath = this.getFileContentPath(fileId, fileContentName);
const filePath = this.getFileContentPath(
fileId,
fileContentName
);
const fileContentOrNull = this.storageModel.readStringifiable(
filePath,
reference
@@ -1247,7 +1261,9 @@
this.storageModel.writeStringifiable(messagePath, chatMessage);
this.chatMessageHandlerManager.trigger(chatMessage);
}
this.fileModel.handleStringifiedFileContent(chatMessage.stringifiedFile);
this.fileModel.handleStringifiedFileContent(
chatMessage.stringifiedFile
);
};
this.sendMessage = async (body, fileContent) => {
const senderName = this.settingsModel.username;
@@ -1328,7 +1344,11 @@
this.loadInfo();
this.loadColor();
this.subscribe();
this.fileModel = new FileModel2(this.storageModel, this.settingsModel, this);
this.fileModel = new FileModel2(
this.storageModel,
this.settingsModel,
this
);
}
get secondaryChannels() {
return this.info.secondaryChannels.sort(localeCompare);
@@ -1341,7 +1361,10 @@
const chatMessages = [];
for (const messageId of messageIds) {
const messagePath = this.getMessagePath(messageId);
const chatMessage = this.storageModel.readStringifiable(messagePath, ChatMessageReference);
const chatMessage = this.storageModel.readStringifiable(
messagePath,
ChatMessageReference
);
chatMessages.push(chatMessage);
}
const sorted = chatMessages.sort(
@@ -1378,7 +1401,10 @@
chatMessage.stringifiedFile = stringifiedFile;
}
if (encryptionKey != "") {
chatMessage.body = await encryptString(chatMessage.body, encryptionKey);
chatMessage.body = await encryptString(
chatMessage.body,
encryptionKey
);
chatMessage.stringifiedFile = await encryptString(
chatMessage.stringifiedFile,
encryptionKey
@@ -1463,7 +1489,10 @@
};
// load
this.loadChats = () => {
const chatDir = StorageModel.getPath("chat" /* Chat */, filePaths.chat.base);
const chatDir = StorageModel.getPath(
"chat" /* Chat */,
filePaths.chat.base
);
const chatIds = this.storageModel.list(chatDir);
for (const chatId of chatIds) {
const chatModel = new ChatModel(
@@ -1563,7 +1592,9 @@
) == false) {
this.coreViewModel.taskCategorySuggestions.add(this.category.value);
}
if (this.coreViewModel.taskStatusSuggestions.value.has(this.status.value) == false) {
if (this.coreViewModel.taskStatusSuggestions.value.has(
this.status.value
) == false) {
this.coreViewModel.taskStatusSuggestions.add(this.status.value);
}
};
@@ -1588,14 +1619,15 @@
};
this.deleteTask = () => {
this.close();
this.boardsAndTasksModel.deleteTask(this.task.boardId, this.task.fileId);
this.boardsAndTasksModel.deleteTask(
this.task.boardId,
this.task.fileId
);
this.containingModel.removeTaskFromView(this.task);
};
// load
this.loadVersionIds = () => {
const versionIds = this.boardsAndTasksModel.listTaskVersionIds(
this.task.fileId
);
const versionIds = this.boardsAndTasksModel.listTaskVersionIds(this.task.fileId);
const sortedVersionIds = versionIds.sort(localeCompare).reverse();
this.versionIds.clear();
this.versionIds.add(...sortedVersionIds);
@@ -1810,7 +1842,11 @@
}
};
this.handleDrop = (year, month, date) => {
const ISOString = CalendarModel.getISODateString(year, month, date);
const ISOString = CalendarModel.getISODateString(
year,
month,
date
);
const draggedObject = this.coreViewModel.draggedObject.value;
if (draggedObject instanceof TaskViewModel == false) return;
draggedObject.setDate(ISOString);
@@ -1822,7 +1858,9 @@
this.selectedMonth.value,
() => new MapState()
);
const taskIds = this.calendarModel.listTaskIds(this.monthString);
const taskIds = this.calendarModel.listTaskIds(
this.monthString
);
for (const taskId of taskIds) {
const taskFileContent = this.boardsAndTasksModel.getLatestTaskFileContent(taskId);
if (taskFileContent == null) continue;
@@ -1935,7 +1973,10 @@
existingChatMessageViewModel.body.value = chatMessage.body;
existingChatMessageViewModel.status.value = chatMessage.status;
} else {
this.chatMessageViewModels.set(chatMessage.id, chatMessageViewModel);
this.chatMessageViewModels.set(
chatMessage.id,
chatMessageViewModel
);
}
};
// load
@@ -1947,7 +1988,10 @@
};
this.chatViewModel = chatViewModel;
this.cannotSendMessage = createProxyState(
[this.chatViewModel.settingsViewModel.username, this.composingMessage],
[
this.chatViewModel.settingsViewModel.username,
this.composingMessage
],
() => this.chatViewModel.settingsViewModel.username.value == "" || this.composingMessage.value == ""
);
}
@@ -2116,7 +2160,9 @@
this.filteredTaskViewModels = new ListState();
// paths
this.getBasePath = () => {
return [...this.taskPageViewModel.getBoardViewPath(this.boardInfo.fileId)];
return [
...this.taskPageViewModel.getBoardViewPath(this.boardInfo.fileId)
];
};
this.getLastUsedBoardPath = () => {
return [...this.getBasePath(), "last-used-view" /* LastUsedView */];
@@ -2137,7 +2183,9 @@
this.taskPageViewModel.updateBoard(newBoardInfoFileContent);
};
this.applyColor = () => {
this.taskPageViewModel.chatViewModel.setDisplayedColor(this.color.value);
this.taskPageViewModel.chatViewModel.setDisplayedColor(
this.color.value
);
};
this.deleteBoard = () => {
this.taskPageViewModel.deleteBoard(this.boardInfo);
@@ -3266,7 +3314,10 @@
"select",
{
"bind:value": taskViewModel.selectedVersionId,
"children:append": [taskViewModel.versionIds, VersionIdToOption]
"children:append": [
taskViewModel.versionIds,
VersionIdToOption
]
}
), /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_drop_down"))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("label", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "label"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.task.taskNameLabel), /* @__PURE__ */ createElement("input", { "bind:value": taskViewModel.name }))), /* @__PURE__ */ createElement("label", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "category"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.task.taskBoardLabel), /* @__PURE__ */ createElement(
"select",
@@ -3315,11 +3366,44 @@
StringToOption
]
}
), /* @__PURE__ */ createElement("label", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "priority_high"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.task.taskPriorityLabel), /* @__PURE__ */ createElement("input", { type: "number", "bind:value": taskViewModel.priority }))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("label", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "calendar_month"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.task.taskDateLabel), /* @__PURE__ */ createElement("input", { type: "date", "bind:value": taskViewModel.date }))), /* @__PURE__ */ createElement("label", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "schedule"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.task.taskTimeLabel), /* @__PURE__ */ createElement("input", { type: "time", "bind:value": taskViewModel.time }))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "width-input" }, DangerousActionButton(
), /* @__PURE__ */ createElement("label", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "priority_high"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.task.taskPriorityLabel), /* @__PURE__ */ createElement(
"input",
{
type: "number",
"bind:value": taskViewModel.priority
}
))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("label", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "calendar_month"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.task.taskDateLabel), /* @__PURE__ */ createElement(
"input",
{
type: "date",
"bind:value": taskViewModel.date
}
))), /* @__PURE__ */ createElement("label", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "schedule"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.task.taskTimeLabel), /* @__PURE__ */ createElement(
"input",
{
type: "time",
"bind:value": taskViewModel.time
}
))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "width-input" }, DangerousActionButton(
translations.chatPage.task.deleteTaskButton,
"delete_forever",
taskViewModel.deleteTask
))), /* @__PURE__ */ createElement("div", { class: "flex-row width-100" }, /* @__PURE__ */ createElement("button", { class: "flex", "on:click": taskViewModel.closeAndDiscard }, translations.general.closeButton), /* @__PURE__ */ createElement("button", { class: "flex primary", "on:click": taskViewModel.closeAndSave }, translations.general.saveButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "save")))));
))), /* @__PURE__ */ createElement("div", { class: "flex-row width-100" }, /* @__PURE__ */ createElement(
"button",
{
class: "flex",
"on:click": taskViewModel.closeAndDiscard
},
translations.general.closeButton
), /* @__PURE__ */ createElement(
"button",
{
class: "flex primary",
"on:click": taskViewModel.closeAndSave
},
translations.general.saveButton,
/* @__PURE__ */ createElement("span", { class: "icon" }, "save")
))));
}
// src/View/Components/taskEntry.tsx
@@ -3341,7 +3425,13 @@
"on:click": taskViewModel.open,
"on:dragstart": taskViewModel.dragStart
},
/* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", { class: "ellipsis", "subscribe:innerText": taskViewModel.name }), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement(
/* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement(
"b",
{
class: "ellipsis",
"subscribe:innerText": taskViewModel.name
}
), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement(
"div",
{
class: "grid secondary",
@@ -3384,7 +3474,11 @@
date
);
};
return MonthGrid2(monthGrid, calendarPageViewModel.selectedDate, drop);
return MonthGrid2(
monthGrid,
calendarPageViewModel.selectedDate,
drop
);
}
}
);
@@ -3399,19 +3493,25 @@
if (listState == void 0) {
return /* @__PURE__ */ createElement("div", null);
} else {
const sidePaneContent = createProxyState([listState], () => {
if (listState.value.size == 0) {
return /* @__PURE__ */ createElement("div", { class: "width-100 height-100 flex-column justify-center align-center" }, /* @__PURE__ */ createElement("span", { class: "secondary slide-up" }, translations.chatPage.calendar.noEvents));
} else {
return /* @__PURE__ */ createElement(
"div",
{
class: "flex-column gap slide-up padding-bottom",
"children:append": [listState, TaskViewModelToEntry]
}
);
const sidePaneContent = createProxyState(
[listState],
() => {
if (listState.value.size == 0) {
return /* @__PURE__ */ createElement("div", { class: "width-100 height-100 flex-column justify-center align-center" }, /* @__PURE__ */ createElement("span", { class: "secondary slide-up" }, translations.chatPage.calendar.noEvents));
} else {
return /* @__PURE__ */ createElement(
"div",
{
class: "flex-column gap slide-up padding-bottom",
"children:append": [
listState,
TaskViewModelToEntry
]
}
);
}
}
});
);
return /* @__PURE__ */ createElement(
"div",
{
@@ -3484,13 +3584,25 @@
"on:click": calendarPageViewModel.createEvent
},
/* @__PURE__ */ createElement("span", { class: "icon" }, "add")
))), /* @__PURE__ */ createElement("div", { class: "content padding-0", "children:set": mainContent }))), /* @__PURE__ */ createElement(
))), /* @__PURE__ */ createElement(
"div",
{
class: "content padding-0",
"children:set": mainContent
}
))), /* @__PURE__ */ createElement(
"div",
{
class: "pane-wrapper side background",
"set:color": calendarPageViewModel.chatViewModel.displayedColor
},
/* @__PURE__ */ createElement("div", { class: "pane" }, /* @__PURE__ */ createElement("div", { class: "content", "children:set": sidePaneContentWrapper }))
/* @__PURE__ */ createElement("div", { class: "pane" }, /* @__PURE__ */ createElement(
"div",
{
class: "content",
"children:set": sidePaneContentWrapper
}
))
), /* @__PURE__ */ createElement("div", { "children:set": taskSettingsModal }));
}
@@ -3522,13 +3634,20 @@
// src/View/Modals/chatMessageInfoModal.tsx
function ChatMessageInfoModal(chatMessageViewModel) {
return /* @__PURE__ */ createElement("div", { class: "modal", "toggle:open": chatMessageViewModel.isPresentingInfoModal }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("main", null, /* @__PURE__ */ createElement("h2", null, translations.chatPage.message.messageInfoHeadline), /* @__PURE__ */ createElement("div", { class: "flex-column gap" }, /* @__PURE__ */ createElement("div", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "account_circle"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.message.sentBy), /* @__PURE__ */ createElement("b", { class: "break-word" }, chatMessageViewModel.sender))), /* @__PURE__ */ createElement("div", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "schedule"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.message.timeSent), /* @__PURE__ */ createElement("b", { class: "break-word" }, chatMessageViewModel.dateSent))), /* @__PURE__ */ createElement("div", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "forum"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.message.channel), /* @__PURE__ */ createElement("b", { class: "break-word" }, chatMessageViewModel.channel))), /* @__PURE__ */ createElement("div", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "description"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.message.messageContent), /* @__PURE__ */ createElement(
"b",
return /* @__PURE__ */ createElement(
"div",
{
class: "break-word",
"subscribe:innerText": chatMessageViewModel.body
}
)))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "flex-column gap" }, /* @__PURE__ */ createElement("button", { "on:click": chatMessageViewModel.copyMessage }, translations.chatPage.message.copyMessageButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "content_copy")), /* @__PURE__ */ createElement("button", { "on:click": chatMessageViewModel.resendMessage }, translations.chatPage.message.resendMessageButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "redo")), /* @__PURE__ */ createElement("button", { "on:click": chatMessageViewModel.decryptMessage }, translations.chatPage.message.decryptMessageButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "key")))), /* @__PURE__ */ createElement("button", { "on:click": chatMessageViewModel.hideInfoModal }, translations.general.closeButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "close"))));
class: "modal",
"toggle:open": chatMessageViewModel.isPresentingInfoModal
},
/* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("main", null, /* @__PURE__ */ createElement("h2", null, translations.chatPage.message.messageInfoHeadline), /* @__PURE__ */ createElement("div", { class: "flex-column gap" }, /* @__PURE__ */ createElement("div", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "account_circle"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.message.sentBy), /* @__PURE__ */ createElement("b", { class: "break-word" }, chatMessageViewModel.sender))), /* @__PURE__ */ createElement("div", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "schedule"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.message.timeSent), /* @__PURE__ */ createElement("b", { class: "break-word" }, chatMessageViewModel.dateSent))), /* @__PURE__ */ createElement("div", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "forum"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.message.channel), /* @__PURE__ */ createElement("b", { class: "break-word" }, chatMessageViewModel.channel))), /* @__PURE__ */ createElement("div", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "description"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.message.messageContent), /* @__PURE__ */ createElement(
"b",
{
class: "break-word",
"subscribe:innerText": chatMessageViewModel.body
}
)))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "flex-column gap" }, /* @__PURE__ */ createElement("button", { "on:click": chatMessageViewModel.copyMessage }, translations.chatPage.message.copyMessageButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "content_copy")), /* @__PURE__ */ createElement("button", { "on:click": chatMessageViewModel.resendMessage }, translations.chatPage.message.resendMessageButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "redo")), /* @__PURE__ */ createElement("button", { "on:click": chatMessageViewModel.decryptMessage }, translations.chatPage.message.decryptMessageButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "key")))), /* @__PURE__ */ createElement("button", { "on:click": chatMessageViewModel.hideInfoModal }, translations.general.closeButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "close")))
);
}
// src/View/Components/chatMessage.tsx
@@ -3560,7 +3679,13 @@
class: "body",
"subscribe:innerText": chatMessageViewModel.body
}
), /* @__PURE__ */ createElement("span", { class: "timestamp ellipsis" }, /* @__PURE__ */ createElement("span", { class: "icon", "subscribe:innerText": statusIcon }), chatMessageViewModel.dateSent)), /* @__PURE__ */ createElement("div", { class: "button-container" }, /* @__PURE__ */ createElement(
), /* @__PURE__ */ createElement("span", { class: "timestamp ellipsis" }, /* @__PURE__ */ createElement(
"span",
{
class: "icon",
"subscribe:innerText": statusIcon
}
), chatMessageViewModel.dateSent)), /* @__PURE__ */ createElement("div", { class: "button-container" }, /* @__PURE__ */ createElement(
"button",
{
"on:click": chatMessageViewModel.showInfoModal,
@@ -3872,32 +3997,45 @@
(taskViewModel) => taskViewModel.task,
boardViewModel.filteredTaskViewModels,
(taskViewModels) => {
const viewModel = new TaskCategoryBulkChangeViewModel(taskViewModels, categoryName);
const viewModel = new TaskCategoryBulkChangeViewModel(
taskViewModels,
categoryName
);
function drop() {
boardViewModel.handleDropWithinBoard(categoryName);
}
const view = /* @__PURE__ */ createElement("div", { class: "flex-column flex-no", "on:dragover": allowDrop, "on:drop": drop }, /* @__PURE__ */ createElement("div", { class: "flex-row width-input" }, /* @__PURE__ */ createElement(
"input",
{
placeholder: translations.chatPage.task.renameCategoryInputPlaceholder,
"bind:value": viewModel.inputValue,
"on:enter": viewModel.set
}
), /* @__PURE__ */ createElement(
"button",
{
class: "primary",
"on:click": viewModel.set,
"toggle:disabled": viewModel.cannotSet
},
/* @__PURE__ */ createElement("span", { class: "icon" }, "check")
)), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement(
const view = /* @__PURE__ */ createElement(
"div",
{
class: "kanban-column",
"children:append": [taskViewModels, TaskViewModelToEntry]
}
));
class: "flex-column flex-no",
"on:dragover": allowDrop,
"on:drop": drop
},
/* @__PURE__ */ createElement("div", { class: "flex-row width-input" }, /* @__PURE__ */ createElement(
"input",
{
placeholder: translations.chatPage.task.renameCategoryInputPlaceholder,
"bind:value": viewModel.inputValue,
"on:enter": viewModel.set
}
), /* @__PURE__ */ createElement(
"button",
{
class: "primary",
"on:click": viewModel.set,
"toggle:disabled": viewModel.cannotSet
},
/* @__PURE__ */ createElement("span", { class: "icon" }, "check")
)),
/* @__PURE__ */ createElement("hr", null),
/* @__PURE__ */ createElement(
"div",
{
class: "kanban-column",
"children:append": [taskViewModels, TaskViewModelToEntry]
}
)
);
index.subscribe((newIndex) => {
view.style.order = newIndex;
});
@@ -3908,17 +4046,24 @@
// src/View/Modals/boardSettingsModal.tsx
function BoardSettingsModal(boardViewModel) {
return /* @__PURE__ */ createElement("div", { class: "modal", "toggle:open": boardViewModel.isPresentingSettingsModal }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("main", null, /* @__PURE__ */ createElement("h2", null, translations.chatPage.task.boardSettingsHeadline), /* @__PURE__ */ createElement("label", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "label"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.task.boardNameInputLabel), /* @__PURE__ */ createElement(
"input",
return /* @__PURE__ */ createElement(
"div",
{
"on:enter": boardViewModel.saveSettings,
"bind:value": boardViewModel.name
}
))), /* @__PURE__ */ createElement("hr", null), ColorPicker(boardViewModel.color), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "width-input" }, DangerousActionButton(
translations.chatPage.task.deleteBoardButton,
"delete_forever",
boardViewModel.deleteBoard
))), /* @__PURE__ */ createElement("button", { "on:click": boardViewModel.hideSettings }, translations.general.closeButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "close"))));
class: "modal",
"toggle:open": boardViewModel.isPresentingSettingsModal
},
/* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("main", null, /* @__PURE__ */ createElement("h2", null, translations.chatPage.task.boardSettingsHeadline), /* @__PURE__ */ createElement("label", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "label"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.task.boardNameInputLabel), /* @__PURE__ */ createElement(
"input",
{
"on:enter": boardViewModel.saveSettings,
"bind:value": boardViewModel.name
}
))), /* @__PURE__ */ createElement("hr", null), ColorPicker(boardViewModel.color), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "width-input" }, DangerousActionButton(
translations.chatPage.task.deleteBoardButton,
"delete_forever",
boardViewModel.deleteBoard
))), /* @__PURE__ */ createElement("button", { "on:click": boardViewModel.hideSettings }, translations.general.closeButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "close")))
);
}
// src/View/ChatPages/boardStatusGridPage.tsx
@@ -3978,9 +4123,12 @@
const doesMatchStatus = taskViewModel.task.status == statusName;
if (doesMatchStatus == false) return;
taskViewModelsWithMatchingStatus.add(taskViewModel);
boardViewModel.filteredTaskViewModels.handleRemoval(taskViewModel, () => {
taskViewModelsWithMatchingStatus.remove(taskViewModel);
});
boardViewModel.filteredTaskViewModels.handleRemoval(
taskViewModel,
() => {
taskViewModelsWithMatchingStatus.remove(taskViewModel);
}
);
}
);
const viewModel = new TaskStatusBulkChangeViewModel(
@@ -4015,7 +4163,10 @@
boardViewModel.filteredTaskViewModels,
(taskViewModels) => {
const statusNameConverter = (statusName) => {
const index2 = createPropertyValueIndexState(sortedStatuses, statusName);
const index2 = createPropertyValueIndexState(
sortedStatuses,
statusName
);
return CategoryStatusColumn(
categoryName,
statusName,
@@ -4024,7 +4175,10 @@
taskViewModels
);
};
const viewModel = new TaskCategoryBulkChangeViewModel(taskViewModels, categoryName);
const viewModel = new TaskCategoryBulkChangeViewModel(
taskViewModels,
categoryName
);
const view = /* @__PURE__ */ createElement("div", { class: "flex-row flex-no large-gap" }, /* @__PURE__ */ createElement("div", { class: "property-input-wrapper" }, /* @__PURE__ */ createElement(
"input",
{
@@ -4113,7 +4267,10 @@
{
hidden: true,
id: suggestionId,
"children:append": [searchViewModel.suggestions, StringToOption]
"children:append": [
searchViewModel.suggestions,
StringToOption
]
}
), /* @__PURE__ */ createElement(
"button",
@@ -4129,7 +4286,10 @@
{
class: "grid gap",
style: "grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr))",
"children:append": [searchViewModel.matchingObjects, converter]
"children:append": [
searchViewModel.matchingObjects,
converter
]
}
)), /* @__PURE__ */ createElement("button", { "on:click": close }, translations.general.closeButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "close"))));
}
@@ -4168,7 +4328,9 @@
if (boardViewModel.selectedTaskViewModel.value == void 0) {
return /* @__PURE__ */ createElement("div", null);
} else {
return TaskSettingsModal(boardViewModel.selectedTaskViewModel.value);
return TaskSettingsModal(
boardViewModel.selectedTaskViewModel.value
);
}
}
);
@@ -4248,7 +4410,13 @@
"on:dragover": allowDrop,
"on:drop": boardViewModel.handleDropBetweenBoards
},
/* @__PURE__ */ createElement("span", { class: "shadow", "subscribe:innerText": boardViewModel.name }),
/* @__PURE__ */ createElement(
"span",
{
class: "shadow",
"subscribe:innerText": boardViewModel.name
}
),
/* @__PURE__ */ createElement("b", { "subscribe:innerText": boardViewModel.name })
);
boardViewModel.index.subscribe((newIndex) => {
@@ -4281,49 +4449,58 @@
return BoardPage(selectedBoard);
}
);
return /* @__PURE__ */ createElement("div", { id: "task-page", "toggle:isshowingboard": isShowingBoard, "set:showingboardlist": taskPageViewModel.isShowingBoadList }, /* @__PURE__ */ createElement(
return /* @__PURE__ */ createElement(
"div",
{
id: "board-list",
class: "pane-wrapper side background",
"set:color": taskPageViewModel.chatViewModel.displayedColor
id: "task-page",
"toggle:isshowingboard": isShowingBoard,
"set:showingboardlist": taskPageViewModel.isShowingBoadList
},
/* @__PURE__ */ createElement("div", { class: "pane" }, /* @__PURE__ */ createElement("div", { class: "toolbar" }, /* @__PURE__ */ createElement("div", { class: "flex-row width-input" }, /* @__PURE__ */ createElement(
"input",
{
class: "no-outline",
"bind:value": taskPageViewModel.newBoardNameInput,
"on:enter": taskPageViewModel.createBoard,
placeholder: translations.chatPage.task.newBoardNamePlaceholder
}
), /* @__PURE__ */ createElement(
"button",
{
class: "primary",
"aria-label": translations.chatPage.task.createBoardButtonAudioLabel,
"on:click": taskPageViewModel.createBoard,
"toggle:disabled": taskPageViewModel.cannotCreateBoard
},
/* @__PURE__ */ createElement("span", { class: "icon" }, "add")
))), /* @__PURE__ */ createElement("div", { class: "content" }, /* @__PURE__ */ createElement(
/* @__PURE__ */ createElement(
"div",
{
class: "grid gap",
style: "grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr))",
"children:append": [
taskPageViewModel.boardViewModels,
BoardViewModelToEntry
]
id: "board-list",
class: "pane-wrapper side background",
"set:color": taskPageViewModel.chatViewModel.displayedColor
},
/* @__PURE__ */ createElement("div", { class: "pane" }, /* @__PURE__ */ createElement("div", { class: "toolbar" }, /* @__PURE__ */ createElement("div", { class: "flex-row width-input" }, /* @__PURE__ */ createElement(
"input",
{
class: "no-outline",
"bind:value": taskPageViewModel.newBoardNameInput,
"on:enter": taskPageViewModel.createBoard,
placeholder: translations.chatPage.task.newBoardNamePlaceholder
}
), /* @__PURE__ */ createElement(
"button",
{
class: "primary",
"aria-label": translations.chatPage.task.createBoardButtonAudioLabel,
"on:click": taskPageViewModel.createBoard,
"toggle:disabled": taskPageViewModel.cannotCreateBoard
},
/* @__PURE__ */ createElement("span", { class: "icon" }, "add")
))), /* @__PURE__ */ createElement("div", { class: "content" }, /* @__PURE__ */ createElement(
"div",
{
class: "grid gap",
style: "grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr))",
"children:append": [
taskPageViewModel.boardViewModels,
BoardViewModelToEntry
]
}
)))
),
/* @__PURE__ */ createElement(
"div",
{
id: "board-content",
class: "pane-wrapper",
"children:set": paneContent
}
)))
), /* @__PURE__ */ createElement(
"div",
{
id: "board-content",
class: "pane-wrapper",
"children:set": paneContent
}
));
)
);
}
// src/View/chatPage.tsx
@@ -4399,13 +4576,7 @@
}
}
);
return /* @__PURE__ */ createElement(
"div",
{
id: "chat-page-wrapper",
"children:set": chatPageContent
}
);
return /* @__PURE__ */ createElement("div", { id: "chat-page-wrapper", "children:set": chatPageContent });
}
// src/View/Modals/connectionModal.tsx
@@ -4572,6 +4743,8 @@
var ConnectionModel = class {
// init
constructor(storageModel2) {
this.reconnectInterval = void 0;
this.shouldAttemptReconnect = false;
this.connectionChangeHandlerManager = new HandlerManager();
this.messageHandlerManager = new HandlerManager();
this.messageSentHandlerManager = new HandlerManager();
@@ -4583,11 +4756,22 @@
this.handleConnectionChange = () => {
console.log("connection status:", this.isConnected, this.address);
this.connectionChangeHandlerManager.trigger();
if (this.isConnected == false) return;
if (this.isConnected == false) {
if (this.shouldAttemptReconnect == false) return;
this.reconnectInterval = setInterval(() => {
if (this.isConnected == true) {
clearInterval(this.reconnectInterval);
return;
}
this.reconnect();
}, 5e3);
return;
}
if (this.address == void 0) return;
this.storeAddress(this.address);
this.sendSubscriptionRequest();
this.sendMessagesInOutbox();
this.shouldAttemptReconnect = true;
};
// connection
this.connect = (address) => {
@@ -4600,6 +4784,14 @@
filePaths.connectionModel.reconnectAddress
);
this.storageModel.remove(reconnectAddressPath);
this.shouldAttemptReconnect = false;
};
this.reconnect = () => {
const reconnectAddressPath = this.getReconnectAddressPath();
const reconnectAddress = this.storageModel.read(reconnectAddressPath);
if (reconnectAddress == null) return;
console.log("reconnecting...");
this.connect(reconnectAddress);
};
// mailbox
this.getMailboxPath = (address) => {
@@ -4616,7 +4808,9 @@
};
this.connectMailbox = () => {
if (this.address == void 0) return;
const mailboxId = this.storageModel.read(this.getMailboxPath(this.address));
const mailboxId = this.storageModel.read(
this.getMailboxPath(this.address)
);
console.log("connecting mailbox", mailboxId);
if (mailboxId == null) return this.requestNewMailbox();
this.udn.connectMailbox(mailboxId);
@@ -4742,11 +4936,7 @@
this.udn.onmailboxconnect = (mailboxId) => {
console.log(`using mailbox ${mailboxId}`);
};
const reconnectAddressPath = this.getReconnectAddressPath();
const reconnectAddress = storageModel2.read(reconnectAddressPath);
if (reconnectAddress != null) {
this.connect(reconnectAddress);
}
this.reconnect();
}
// data
get isConnected() {
@@ -4860,7 +5050,9 @@
);
this.filesSentText = createProxyState(
[this.filesSentCount],
() => translations.dataTransferModal.filesSentCount(this.filesSentCount.value)
() => translations.dataTransferModal.filesSentCount(
this.filesSentCount.value
)
);
this.filePathsReceived = new ListState();
this.filesReceivedCount = createProxyState(
@@ -4902,7 +5094,10 @@
},
{
label: translations.dataTransferModal.settingsData,
path: StorageModel.getPath("settings" /* SettingsModel */, [])
path: StorageModel.getPath(
"settings" /* SettingsModel */,
[]
)
}
);
const chatModels = this.chatListModel.chatModels;
@@ -4973,7 +5168,10 @@
// src/View/Modals/dataTransferModal.tsx
function DataTransferModalWrapper(connectionViewModel2, fileTransferViewModel2) {
return /* @__PURE__ */ createElement("div", null, DirectionSelectionModal(connectionViewModel2, fileTransferViewModel2), FileSelectionModal(fileTransferViewModel2), TransferDataDisplayModal(fileTransferViewModel2), TransferDisplayModal(fileTransferViewModel2), TransferDataInputModal(fileTransferViewModel2), DataReceptionModal(fileTransferViewModel2));
return /* @__PURE__ */ createElement("div", null, DirectionSelectionModal(
connectionViewModel2,
fileTransferViewModel2
), FileSelectionModal(fileTransferViewModel2), TransferDataDisplayModal(fileTransferViewModel2), TransferDisplayModal(fileTransferViewModel2), TransferDataInputModal(fileTransferViewModel2), DataReceptionModal(fileTransferViewModel2));
}
function DirectionSelectionModal(connectionViewModel2, fileTransferViewModel2) {
const isPresented = createProxyState(
@@ -4984,7 +5182,14 @@
[connectionViewModel2.isConnected],
() => connectionViewModel2.isConnected.value == false
);
return /* @__PURE__ */ createElement("div", { class: "modal", "toggle:open": isPresented }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("main", null, /* @__PURE__ */ createElement("h2", null, translations.dataTransferModal.transferDataHeadline), /* @__PURE__ */ createElement("p", { class: "error", "toggle:hidden": connectionViewModel2.isConnected }, translations.dataTransferModal.notConnectedError), /* @__PURE__ */ createElement(
return /* @__PURE__ */ createElement("div", { class: "modal", "toggle:open": isPresented }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("main", null, /* @__PURE__ */ createElement("h2", null, translations.dataTransferModal.transferDataHeadline), /* @__PURE__ */ createElement(
"p",
{
class: "error",
"toggle:hidden": connectionViewModel2.isConnected
},
translations.dataTransferModal.notConnectedError
), /* @__PURE__ */ createElement(
"div",
{
class: "flex-column gap content-margin-bottom",
@@ -5031,7 +5236,9 @@
function toggle() {
isSelected.value = !isSelected.value;
}
return /* @__PURE__ */ createElement("button", { class: "tile", "toggle:selected": isSelected, "on:click": toggle }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", { class: "ellipsis" }, fileOption.label), /* @__PURE__ */ createElement("span", { class: "secondary ellipsis" }, StorageModel.pathComponentsToString(...fileOption.path))));
return /* @__PURE__ */ createElement("button", { class: "tile", "toggle:selected": isSelected, "on:click": toggle }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", { class: "ellipsis" }, fileOption.label), /* @__PURE__ */ createElement("span", { class: "secondary ellipsis" }, StorageModel.pathComponentsToString(
...fileOption.path
))));
}
const isPresented = createProxyState(
[fileTransferViewModel2.presentedModal],
@@ -5083,7 +5290,12 @@
{
"subscribe:innerText": fileTransferViewModel2.transferChannel
}
))), /* @__PURE__ */ createElement("div", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "key"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", { class: "secondary" }, translations.dataTransferModal.transferKeyHeadline), /* @__PURE__ */ createElement("b", { "subscribe:innerText": fileTransferViewModel2.transferKey }))))), /* @__PURE__ */ createElement("div", { class: "flex-row width-100" }, /* @__PURE__ */ createElement(
))), /* @__PURE__ */ createElement("div", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "key"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", { class: "secondary" }, translations.dataTransferModal.transferKeyHeadline), /* @__PURE__ */ createElement(
"b",
{
"subscribe:innerText": fileTransferViewModel2.transferKey
}
))))), /* @__PURE__ */ createElement("div", { class: "flex-row width-100" }, /* @__PURE__ */ createElement(
"button",
{
class: "flex",
@@ -5279,7 +5491,9 @@
};
this.storageModel = storageModel2;
this.connectionModel = connectionModel2;
this.connectionModel.messageHandlerManager.addHandler(this.handleMessage);
this.connectionModel.messageHandlerManager.addHandler(
this.handleMessage
);
}
};
var FileDataReference = {
@@ -5399,7 +5613,15 @@
},
/* @__PURE__ */ createElement("span", { class: "icon" }, "sync_alt"),
/* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.homePage.transferDataButton))
), /* @__PURE__ */ createElement("button", { class: "tile flex-no", "on:click": storageViewModel2.showStorageModal }, /* @__PURE__ */ createElement("span", { class: "icon" }, "hard_drive_2"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.homePage.manageStorageButton))), /* @__PURE__ */ createElement("div", { class: "mobile-only" }, /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "flex-row justify-end" }, /* @__PURE__ */ createElement("button", { class: "ghost width-50", "on:click": scrollToChat }, translations.homePage.scrollToChatButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_forward")))));
), /* @__PURE__ */ createElement(
"button",
{
class: "tile flex-no",
"on:click": storageViewModel2.showStorageModal
},
/* @__PURE__ */ createElement("span", { class: "icon" }, "hard_drive_2"),
/* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.homePage.manageStorageButton))
), /* @__PURE__ */ createElement("div", { class: "mobile-only" }, /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "flex-row justify-end" }, /* @__PURE__ */ createElement("button", { class: "ghost width-50", "on:click": scrollToChat }, translations.homePage.scrollToChatButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_forward")))));
const chatSection = /* @__PURE__ */ createElement("div", { id: "chat-section" }, /* @__PURE__ */ createElement("h2", null, translations.homePage.chatsHeadline), /* @__PURE__ */ createElement("div", { class: "flex-row width-input" }, /* @__PURE__ */ createElement(
"input",
{
@@ -5611,17 +5833,23 @@
this.coreViewModel = coreViewModel;
// state
this.isShowingStorageModal = new State(false);
this.selectedPath = new State(PATH_COMPONENT_SEPARATOR);
this.selectedPath = new State(
PATH_COMPONENT_SEPARATOR
);
this.didMakeChanges = new State(false);
this.lastDeletedItemPath = new State("");
// methods
this.getSelectedItemContent = () => {
const path = StorageModel.stringToPathComponents(this.selectedPath.value);
const path = StorageModel.stringToPathComponents(
this.selectedPath.value
);
const content = this.storageModel.read(path);
return (content ?? translations.storage.notAFile) || translations.storage.contentEmpty;
};
this.deleteSelectedItem = () => {
const path = StorageModel.stringToPathComponents(this.selectedPath.value);
const path = StorageModel.stringToPathComponents(
this.selectedPath.value
);
this.lastDeletedItemPath.value = this.selectedPath.value;
this.storageModel.removeRecursively(path);
this.didMakeChanges.value = true;
@@ -5699,9 +5927,7 @@
if (primaryChannel == null) return;
const parsedPriamryChannel = parseOrFallback(primaryChannel);
const chatModel = this.chatListModel.createChat(parsedPriamryChannel);
const secondaryChannelString = getLocalStorageItemAndClear(
storageKeys.secondaryChannels(id)
);
const secondaryChannelString = getLocalStorageItemAndClear(storageKeys.secondaryChannels(id));
if (secondaryChannelString != null) {
const potentialSecondaryChannels = parseArray(
secondaryChannelString
@@ -5744,7 +5970,9 @@
};
chatModel.addMessage(convertedChatMessage);
if (status == "outbox" /* Outbox */) {
this.connectionModel.sendMessageOrStore(convertedChatMessage);
this.connectionModel.sendMessageOrStore(
convertedChatMessage
);
}
}
};
@@ -5789,7 +6017,9 @@
time: potentialVersion.time ?? ""
};
console.log(convertedTaskFileContent);
chatModel.fileModel.handleFileContent(convertedTaskFileContent);
chatModel.fileModel.handleFileContent(
convertedTaskFileContent
);
}
}
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "Comms",
"display": "standalone",
"display": "fullscreen",
"icons": [
{
"src": "icon.png",
+99 -93
View File
@@ -1,5 +1,8 @@
import ChatModel, { ChatMessage } from "./chatModel";
import StorageModel, { StorageModelSubPath, filePaths } from "../Global/storageModel";
import StorageModel, {
StorageModelSubPath,
filePaths,
} from "../Global/storageModel";
import ConnectionModel from "../Global/connectionModel";
import { Message } from "udn-frontend";
@@ -7,104 +10,107 @@ import SettingsModel from "../Global/settingsModel";
import { v4 } from "uuid";
export default class ChatListModel {
storageModel: StorageModel;
settingsModel: SettingsModel;
connectionModel: ConnectionModel;
storageModel: StorageModel;
settingsModel: SettingsModel;
connectionModel: ConnectionModel;
// data
chatModels = new Set<ChatModel>();
// data
chatModels = new Set<ChatModel>();
// handlers
messageHandler = (data: Message): void => {
const channel: string | undefined = data.messageChannel;
const body: string | undefined = data.messageBody;
if (channel == undefined) return;
if (body == undefined) return;
// handlers
messageHandler = (data: Message): void => {
const channel: string | undefined = data.messageChannel;
const body: string | undefined = data.messageBody;
if (channel == undefined) return;
if (body == undefined) return;
this.routeMessageToCorrectChatModel(channel, (chatModel: ChatModel) =>
chatModel.handleMessage(body)
);
};
this.routeMessageToCorrectChatModel(channel, (chatModel: ChatModel) =>
chatModel.handleMessage(body),
);
};
messageSentHandler = (chatMessage: ChatMessage): void => {
const channel: string = chatMessage.channel;
messageSentHandler = (chatMessage: ChatMessage): void => {
const channel: string = chatMessage.channel;
this.routeMessageToCorrectChatModel(channel, (chatModel: ChatModel) =>
chatModel.handleMessageSent(chatMessage)
);
};
this.routeMessageToCorrectChatModel(channel, (chatModel: ChatModel) =>
chatModel.handleMessageSent(chatMessage),
);
};
// methods
routeMessageToCorrectChatModel = (
channel: string,
fn: (chatModel: ChatModel) => void
): void => {
const allChannels = channel.split("/");
// methods
routeMessageToCorrectChatModel = (
channel: string,
fn: (chatModel: ChatModel) => void,
): void => {
const allChannels = channel.split("/");
for (const chatModel of this.chatModels) {
for (const channel of allChannels) {
if (channel != chatModel.info.primaryChannel) continue;
fn(chatModel);
break;
}
for (const chatModel of this.chatModels) {
for (const channel of allChannels) {
if (channel != chatModel.info.primaryChannel) continue;
fn(chatModel);
break;
}
}
};
// storage
addChatModel = (chatModel: ChatModel) => {
this.chatModels.add(chatModel);
};
createChat = (primaryChannel: string): ChatModel => {
const id: string = v4();
const chatModel = new ChatModel(
this.storageModel,
this.connectionModel,
this.settingsModel,
this,
id,
);
chatModel.setPrimaryChannel(primaryChannel);
this.addChatModel(chatModel);
return chatModel;
};
untrackChat = (chat: ChatModel): void => {
this.chatModels.delete(chat);
};
// load
loadChats = (): void => {
const chatDir = StorageModel.getPath(
StorageModelSubPath.Chat,
filePaths.chat.base,
);
const chatIds = this.storageModel.list(chatDir);
for (const chatId of chatIds) {
const chatModel = new ChatModel(
this.storageModel,
this.connectionModel,
this.settingsModel,
this,
chatId,
);
this.addChatModel(chatModel);
}
};
// init
constructor(
storageModel: StorageModel,
settingsModel: SettingsModel,
connectionModel: ConnectionModel,
) {
this.storageModel = storageModel;
this.settingsModel = settingsModel;
this.connectionModel = connectionModel;
this.loadChats();
connectionModel.messageHandlerManager.addHandler(this.messageHandler);
connectionModel.messageSentHandlerManager.addHandler(
this.messageSentHandler,
);
}
};
// storage
addChatModel = (chatModel: ChatModel) => {
this.chatModels.add(chatModel);
};
createChat = (primaryChannel: string): ChatModel => {
const id: string = v4();
const chatModel = new ChatModel(
this.storageModel,
this.connectionModel,
this.settingsModel,
this,
id
);
chatModel.setPrimaryChannel(primaryChannel);
this.addChatModel(chatModel);
return chatModel;
};
untrackChat = (chat: ChatModel): void => {
this.chatModels.delete(chat);
};
// load
loadChats = (): void => {
const chatDir = StorageModel.getPath(StorageModelSubPath.Chat, filePaths.chat.base);
const chatIds = this.storageModel.list(chatDir);
for (const chatId of chatIds) {
const chatModel = new ChatModel(
this.storageModel,
this.connectionModel,
this.settingsModel,
this,
chatId
);
this.addChatModel(chatModel);
}
};
// init
constructor(
storageModel: StorageModel,
settingsModel: SettingsModel,
connectionModel: ConnectionModel
) {
this.storageModel = storageModel;
this.settingsModel = settingsModel;
this.connectionModel = connectionModel;
this.loadChats();
connectionModel.messageHandlerManager.addHandler(this.messageHandler);
connectionModel.messageSentHandlerManager.addHandler(
this.messageSentHandler
);
}
}
+321 -308
View File
@@ -3,15 +3,15 @@
import { DATA_VERSION, ValidObject } from "../Utility/typeSafety";
import FileModel, { FileContent } from "../Files/fileModel";
import {
HandlerManager,
createTimestamp,
localeCompare,
parseValidObject,
stringify,
HandlerManager,
createTimestamp,
localeCompare,
parseValidObject,
stringify,
} from "../Utility/utility";
import StorageModel, {
StorageModelSubPath,
filePaths,
StorageModelSubPath,
filePaths,
} from "../Global/storageModel";
import { decryptString, encryptString } from "../Utility/crypto";
@@ -22,345 +22,358 @@ import SettingsModel from "../Global/settingsModel";
import { v4 } from "uuid";
export default class ChatModel {
connectionModel: ConnectionModel;
storageModel: StorageModel;
settingsModel: SettingsModel;
chatListModel: ChatListModel;
connectionModel: ConnectionModel;
storageModel: StorageModel;
settingsModel: SettingsModel;
chatListModel: ChatListModel;
fileModel: FileModel;
fileModel: FileModel;
// data
id: string;
info: ChatInfo;
color: Color;
// data
id: string;
info: ChatInfo;
color: Color;
chatMessageHandlerManager: HandlerManager<ChatMessage> = new HandlerManager();
chatMessageHandlerManager: HandlerManager<ChatMessage> =
new HandlerManager();
get secondaryChannels(): string[] {
return this.info.secondaryChannels.sort(localeCompare);
}
// paths
getBasePath = (): string[] => {
return StorageModel.getPath(
StorageModelSubPath.Chat,
filePaths.chat.chatBase(this.id)
);
};
getInfoPath = (): string[] => {
return StorageModel.getPath(
StorageModelSubPath.Chat,
filePaths.chat.info(this.id)
);
};
getColorPath = (): string[] => {
return StorageModel.getPath(
StorageModelSubPath.Chat,
filePaths.chat.color(this.id)
);
};
getMessageDirPath = (): string[] => {
return StorageModel.getPath(
StorageModelSubPath.Chat,
filePaths.chat.messages(this.id)
);
};
getMessagePath = (id: string): string[] => {
return [...this.getMessageDirPath(), id];
};
// handlers
handleMessage = (body: string): void => {
const chatMessage: ChatMessage | null = parseValidObject(
body,
ChatMessageReference
);
if (chatMessage == null) return;
chatMessage.status = ChatMessageStatus.Received;
this.addMessage(chatMessage);
this.info.hasUnreadMessages = true;
this.storeInfo();
};
handleMessageSent = (chatMessage: ChatMessage): void => {
chatMessage.status = ChatMessageStatus.Sent;
this.addMessage(chatMessage);
};
// settings
setPrimaryChannel = (primaryChannel: string): void => {
this.info.primaryChannel = primaryChannel;
this.storeInfo();
this.subscribe();
};
setSecondaryChannels = (secondaryChannels: string[]): void => {
this.info.secondaryChannels = secondaryChannels;
this.storeInfo();
};
setEncryptionKey = (key: string): void => {
this.info.encryptionKey = key;
this.storeInfo();
};
setColor = (color: Color): void => {
this.color = color;
this.storeColor();
};
// messaging
addMessage = async (chatMessage: ChatMessage): Promise<void> => {
await this.decryptMessage(chatMessage);
// message
if (chatMessage.body != "") {
const messagePath: string[] = this.getMessagePath(chatMessage.id);
this.storageModel.writeStringifiable(messagePath, chatMessage);
this.chatMessageHandlerManager.trigger(chatMessage);
get secondaryChannels(): string[] {
return this.info.secondaryChannels.sort(localeCompare);
}
// file
this.fileModel.handleStringifiedFileContent(chatMessage.stringifiedFile);
};
sendMessage = async (
body: string,
fileContent?: FileContent<string>
): Promise<boolean> => {
const senderName = this.settingsModel.username;
if (senderName == "") return false;
const allChannels = [this.info.primaryChannel];
for (const secondaryChannel of this.info.secondaryChannels) {
allChannels.push(secondaryChannel);
}
const combinedChannel: string = allChannels.join("/");
const chatMessage: ChatMessage = await ChatModel.createChatMessage(
combinedChannel,
senderName,
this.info.encryptionKey,
body,
fileContent
);
this.addMessage(chatMessage);
this.connectionModel.sendMessageOrStore(chatMessage);
return true;
};
decryptMessage = async (chatMessage: ChatMessage): Promise<void> => {
const decryptedBody: string = await decryptString(
chatMessage.body,
this.info.encryptionKey
);
const decryptedFile: string = await decryptString(
chatMessage.stringifiedFile ?? "",
this.info.encryptionKey
);
chatMessage.body = decryptedBody;
chatMessage.stringifiedFile = decryptedFile;
};
subscribe = (): void => {
this.connectionModel.addChannel(this.info.primaryChannel);
};
markRead = (): void => {
this.info.hasUnreadMessages = false;
this.storeInfo();
}
// storage
storeInfo = (): void => {
this.storageModel.writeStringifiable(this.getInfoPath(), this.info);
};
storeColor = (): void => {
this.storageModel.write(this.getColorPath(), this.color);
};
delete = () => {
// untrack
this.chatListModel.untrackChat(this);
// delete
const dirPath: string[] = this.getBasePath();
this.storageModel.removeRecursively(dirPath);
};
// load
loadInfo = (): void => {
const info: ChatInfo | null = this.storageModel.readStringifiable(
this.getInfoPath(),
ChatInfoReference
);
if (info != null) {
this.info = info;
} else {
this.info = ChatModel.generateChatInfo("0");
}
};
loadColor = (): void => {
const path: string[] = this.getColorPath();
const color: string | null = this.storageModel.read(path);
if (!color) {
this.color = Color.Standard;
} else {
this.color = color as any;
}
};
get messages(): ChatMessage[] {
const messageIds: string[] = this.storageModel.list(
this.getMessageDirPath()
);
if (!Array.isArray(messageIds)) return [];
const chatMessages: ChatMessage[] = [];
for (const messageId of messageIds) {
const messagePath: string[] = this.getMessagePath(messageId);
const chatMessage: ChatMessage | any =
this.storageModel.readStringifiable(messagePath, ChatMessageReference);
chatMessages.push(chatMessage);
}
const sorted = chatMessages.sort((a, b) =>
a.dateSent.localeCompare(b.dateSent)
);
return sorted;
}
// init
constructor(
storageModel: StorageModel,
connectionModel: ConnectionModel,
settingsModel: SettingsModel,
chatListModel: ChatListModel,
chatId: string
) {
this.id = chatId;
this.connectionModel = connectionModel;
this.settingsModel = settingsModel;
this.storageModel = storageModel;
this.chatListModel = chatListModel;
this.loadInfo();
this.loadColor();
this.subscribe();
this.fileModel = new FileModel(this.storageModel, this.settingsModel, this);
}
// utility
static generateChatInfo = (primaryChannel: string): ChatInfo => {
return {
dataVersion: DATA_VERSION,
primaryChannel,
secondaryChannels: [],
encryptionKey: "",
hasUnreadMessages: false,
// paths
getBasePath = (): string[] => {
return StorageModel.getPath(
StorageModelSubPath.Chat,
filePaths.chat.chatBase(this.id),
);
};
};
static createChatMessage = async (
channel: string,
sender: string,
encryptionKey: string,
body: string,
fileContent?: FileContent<string>
): Promise<ChatMessage> => {
const chatMessage: ChatMessage = {
dataVersion: DATA_VERSION,
id: v4(),
channel,
sender,
body,
dateSent: createTimestamp(),
status: ChatMessageStatus.Outbox,
stringifiedFile: "",
getInfoPath = (): string[] => {
return StorageModel.getPath(
StorageModelSubPath.Chat,
filePaths.chat.info(this.id),
);
};
if (fileContent != undefined) {
const stringifiedFile = stringify(fileContent);
chatMessage.stringifiedFile = stringifiedFile;
getColorPath = (): string[] => {
return StorageModel.getPath(
StorageModelSubPath.Chat,
filePaths.chat.color(this.id),
);
};
getMessageDirPath = (): string[] => {
return StorageModel.getPath(
StorageModelSubPath.Chat,
filePaths.chat.messages(this.id),
);
};
getMessagePath = (id: string): string[] => {
return [...this.getMessageDirPath(), id];
};
// handlers
handleMessage = (body: string): void => {
const chatMessage: ChatMessage | null = parseValidObject(
body,
ChatMessageReference,
);
if (chatMessage == null) return;
chatMessage.status = ChatMessageStatus.Received;
this.addMessage(chatMessage);
this.info.hasUnreadMessages = true;
this.storeInfo();
};
handleMessageSent = (chatMessage: ChatMessage): void => {
chatMessage.status = ChatMessageStatus.Sent;
this.addMessage(chatMessage);
};
// settings
setPrimaryChannel = (primaryChannel: string): void => {
this.info.primaryChannel = primaryChannel;
this.storeInfo();
this.subscribe();
};
setSecondaryChannels = (secondaryChannels: string[]): void => {
this.info.secondaryChannels = secondaryChannels;
this.storeInfo();
};
setEncryptionKey = (key: string): void => {
this.info.encryptionKey = key;
this.storeInfo();
};
setColor = (color: Color): void => {
this.color = color;
this.storeColor();
};
// messaging
addMessage = async (chatMessage: ChatMessage): Promise<void> => {
await this.decryptMessage(chatMessage);
// message
if (chatMessage.body != "") {
const messagePath: string[] = this.getMessagePath(chatMessage.id);
this.storageModel.writeStringifiable(messagePath, chatMessage);
this.chatMessageHandlerManager.trigger(chatMessage);
}
// file
this.fileModel.handleStringifiedFileContent(
chatMessage.stringifiedFile,
);
};
sendMessage = async (
body: string,
fileContent?: FileContent<string>,
): Promise<boolean> => {
const senderName = this.settingsModel.username;
if (senderName == "") return false;
const allChannels = [this.info.primaryChannel];
for (const secondaryChannel of this.info.secondaryChannels) {
allChannels.push(secondaryChannel);
}
const combinedChannel: string = allChannels.join("/");
const chatMessage: ChatMessage = await ChatModel.createChatMessage(
combinedChannel,
senderName,
this.info.encryptionKey,
body,
fileContent,
);
this.addMessage(chatMessage);
this.connectionModel.sendMessageOrStore(chatMessage);
return true;
};
decryptMessage = async (chatMessage: ChatMessage): Promise<void> => {
const decryptedBody: string = await decryptString(
chatMessage.body,
this.info.encryptionKey,
);
const decryptedFile: string = await decryptString(
chatMessage.stringifiedFile ?? "",
this.info.encryptionKey,
);
chatMessage.body = decryptedBody;
chatMessage.stringifiedFile = decryptedFile;
};
subscribe = (): void => {
this.connectionModel.addChannel(this.info.primaryChannel);
};
markRead = (): void => {
this.info.hasUnreadMessages = false;
this.storeInfo();
};
// storage
storeInfo = (): void => {
this.storageModel.writeStringifiable(this.getInfoPath(), this.info);
};
storeColor = (): void => {
this.storageModel.write(this.getColorPath(), this.color);
};
delete = () => {
// untrack
this.chatListModel.untrackChat(this);
// delete
const dirPath: string[] = this.getBasePath();
this.storageModel.removeRecursively(dirPath);
};
// load
loadInfo = (): void => {
const info: ChatInfo | null = this.storageModel.readStringifiable(
this.getInfoPath(),
ChatInfoReference,
);
if (info != null) {
this.info = info;
} else {
this.info = ChatModel.generateChatInfo("0");
}
};
loadColor = (): void => {
const path: string[] = this.getColorPath();
const color: string | null = this.storageModel.read(path);
if (!color) {
this.color = Color.Standard;
} else {
this.color = color as any;
}
};
get messages(): ChatMessage[] {
const messageIds: string[] = this.storageModel.list(
this.getMessageDirPath(),
);
if (!Array.isArray(messageIds)) return [];
const chatMessages: ChatMessage[] = [];
for (const messageId of messageIds) {
const messagePath: string[] = this.getMessagePath(messageId);
const chatMessage: ChatMessage | any =
this.storageModel.readStringifiable(
messagePath,
ChatMessageReference,
);
chatMessages.push(chatMessage);
}
const sorted = chatMessages.sort((a, b) =>
a.dateSent.localeCompare(b.dateSent),
);
return sorted;
}
if (encryptionKey != "") {
chatMessage.body = await encryptString(chatMessage.body, encryptionKey);
chatMessage.stringifiedFile = await encryptString(
chatMessage.stringifiedFile,
encryptionKey
);
// init
constructor(
storageModel: StorageModel,
connectionModel: ConnectionModel,
settingsModel: SettingsModel,
chatListModel: ChatListModel,
chatId: string,
) {
this.id = chatId;
this.connectionModel = connectionModel;
this.settingsModel = settingsModel;
this.storageModel = storageModel;
this.chatListModel = chatListModel;
this.loadInfo();
this.loadColor();
this.subscribe();
this.fileModel = new FileModel(
this.storageModel,
this.settingsModel,
this,
);
}
return chatMessage;
};
// utility
static generateChatInfo = (primaryChannel: string): ChatInfo => {
return {
dataVersion: DATA_VERSION,
primaryChannel,
secondaryChannels: [],
encryptionKey: "",
hasUnreadMessages: false,
};
};
static createChatMessage = async (
channel: string,
sender: string,
encryptionKey: string,
body: string,
fileContent?: FileContent<string>,
): Promise<ChatMessage> => {
const chatMessage: ChatMessage = {
dataVersion: DATA_VERSION,
id: v4(),
channel,
sender,
body,
dateSent: createTimestamp(),
status: ChatMessageStatus.Outbox,
stringifiedFile: "",
};
if (fileContent != undefined) {
const stringifiedFile = stringify(fileContent);
chatMessage.stringifiedFile = stringifiedFile;
}
if (encryptionKey != "") {
chatMessage.body = await encryptString(
chatMessage.body,
encryptionKey,
);
chatMessage.stringifiedFile = await encryptString(
chatMessage.stringifiedFile,
encryptionKey,
);
}
return chatMessage;
};
}
// types
export interface ChatInfo extends ValidObject {
primaryChannel: string;
secondaryChannels: string[];
encryptionKey: string;
primaryChannel: string;
secondaryChannels: string[];
encryptionKey: string;
hasUnreadMessages: boolean;
hasUnreadMessages: boolean;
}
export enum ChatMessageStatus {
Outbox = "outbox",
Sent = "sent",
Received = "received",
Other = "other",
Outbox = "outbox",
Sent = "sent",
Received = "received",
Other = "other",
}
export interface ChatMessage extends ValidObject {
id: string;
id: string;
channel: string;
sender: string;
body: string;
dateSent: string;
channel: string;
sender: string;
body: string;
dateSent: string;
status: ChatMessageStatus;
status: ChatMessageStatus;
stringifiedFile: string;
stringifiedFile: string;
}
// references
export const ChatInfoReference: ChatInfo = {
dataVersion: DATA_VERSION,
dataVersion: DATA_VERSION,
primaryChannel: "",
secondaryChannels: [""],
encryptionKey: "",
primaryChannel: "",
secondaryChannels: [""],
encryptionKey: "",
hasUnreadMessages: true,
hasUnreadMessages: true,
};
export const ChatMessageReference: ChatMessage = {
dataVersion: DATA_VERSION,
dataVersion: DATA_VERSION,
id: "",
id: "",
channel: "",
sender: "",
body: "",
dateSent: "",
channel: "",
sender: "",
body: "",
dateSent: "",
status: "" as ChatMessageStatus,
status: "" as ChatMessageStatus,
stringifiedFile: "",
stringifiedFile: "",
};
+278 -270
View File
@@ -1,8 +1,8 @@
// this file is responsible for managing boards within chats and tasks within boards.
import {
DATA_VERSION,
checkMatchesObjectStructure,
DATA_VERSION,
checkMatchesObjectStructure,
} from "../Utility/typeSafety";
import FileModel, { FileContent, FileModelSubPath } from "./fileModel";
@@ -15,316 +15,324 @@ import StorageModel from "../Global/storageModel";
import { v4 } from "uuid";
export default class BoardsAndTasksModel {
storageModel: StorageModel;
settingsModel: SettingsModel;
chatModel: ChatModel;
fileModel: FileModel;
storageModel: StorageModel;
settingsModel: SettingsModel;
chatModel: ChatModel;
fileModel: FileModel;
calendarModel: CalendarModel;
calendarModel: CalendarModel;
// data
boardHandlerManager: HandlerManager<BoardInfoFileContent> =
new HandlerManager();
taskHandlerManager: HandlerManager<TaskFileContent> = new HandlerManager();
// data
boardHandlerManager: HandlerManager<BoardInfoFileContent> =
new HandlerManager();
taskHandlerManager: HandlerManager<TaskFileContent> = new HandlerManager();
// paths
getBasePath = (): string[] => {
return this.fileModel.getModelContainerPath(FileModelSubPath.ModelTask);
};
// paths
getBasePath = (): string[] => {
return this.fileModel.getModelContainerPath(FileModelSubPath.ModelTask);
};
getViewPath = (): string[] => {
return [...this.getBasePath(), FileModelSubPath.ModelView];
};
getViewPath = (): string[] => {
return [...this.getBasePath(), FileModelSubPath.ModelView];
};
getBoardFilePath = (boardId: string): string[] => {
return [...this.fileModel.getFilePath(boardId)];
};
getBoardFilePath = (boardId: string): string[] => {
return [...this.fileModel.getFilePath(boardId)];
};
getTaskFilePath = (taskId: string): string[] => {
return [...this.fileModel.getFilePath(taskId)];
};
getTaskFilePath = (taskId: string): string[] => {
return [...this.fileModel.getFilePath(taskId)];
};
getBoardContainerPath = (): string[] => {
return [...this.getBasePath(), TaskModelSubPaths.Boards];
};
getBoardContainerPath = (): string[] => {
return [...this.getBasePath(), TaskModelSubPaths.Boards];
};
getBoardDirectoryPath = (boardId: string): string[] => {
return [...this.getBoardContainerPath(), boardId];
};
getBoardDirectoryPath = (boardId: string): string[] => {
return [...this.getBoardContainerPath(), boardId];
};
getTaskContainerPath = (boardId: string): string[] => {
return [
...this.getBoardDirectoryPath(boardId),
TaskModelSubPaths.BoardTasks,
];
};
getTaskContainerPath = (boardId: string): string[] => {
return [
...this.getBoardDirectoryPath(boardId),
TaskModelSubPaths.BoardTasks,
];
};
getTaskReferencePath = (boardId: string, fileId: string): string[] => {
return [...this.getTaskContainerPath(boardId), fileId];
};
getTaskReferencePath = (boardId: string, fileId: string): string[] => {
return [...this.getTaskContainerPath(boardId), fileId];
};
// handlers
handleFileContent = (fileContent: FileContent<string>): void => {
if (
checkMatchesObjectStructure(fileContent, BoardInfoFileContentReference) ==
true
// handlers
handleFileContent = (fileContent: FileContent<string>): void => {
if (
checkMatchesObjectStructure(
fileContent,
BoardInfoFileContentReference,
) == true
) {
this.handleBoard(fileContent as BoardInfoFileContent);
} else if (
checkMatchesObjectStructure(
fileContent,
TaskFileContentReference,
) == true
) {
this.handleTask(fileContent as TaskFileContent);
}
};
handleBoard = (boardInfoFileContent: BoardInfoFileContent) => {
this.updateBoard(boardInfoFileContent);
};
handleTask = (taskFileContent: TaskFileContent) => {
this.updateTask(taskFileContent);
};
// boards
createBoard = (name: string): BoardInfoFileContent => {
const boardInfoFileContent: BoardInfoFileContent =
BoardsAndTasksModel.createBoardInfoFileContent(
v4(),
name,
Color.Standard,
);
return boardInfoFileContent;
};
updateBoard = (boardInfoFileContent: BoardInfoFileContent): void => {
this.storeBoard(boardInfoFileContent);
this.boardHandlerManager.trigger(boardInfoFileContent);
};
updateBoardAndSend = (boardInfoFileContent: BoardInfoFileContent): void => {
this.updateBoard(boardInfoFileContent);
this.chatModel.sendMessage("", boardInfoFileContent);
};
storeBoard = (boardInfoFileContent: BoardInfoFileContent): void => {
// store info
this.fileModel.storeFileContent(boardInfoFileContent);
// add to list
const boardDirectoryPath: string[] = this.getBoardDirectoryPath(
boardInfoFileContent.fileId,
);
this.storageModel.write(boardDirectoryPath, "");
};
deleteBoard = (boardId: string): void => {
const boardFilePath: string[] = this.getBoardFilePath(boardId);
const boardDirectoryPath: string[] =
this.getBoardDirectoryPath(boardId);
this.storageModel.removeRecursively(boardFilePath);
this.storageModel.removeRecursively(boardDirectoryPath);
};
listBoardIds = (): string[] => {
const boardContainerPath: string[] = this.getBoardContainerPath();
const boardIds: string[] = this.storageModel.list(boardContainerPath);
return boardIds;
};
getBoardInfo = (fileId: string): BoardInfoFileContent | null => {
const boardInfoFileContentOrNull: BoardInfoFileContent | null =
this.fileModel.getLatestFileContent(
fileId,
BoardInfoFileContentReference,
);
return boardInfoFileContentOrNull;
};
getBoardName = (boardId: string): string => {
const boardInfo: BoardInfoFileContent | null =
this.getBoardInfo(boardId);
if (boardInfo == null) return "";
return boardInfo.name;
};
//tasks
createTask = (boardId: string): TaskFileContent => {
const taskFileContent: TaskFileContent =
BoardsAndTasksModel.createTaskFileContent(v4(), "", boardId);
return taskFileContent;
};
updateTask = (taskFileContent: TaskFileContent): void => {
this.storeTask(taskFileContent);
this.taskHandlerManager.trigger(taskFileContent);
};
updateTaskAndSend = (taskFileContent: TaskFileContent): void => {
this.updateTask(taskFileContent);
this.chatModel.sendMessage("", taskFileContent);
};
storeTask = (taskFileContent: TaskFileContent): void => {
// store info
this.fileModel.storeFileContent(taskFileContent);
// add to board
const taskReferencePath: string[] = this.getTaskReferencePath(
taskFileContent.boardId,
taskFileContent.fileId,
);
this.storageModel.write(taskReferencePath, "");
// add to calendar
this.calendarModel.storeTaskReference(taskFileContent);
};
listTaskIds = (boardId: string): string[] => {
const taskContainerPath: string[] = this.getTaskContainerPath(boardId);
const fileIds: string[] = this.storageModel.list(taskContainerPath);
return fileIds;
};
listTaskVersionIds = (taskId: string): string[] => {
const versionIds: string[] = this.fileModel.listFileContentIds(taskId);
return versionIds;
};
getLatestTaskFileContent = (taskId: string): TaskFileContent | null => {
const taskFileContentOrNull: TaskFileContent | null =
this.fileModel.getLatestFileContent(
taskId,
TaskFileContentReference,
);
return taskFileContentOrNull;
};
getSpecificTaskFileContent = (
taskId: string,
versionId: string,
): TaskFileContent | null => {
const taskFileContentOrNull: TaskFileContent | null =
this.fileModel.getFileContent(
taskId,
versionId,
TaskFileContentReference,
);
return taskFileContentOrNull;
};
deleteTask = (boardId: string, taskId: string): void => {
const taskFilePath: string[] = this.getTaskFilePath(taskId);
this.storageModel.removeRecursively(taskFilePath);
this.deleteTaskReference(boardId, taskId);
};
deleteTaskReference = (boardId: string, taskId: string): void => {
const taskReferencePath: string[] = this.getTaskReferencePath(
boardId,
taskId,
);
this.storageModel.removeRecursively(taskReferencePath);
};
// init
constructor(
storageModel: StorageModel,
settingsModel: SettingsModel,
chatModel: ChatModel,
fileModel: FileModel,
) {
this.handleBoard(fileContent as BoardInfoFileContent);
} else if (
checkMatchesObjectStructure(fileContent, TaskFileContentReference) == true
) {
this.handleTask(fileContent as TaskFileContent);
this.storageModel = storageModel;
this.settingsModel = settingsModel;
this.chatModel = chatModel;
this.fileModel = fileModel;
this.calendarModel = new CalendarModel(
this.storageModel,
this.settingsModel,
this.fileModel,
);
}
};
handleBoard = (boardInfoFileContent: BoardInfoFileContent) => {
this.updateBoard(boardInfoFileContent);
};
// utility
static createBoardInfoFileContent = (
fileId: string,
name: string,
color: Color,
): BoardInfoFileContent => {
const fileContent: FileContent<"board-info"> =
FileModel.createFileContent(fileId, "board-info");
return {
...fileContent,
handleTask = (taskFileContent: TaskFileContent) => {
this.updateTask(taskFileContent);
};
// boards
createBoard = (name: string): BoardInfoFileContent => {
const boardInfoFileContent: BoardInfoFileContent =
BoardsAndTasksModel.createBoardInfoFileContent(
v4(),
name,
Color.Standard
);
return boardInfoFileContent;
};
updateBoard = (boardInfoFileContent: BoardInfoFileContent): void => {
this.storeBoard(boardInfoFileContent);
this.boardHandlerManager.trigger(boardInfoFileContent);
};
updateBoardAndSend = (boardInfoFileContent: BoardInfoFileContent): void => {
this.updateBoard(boardInfoFileContent);
this.chatModel.sendMessage("", boardInfoFileContent);
};
storeBoard = (boardInfoFileContent: BoardInfoFileContent): void => {
// store info
this.fileModel.storeFileContent(boardInfoFileContent);
// add to list
const boardDirectoryPath: string[] = this.getBoardDirectoryPath(
boardInfoFileContent.fileId
);
this.storageModel.write(boardDirectoryPath, "");
};
deleteBoard = (boardId: string): void => {
const boardFilePath: string[] = this.getBoardFilePath(boardId);
const boardDirectoryPath: string[] = this.getBoardDirectoryPath(boardId);
this.storageModel.removeRecursively(boardFilePath);
this.storageModel.removeRecursively(boardDirectoryPath);
};
listBoardIds = (): string[] => {
const boardContainerPath: string[] = this.getBoardContainerPath();
const boardIds: string[] = this.storageModel.list(boardContainerPath);
return boardIds;
};
getBoardInfo = (fileId: string): BoardInfoFileContent | null => {
const boardInfoFileContentOrNull: BoardInfoFileContent | null =
this.fileModel.getLatestFileContent(
fileId,
BoardInfoFileContentReference
);
return boardInfoFileContentOrNull;
};
getBoardName = (boardId: string): string => {
const boardInfo: BoardInfoFileContent|null = this.getBoardInfo(boardId);
if (boardInfo == null) return "";
return boardInfo.name;
}
//tasks
createTask = (boardId: string): TaskFileContent => {
const taskFileContent: TaskFileContent =
BoardsAndTasksModel.createTaskFileContent(v4(), "", boardId);
return taskFileContent;
};
updateTask = (taskFileContent: TaskFileContent): void => {
this.storeTask(taskFileContent);
this.taskHandlerManager.trigger(taskFileContent);
};
updateTaskAndSend = (taskFileContent: TaskFileContent): void => {
this.updateTask(taskFileContent);
this.chatModel.sendMessage("", taskFileContent);
};
storeTask = (taskFileContent: TaskFileContent): void => {
// store info
this.fileModel.storeFileContent(taskFileContent);
// add to board
const taskReferencePath: string[] = this.getTaskReferencePath(
taskFileContent.boardId,
taskFileContent.fileId
);
this.storageModel.write(taskReferencePath, "");
// add to calendar
this.calendarModel.storeTaskReference(taskFileContent);
};
listTaskIds = (boardId: string): string[] => {
const taskContainerPath: string[] = this.getTaskContainerPath(boardId);
const fileIds: string[] = this.storageModel.list(taskContainerPath);
return fileIds;
};
listTaskVersionIds = (taskId: string): string[] => {
const versionIds: string[] = this.fileModel.listFileContentIds(taskId);
return versionIds;
};
getLatestTaskFileContent = (taskId: string): TaskFileContent | null => {
const taskFileContentOrNull: TaskFileContent | null =
this.fileModel.getLatestFileContent(taskId, TaskFileContentReference);
return taskFileContentOrNull;
};
getSpecificTaskFileContent = (
taskId: string,
versionId: string
): TaskFileContent | null => {
const taskFileContentOrNull: TaskFileContent | null =
this.fileModel.getFileContent(
taskId,
versionId,
TaskFileContentReference
);
return taskFileContentOrNull;
};
deleteTask = (boardId: string, taskId: string): void => {
const taskFilePath: string[] = this.getTaskFilePath(taskId);
this.storageModel.removeRecursively(taskFilePath);
this.deleteTaskReference(boardId, taskId);
};
deleteTaskReference = (boardId: string, taskId: string): void => {
const taskReferencePath: string[] = this.getTaskReferencePath(
boardId,
taskId
);
this.storageModel.removeRecursively(taskReferencePath);
};
// init
constructor(
storageModel: StorageModel,
settingsModel: SettingsModel,
chatModel: ChatModel,
fileModel: FileModel
) {
this.storageModel = storageModel;
this.settingsModel = settingsModel;
this.chatModel = chatModel;
this.fileModel = fileModel;
this.calendarModel = new CalendarModel(
this.storageModel,
this.settingsModel,
this.fileModel
);
}
// utility
static createBoardInfoFileContent = (
fileId: string,
name: string,
color: Color
): BoardInfoFileContent => {
const fileContent: FileContent<"board-info"> = FileModel.createFileContent(
fileId,
"board-info"
);
return {
...fileContent,
name,
color,
name,
color,
};
};
};
static createTaskFileContent = (
fileId: string,
name: string,
boardId: string
): TaskFileContent => {
const fileContent: FileContent<"task"> = FileModel.createFileContent(
fileId,
"task"
);
return {
...fileContent,
static createTaskFileContent = (
fileId: string,
name: string,
boardId: string,
): TaskFileContent => {
const fileContent: FileContent<"task"> = FileModel.createFileContent(
fileId,
"task",
);
return {
...fileContent,
name,
boardId,
name,
boardId,
};
};
};
}
export enum TaskModelSubPaths {
Boards = "boards",
BoardTasks = "tasks",
Boards = "boards",
BoardTasks = "tasks",
}
// types
export interface BoardInfoFileContent extends FileContent<"board-info"> {
name: string;
color: Color;
name: string;
color: Color;
}
export interface TaskFileContent extends FileContent<"task"> {
name: string;
boardId: string;
name: string;
boardId: string;
description?: string;
description?: string;
category?: string;
status?: string;
priority?: string;
category?: string;
status?: string;
priority?: string;
date?: string;
time?: string;
date?: string;
time?: string;
}
// reference
export const BoardInfoFileContentReference: BoardInfoFileContent = {
dataVersion: DATA_VERSION,
dataVersion: DATA_VERSION,
fileId: "string",
fileContentId: "",
creationDate: "",
fileId: "string",
fileContentId: "",
creationDate: "",
type: "board-info",
type: "board-info",
name: "",
color: "" as Color,
name: "",
color: "" as Color,
};
export const TaskFileContentReference: TaskFileContent = {
dataVersion: DATA_VERSION,
dataVersion: DATA_VERSION,
fileId: "string",
fileContentId: "",
creationDate: "",
type: "task",
fileId: "string",
fileContentId: "",
creationDate: "",
type: "task",
name: "",
boardId: "",
name: "",
boardId: "",
};
+138 -136
View File
@@ -7,158 +7,160 @@ import StorageModel from "../Global/storageModel";
import { TaskFileContent } from "./boardsAndTasksModel";
export default class CalendarModel {
storageModel: StorageModel;
fileModel: FileModel;
settingsModel: SettingsModel;
storageModel: StorageModel;
fileModel: FileModel;
settingsModel: SettingsModel;
// paths
getBasePath = (): string[] => {
return this.fileModel.getModelContainerPath(FileModelSubPath.ModelCalendar);
};
getViewPath = (): string[] => {
return [...this.getBasePath(), FileModelSubPath.ModelView];
};
getMonthContainerPath = (): string[] => {
return [...this.getBasePath(), CalendarModelSubPaths.Months];
};
getMonthPath = (monthString: string): string[] => {
return [...this.getMonthContainerPath(), monthString];
};
// main
storeTaskReference = (taskFileContent: TaskFileContent): void => {
if (taskFileContent.date == undefined) return;
const monthString: string = CalendarModel.isoToMonthString(
taskFileContent.date
);
const monthPath: string[] = this.getMonthPath(monthString);
const referencePath: string[] = [...monthPath, taskFileContent.fileId];
this.storageModel.write(referencePath, "");
};
deleteTaskReference = (monthString: string, taskId: string): void => {
const monthPath: string[] = this.getMonthPath(monthString);
const referencePath: string[] = [...monthPath, taskId];
this.storageModel.write(referencePath, "");
};
listTaskIds = (monthString: string): string[] => {
const monthPath: string[] = this.getMonthPath(monthString);
return this.storageModel.list(monthPath);
};
generateMonthGrid = <T>(
year: number,
month: number,
defaultValueCreator: () => T
): MonthGrid<T> => {
// get today
const date: Date = new Date();
const isCurrentMonth: boolean =
year == date.getFullYear() && month == date.getMonth() + 1;
// get data for offset
date.setDate(1);
date.setMonth(month - 1);
date.setFullYear(year);
const firstWeekdayOfMonth = date.getDay();
const firstDayOfWeekSetting: number = parseInt(
this.settingsModel.firstDayOfWeek
);
const offset: number =
firstWeekdayOfMonth < firstDayOfWeekSetting
? 7 - firstDayOfWeekSetting
: firstWeekdayOfMonth - firstDayOfWeekSetting;
// get total day count
date.setMonth(month);
date.setDate(-1);
const daysInMonth: number = date.getDate() + 1;
const grid: MonthGrid<T> = {
offset,
firstDayOfWeek: parseInt(this.settingsModel.firstDayOfWeek),
isCurrentMonth,
year,
month,
days: {},
// paths
getBasePath = (): string[] => {
return this.fileModel.getModelContainerPath(
FileModelSubPath.ModelCalendar,
);
};
for (let i = 0; i < daysInMonth; i++) {
const paddedDate: string = CalendarModel.padDateOrMonth(
(i + 1).toString()
);
grid.days[paddedDate] = defaultValueCreator();
getViewPath = (): string[] => {
return [...this.getBasePath(), FileModelSubPath.ModelView];
};
getMonthContainerPath = (): string[] => {
return [...this.getBasePath(), CalendarModelSubPaths.Months];
};
getMonthPath = (monthString: string): string[] => {
return [...this.getMonthContainerPath(), monthString];
};
// main
storeTaskReference = (taskFileContent: TaskFileContent): void => {
if (taskFileContent.date == undefined) return;
const monthString: string = CalendarModel.isoToMonthString(
taskFileContent.date,
);
const monthPath: string[] = this.getMonthPath(monthString);
const referencePath: string[] = [...monthPath, taskFileContent.fileId];
this.storageModel.write(referencePath, "");
};
deleteTaskReference = (monthString: string, taskId: string): void => {
const monthPath: string[] = this.getMonthPath(monthString);
const referencePath: string[] = [...monthPath, taskId];
this.storageModel.write(referencePath, "");
};
listTaskIds = (monthString: string): string[] => {
const monthPath: string[] = this.getMonthPath(monthString);
return this.storageModel.list(monthPath);
};
generateMonthGrid = <T>(
year: number,
month: number,
defaultValueCreator: () => T,
): MonthGrid<T> => {
// get today
const date: Date = new Date();
const isCurrentMonth: boolean =
year == date.getFullYear() && month == date.getMonth() + 1;
// get data for offset
date.setDate(1);
date.setMonth(month - 1);
date.setFullYear(year);
const firstWeekdayOfMonth = date.getDay();
const firstDayOfWeekSetting: number = parseInt(
this.settingsModel.firstDayOfWeek,
);
const offset: number =
firstWeekdayOfMonth < firstDayOfWeekSetting
? 7 - firstDayOfWeekSetting
: firstWeekdayOfMonth - firstDayOfWeekSetting;
// get total day count
date.setMonth(month);
date.setDate(-1);
const daysInMonth: number = date.getDate() + 1;
const grid: MonthGrid<T> = {
offset,
firstDayOfWeek: parseInt(this.settingsModel.firstDayOfWeek),
isCurrentMonth,
year,
month,
days: {},
};
for (let i = 0; i < daysInMonth; i++) {
const paddedDate: string = CalendarModel.padDateOrMonth(
(i + 1).toString(),
);
grid.days[paddedDate] = defaultValueCreator();
}
return grid;
};
// init
constructor(
storageModel: StorageModel,
settingsModel: SettingsModel,
fileModel: FileModel,
) {
this.storageModel = storageModel;
this.settingsModel = settingsModel;
this.fileModel = fileModel;
}
return grid;
};
// utility
static isoToMonthString = (dateISOString: string): string => {
const [year, month, _] = dateISOString.split("-");
return CalendarModel.getMonthString(year, month);
};
// init
constructor(
storageModel: StorageModel,
settingsModel: SettingsModel,
fileModel: FileModel
) {
this.storageModel = storageModel;
this.settingsModel = settingsModel;
this.fileModel = fileModel;
}
static isoToDateString = (dateISOString: string): string => {
const [year, month, date, _] = dateISOString.split("-");
const paddedDate = CalendarModel.padDateOrMonth(date ?? "");
return paddedDate;
};
// utility
static isoToMonthString = (dateISOString: string): string => {
const [year, month, _] = dateISOString.split("-");
return CalendarModel.getMonthString(year, month);
};
static getMonthString = (year: string = "", month: string = ""): string => {
const paddedYear: string = year.padStart(4, "0");
const paddedMonth: string = CalendarModel.padDateOrMonth(month);
return `${paddedYear}-${paddedMonth}`;
};
static isoToDateString = (dateISOString: string): string => {
const [year, month, date, _] = dateISOString.split("-");
const paddedDate = CalendarModel.padDateOrMonth(date ?? "");
return paddedDate;
};
static getISODateString = (
year: string,
month: string,
date: string,
): string => {
const monthString: string = CalendarModel.getMonthString(year, month);
const paddedDate: string = CalendarModel.padDateOrMonth(date);
return `${monthString}-${paddedDate}`;
};
static getMonthString = (year: string = "", month: string = ""): string => {
const paddedYear: string = year.padStart(4, "0");
const paddedMonth: string = CalendarModel.padDateOrMonth(month);
return `${paddedYear}-${paddedMonth}`;
};
static getISODateString = (
year: string,
month: string,
date: string
): string => {
const monthString: string = CalendarModel.getMonthString(year, month);
const paddedDate: string = CalendarModel.padDateOrMonth(date);
return `${monthString}-${paddedDate}`;
};
static padDateOrMonth = (input: string): string => {
return input.padStart(2, "0");
};
static padDateOrMonth = (input: string): string => {
return input.padStart(2, "0");
};
}
export enum CalendarModelSubPaths {
Months = "months",
Months = "months",
}
// types
export interface MonthGrid<T> {
offset: number;
firstDayOfWeek: number;
isCurrentMonth: boolean;
offset: number;
firstDayOfWeek: number;
isCurrentMonth: boolean;
year: number;
month: number;
year: number;
month: number;
days: { [date: string]: T };
days: { [date: string]: T };
}
+178 -175
View File
@@ -2,14 +2,14 @@
import { DATA_VERSION, ValidObject } from "../Utility/typeSafety";
import StorageModel, {
StorageModelSubPath,
filePaths,
StorageModelSubPath,
filePaths,
} from "../Global/storageModel";
import {
StringEntryObject,
createTimestamp,
parseValidObject,
stringify,
StringEntryObject,
createTimestamp,
parseValidObject,
stringify,
} from "../Utility/utility";
import BoardsAndTasksModel from "./boardsAndTasksModel";
@@ -18,191 +18,194 @@ import SettingsModel from "../Global/settingsModel";
import { v4 } from "uuid";
export default class FileModel {
storageModel: StorageModel;
settingsModel: SettingsModel;
storageModel: StorageModel;
settingsModel: SettingsModel;
chatModel: ChatModel;
boardsAndTasksModel: BoardsAndTasksModel;
chatModel: ChatModel;
boardsAndTasksModel: BoardsAndTasksModel;
// paths
getBasePath = (): string[] => {
return StorageModel.getPath(
StorageModelSubPath.Chat,
filePaths.chat.files(this.chatModel.id)
);
};
getFileContainerPath = (): string[] => {
return [...this.getBasePath(), FileModelSubPath.Data];
};
getModelContainerPath = (modelName: FileModelSubPath): string[] => {
return [...this.getBasePath(), FileModelSubPath.Model, modelName];
};
getFilePath = (fileId: string): string[] => {
return [...this.getFileContainerPath(), fileId];
};
getFileContentPath = (fileId: string, fileContentId: string): string[] => {
const filePath: string[] = this.getFilePath(fileId);
return [...filePath, fileContentId];
};
// handlers
handleStringifiedFileContent = (stringifiedFileContent: string): void => {
const fileContent: FileContent<string> | null = parseValidObject(
stringifiedFileContent,
FileContentReference
);
if (fileContent == null) return;
this.handleFileContent(fileContent);
};
handleFileContent = (fileContent: FileContent<string>): void => {
const didStore: boolean = this.storeFileContent(fileContent);
if (didStore == false) return;
this.boardsAndTasksModel.handleFileContent(fileContent);
};
// methods
addFileContentAndSend = (fileContent: FileContent<string>): void => {
this.handleFileContent(fileContent);
this.chatModel.sendMessage("", fileContent);
};
// storage
storeFileContent = (fileContent: FileContent<string>): boolean => {
const fileContentPath: string[] = this.getFileContentPath(
fileContent.fileId,
fileContent.fileContentId
);
// check if fileContent already exists
const existingFileContent: string | null =
this.storageModel.read(fileContentPath);
if (existingFileContent != null) return false;
const stringifiedContent: string = stringify(fileContent);
this.storageModel.write(fileContentPath, stringifiedContent);
return true;
};
listFileIds = (): string[] => {
return this.storageModel.list(this.getBasePath());
};
listFileContentIds = (fileId: string): string[] => {
const filePath: string[] = this.getFilePath(fileId);
return this.storageModel.list(filePath);
};
selectLatestFileContentId = (
fileContentIds: string[]
): string | undefined => {
return fileContentIds[fileContentIds.length - 1];
};
getFileContent = <T extends FileContent<string>>(
fileId: string,
fileContentName: string,
reference: T
): T | null => {
const filePath: string[] = this.getFileContentPath(fileId, fileContentName);
const fileContentOrNull: T | null = this.storageModel.readStringifiable(
filePath,
reference
);
return fileContentOrNull;
};
getLatestFileContent = <T extends FileContent<string>>(
fileId: string,
reference: T
): T | null => {
const fileContentsIds: string[] = this.listFileContentIds(fileId);
const latestFileContentId: string | undefined =
this.selectLatestFileContentId(fileContentsIds);
if (latestFileContentId == undefined) return null;
const fileContent: T | null = this.getFileContent(
fileId,
latestFileContentId,
reference
);
return fileContent;
};
// init
constructor(
storageModel: StorageModel,
settingsModel: SettingsModel,
chatModel: ChatModel
) {
this.chatModel = chatModel;
this.settingsModel = settingsModel;
this.storageModel = storageModel;
this.boardsAndTasksModel = new BoardsAndTasksModel(
this.storageModel,
this.settingsModel,
chatModel,
this
);
}
// utility
static generateFileContentId = (creationDate: string): string => {
return creationDate + v4();
};
static createFileContent = <T extends string>(
fileId: string,
type: T
): FileContent<T> => {
const creationDate: string = createTimestamp();
const fileContentId: string = FileModel.generateFileContentId(creationDate);
return {
dataVersion: DATA_VERSION,
fileId,
fileContentId,
creationDate,
type,
// paths
getBasePath = (): string[] => {
return StorageModel.getPath(
StorageModelSubPath.Chat,
filePaths.chat.files(this.chatModel.id),
);
};
getFileContainerPath = (): string[] => {
return [...this.getBasePath(), FileModelSubPath.Data];
};
getModelContainerPath = (modelName: FileModelSubPath): string[] => {
return [...this.getBasePath(), FileModelSubPath.Model, modelName];
};
getFilePath = (fileId: string): string[] => {
return [...this.getFileContainerPath(), fileId];
};
getFileContentPath = (fileId: string, fileContentId: string): string[] => {
const filePath: string[] = this.getFilePath(fileId);
return [...filePath, fileContentId];
};
// handlers
handleStringifiedFileContent = (stringifiedFileContent: string): void => {
const fileContent: FileContent<string> | null = parseValidObject(
stringifiedFileContent,
FileContentReference,
);
if (fileContent == null) return;
this.handleFileContent(fileContent);
};
handleFileContent = (fileContent: FileContent<string>): void => {
const didStore: boolean = this.storeFileContent(fileContent);
if (didStore == false) return;
this.boardsAndTasksModel.handleFileContent(fileContent);
};
// methods
addFileContentAndSend = (fileContent: FileContent<string>): void => {
this.handleFileContent(fileContent);
this.chatModel.sendMessage("", fileContent);
};
// storage
storeFileContent = (fileContent: FileContent<string>): boolean => {
const fileContentPath: string[] = this.getFileContentPath(
fileContent.fileId,
fileContent.fileContentId,
);
// check if fileContent already exists
const existingFileContent: string | null =
this.storageModel.read(fileContentPath);
if (existingFileContent != null) return false;
const stringifiedContent: string = stringify(fileContent);
this.storageModel.write(fileContentPath, stringifiedContent);
return true;
};
listFileIds = (): string[] => {
return this.storageModel.list(this.getBasePath());
};
listFileContentIds = (fileId: string): string[] => {
const filePath: string[] = this.getFilePath(fileId);
return this.storageModel.list(filePath);
};
selectLatestFileContentId = (
fileContentIds: string[],
): string | undefined => {
return fileContentIds[fileContentIds.length - 1];
};
getFileContent = <T extends FileContent<string>>(
fileId: string,
fileContentName: string,
reference: T,
): T | null => {
const filePath: string[] = this.getFileContentPath(
fileId,
fileContentName,
);
const fileContentOrNull: T | null = this.storageModel.readStringifiable(
filePath,
reference,
);
return fileContentOrNull;
};
getLatestFileContent = <T extends FileContent<string>>(
fileId: string,
reference: T,
): T | null => {
const fileContentsIds: string[] = this.listFileContentIds(fileId);
const latestFileContentId: string | undefined =
this.selectLatestFileContentId(fileContentsIds);
if (latestFileContentId == undefined) return null;
const fileContent: T | null = this.getFileContent(
fileId,
latestFileContentId,
reference,
);
return fileContent;
};
// init
constructor(
storageModel: StorageModel,
settingsModel: SettingsModel,
chatModel: ChatModel,
) {
this.chatModel = chatModel;
this.settingsModel = settingsModel;
this.storageModel = storageModel;
this.boardsAndTasksModel = new BoardsAndTasksModel(
this.storageModel,
this.settingsModel,
chatModel,
this,
);
}
// utility
static generateFileContentId = (creationDate: string): string => {
return creationDate + v4();
};
static createFileContent = <T extends string>(
fileId: string,
type: T,
): FileContent<T> => {
const creationDate: string = createTimestamp();
const fileContentId: string =
FileModel.generateFileContentId(creationDate);
return {
dataVersion: DATA_VERSION,
fileId,
fileContentId,
creationDate,
type,
};
};
};
}
// paths
export enum FileModelSubPath {
Data = "data",
Model = "model",
ModelView = "view",
ModelTask = "tasks",
ModelCalendar = "calendar",
Data = "data",
Model = "model",
ModelView = "view",
ModelTask = "tasks",
ModelCalendar = "calendar",
}
// types
export interface FileContent<T extends string>
extends ValidObject,
StringEntryObject {
fileId: string;
fileContentId: string;
creationDate: string;
extends ValidObject, StringEntryObject {
fileId: string;
fileContentId: string;
creationDate: string;
type: T;
type: T;
}
// references
export const FileContentReference: FileContent<string> = {
dataVersion: DATA_VERSION,
dataVersion: DATA_VERSION,
fileId: "",
fileContentId: "",
creationDate: "",
fileId: "",
fileContentId: "",
creationDate: "",
type: "",
type: "",
};
+257 -234
View File
@@ -6,255 +6,278 @@ import StorageModel, { StorageModelSubPath, filePaths } from "./storageModel";
import UDNFrontend, { Message } from "udn-frontend";
export default class ConnectionModel {
udn: UDNFrontend;
storageModel: StorageModel;
udn: UDNFrontend;
storageModel: StorageModel;
reconnectInterval: number | undefined = undefined;
shouldAttemptReconnect: boolean = false;
// data
get isConnected(): boolean {
return this.udn.ws != undefined && this.udn.ws.readyState == 1;
}
get address(): string | undefined {
return this.udn.ws?.url;
}
connectionChangeHandlerManager: HandlerManager<void> = new HandlerManager();
messageHandlerManager: HandlerManager<Message> = new HandlerManager();
messageSentHandlerManager: HandlerManager<ChatMessage> = new HandlerManager();
channelsToSubscribe: Set<string> = new Set();
// handlers
handleMessage = (data: Message): void => {
this.messageHandlerManager.trigger(data);
};
handleConnectionChange = (): void => {
console.log("connection status:", this.isConnected, this.address);
this.connectionChangeHandlerManager.trigger();
if (this.isConnected == false) return;
if (this.address == undefined) return;
this.storeAddress(this.address);
this.sendSubscriptionRequest();
this.sendMessagesInOutbox();
};
// connection
connect = (address: string): void => {
this.udn.connect(address);
};
disconnect = (): void => {
this.udn.disconnect();
// do not reconnect
const reconnectAddressPath: string[] = StorageModel.getPath(
StorageModelSubPath.ConnectionModel,
filePaths.connectionModel.reconnectAddress
);
this.storageModel.remove(reconnectAddressPath);
};
// mailbox
getMailboxPath = (address: string): string[] => {
const mailboxDirPath = StorageModel.getPath(
StorageModelSubPath.ConnectionModel,
filePaths.connectionModel.mailboxes
);
const mailboxFilePath = [...mailboxDirPath, address];
return mailboxFilePath;
};
requestNewMailbox = (): void => {
console.log("requesting new mailbox");
this.udn.requestMailbox();
};
connectMailbox = (): void => {
if (this.address == undefined) return;
const mailboxId = this.storageModel.read(this.getMailboxPath(this.address));
console.log("connecting mailbox", mailboxId);
if (mailboxId == null) return this.requestNewMailbox();
this.udn.connectMailbox(mailboxId);
};
storeMailbox = (mailboxId: string): void => {
if (this.address == undefined) return;
this.storageModel.write(this.getMailboxPath(this.address), mailboxId);
};
// subscription
addChannel = (channel: string): void => {
this.channelsToSubscribe.add(channel);
this.sendSubscriptionRequest();
};
sendSubscriptionRequest = (): void => {
if (this.isConnected == false) return;
for (const channel of this.channelsToSubscribe) {
this.udn.subscribe(channel);
// data
get isConnected(): boolean {
return this.udn.ws != undefined && this.udn.ws.readyState == 1;
}
// update mailbox
this.connectMailbox();
};
// outbox
getOutboxPath = (): string[] => {
return StorageModel.getPath(
StorageModelSubPath.ConnectionModel,
filePaths.connectionModel.outbox
);
};
get address(): string | undefined {
return this.udn.ws?.url;
}
getOutboxMessags = (): ChatMessage[] => {
const outboxPath: string[] = this.getOutboxPath();
const messageIds: string[] = this.storageModel.list(outboxPath);
connectionChangeHandlerManager: HandlerManager<void> = new HandlerManager();
messageHandlerManager: HandlerManager<Message> = new HandlerManager();
messageSentHandlerManager: HandlerManager<ChatMessage> =
new HandlerManager();
let chatMessages: ChatMessage[] = [];
for (const messageId of messageIds) {
const chatMessage: ChatMessage | null =
this.storageModel.readStringifiable(
[...outboxPath, messageId],
ChatMessageReference
channelsToSubscribe: Set<string> = new Set();
// handlers
handleMessage = (data: Message): void => {
this.messageHandlerManager.trigger(data);
};
handleConnectionChange = (): void => {
console.log("connection status:", this.isConnected, this.address);
this.connectionChangeHandlerManager.trigger();
if (this.isConnected == false) {
if (this.shouldAttemptReconnect == false) return;
this.reconnectInterval = setInterval(() => {
if (this.isConnected == true) {
clearInterval(this.reconnectInterval);
return;
}
this.reconnect();
}, 5000);
return;
}
if (this.address == undefined) return;
this.storeAddress(this.address);
this.sendSubscriptionRequest();
this.sendMessagesInOutbox();
this.shouldAttemptReconnect = true;
};
// connection
connect = (address: string): void => {
this.udn.connect(address);
};
disconnect = (): void => {
this.udn.disconnect();
// do not reconnect
const reconnectAddressPath: string[] = StorageModel.getPath(
StorageModelSubPath.ConnectionModel,
filePaths.connectionModel.reconnectAddress,
);
this.storageModel.remove(reconnectAddressPath);
this.shouldAttemptReconnect = false;
};
if (chatMessage == null) continue;
chatMessages.push(chatMessage);
reconnect = (): void => {
const reconnectAddressPath: string[] = this.getReconnectAddressPath();
const reconnectAddress: string | null =
this.storageModel.read(reconnectAddressPath);
if (reconnectAddress == null) return;
console.log("reconnecting...");
this.connect(reconnectAddress);
};
// mailbox
getMailboxPath = (address: string): string[] => {
const mailboxDirPath = StorageModel.getPath(
StorageModelSubPath.ConnectionModel,
filePaths.connectionModel.mailboxes,
);
const mailboxFilePath = [...mailboxDirPath, address];
return mailboxFilePath;
};
requestNewMailbox = (): void => {
console.log("requesting new mailbox");
this.udn.requestMailbox();
};
connectMailbox = (): void => {
if (this.address == undefined) return;
const mailboxId = this.storageModel.read(
this.getMailboxPath(this.address),
);
console.log("connecting mailbox", mailboxId);
if (mailboxId == null) return this.requestNewMailbox();
this.udn.connectMailbox(mailboxId);
};
storeMailbox = (mailboxId: string): void => {
if (this.address == undefined) return;
this.storageModel.write(this.getMailboxPath(this.address), mailboxId);
};
// subscription
addChannel = (channel: string): void => {
this.channelsToSubscribe.add(channel);
this.sendSubscriptionRequest();
};
sendSubscriptionRequest = (): void => {
if (this.isConnected == false) return;
for (const channel of this.channelsToSubscribe) {
this.udn.subscribe(channel);
}
// update mailbox
this.connectMailbox();
};
// outbox
getOutboxPath = (): string[] => {
return StorageModel.getPath(
StorageModelSubPath.ConnectionModel,
filePaths.connectionModel.outbox,
);
};
getOutboxMessags = (): ChatMessage[] => {
const outboxPath: string[] = this.getOutboxPath();
const messageIds: string[] = this.storageModel.list(outboxPath);
let chatMessages: ChatMessage[] = [];
for (const messageId of messageIds) {
const chatMessage: ChatMessage | null =
this.storageModel.readStringifiable(
[...outboxPath, messageId],
ChatMessageReference,
);
if (chatMessage == null) continue;
chatMessages.push(chatMessage);
}
return chatMessages;
};
addToOutbox = (chatMessage: ChatMessage): void => {
const messagePath: string[] = [...this.getOutboxPath(), chatMessage.id];
this.storageModel.writeStringifiable(messagePath, chatMessage);
};
removeFromOutbox = (chatMessage: ChatMessage): void => {
const messagePath: string[] = [...this.getOutboxPath(), chatMessage.id];
this.storageModel.remove(messagePath);
};
sendMessagesInOutbox = (): void => {
const messages: ChatMessage[] = this.getOutboxMessags();
for (const message of messages) {
const isSent: boolean = this.tryToSendMessage(message);
if (isSent == false) return;
this.removeFromOutbox(message);
}
};
// messaging
sendMessageOrStore = (chatMessage: ChatMessage): void => {
const isSent = this.tryToSendMessage(chatMessage);
if (isSent == true) return;
this.addToOutbox(chatMessage);
};
tryToSendMessage = (chatMessage: ChatMessage): boolean => {
const stringifiedBody: string = stringify(chatMessage);
const isSent: boolean = this.sendPlainMessage(
chatMessage.channel,
stringifiedBody,
);
if (isSent) this.messageSentHandlerManager.trigger(chatMessage);
return isSent;
};
sendPlainMessage = (channel: string, body: string): boolean => {
return this.udn.sendMessage(channel, body);
};
// storage
getPreviousAddressPath = (): string[] => {
return StorageModel.getPath(
StorageModelSubPath.ConnectionModel,
filePaths.connectionModel.previousAddresses,
);
};
getAddressPath = (address: string): string[] => {
const dirPath = this.getPreviousAddressPath();
return [...dirPath, address];
};
getReconnectAddressPath = (): string[] => {
return StorageModel.getPath(
StorageModelSubPath.ConnectionModel,
filePaths.connectionModel.reconnectAddress,
);
};
storeAddress = (address: string): void => {
// history
const addressPath = this.getAddressPath(address);
this.storageModel.write(addressPath, "");
// reconnect
const reconnectAddressPath: string[] = this.getReconnectAddressPath();
this.storageModel.write(reconnectAddressPath, address);
};
removeAddress = (address: string): void => {
const addressPath = this.getAddressPath(address);
this.storageModel.remove(addressPath);
};
get addresses(): string[] {
const dirPath = this.getPreviousAddressPath();
return this.storageModel.list(dirPath);
}
return chatMessages;
};
// init
constructor(storageModel: StorageModel) {
// create frontend
this.udn = new UDNFrontend();
this.storageModel = storageModel;
addToOutbox = (chatMessage: ChatMessage): void => {
const messagePath: string[] = [...this.getOutboxPath(), chatMessage.id];
this.storageModel.writeStringifiable(messagePath, chatMessage);
};
// setup handlers
this.udn.onmessage = (data: Message) => {
this.handleMessage(data);
};
this.udn.onconnect = () => {
this.handleConnectionChange();
};
this.udn.ondisconnect = () => {
this.handleConnectionChange();
};
removeFromOutbox = (chatMessage: ChatMessage): void => {
const messagePath: string[] = [...this.getOutboxPath(), chatMessage.id];
this.storageModel.remove(messagePath);
};
this.udn.onmailboxcreate = (mailboxId: string) => {
console.log("created mailbox", mailboxId);
this.storeMailbox(mailboxId);
this.connectMailbox();
};
this.udn.onmailboxdelete = (mailboxId: string) => {
console.log(`mailbox ${mailboxId} deleted`);
this.requestNewMailbox();
};
this.udn.onmailboxconnect = (mailboxId: string) => {
console.log(`using mailbox ${mailboxId}`);
};
sendMessagesInOutbox = (): void => {
const messages: ChatMessage[] = this.getOutboxMessags();
for (const message of messages) {
const isSent: boolean = this.tryToSendMessage(message);
if (isSent == false) return;
this.removeFromOutbox(message);
this.reconnect();
}
};
// messaging
sendMessageOrStore = (chatMessage: ChatMessage): void => {
const isSent = this.tryToSendMessage(chatMessage);
if (isSent == true) return;
this.addToOutbox(chatMessage);
};
tryToSendMessage = (chatMessage: ChatMessage): boolean => {
const stringifiedBody: string = stringify(chatMessage);
const isSent: boolean = this.sendPlainMessage(
chatMessage.channel,
stringifiedBody
);
if (isSent) this.messageSentHandlerManager.trigger(chatMessage);
return isSent;
};
sendPlainMessage = (channel: string, body: string): boolean => {
return this.udn.sendMessage(channel, body);
};
// storage
getPreviousAddressPath = (): string[] => {
return StorageModel.getPath(
StorageModelSubPath.ConnectionModel,
filePaths.connectionModel.previousAddresses
);
};
getAddressPath = (address: string): string[] => {
const dirPath = this.getPreviousAddressPath();
return [...dirPath, address];
};
getReconnectAddressPath = (): string[] => {
return StorageModel.getPath(
StorageModelSubPath.ConnectionModel,
filePaths.connectionModel.reconnectAddress
);
};
storeAddress = (address: string): void => {
// history
const addressPath = this.getAddressPath(address);
this.storageModel.write(addressPath, "");
// reconnect
const reconnectAddressPath: string[] = this.getReconnectAddressPath();
this.storageModel.write(reconnectAddressPath, address);
};
removeAddress = (address: string): void => {
const addressPath = this.getAddressPath(address);
this.storageModel.remove(addressPath);
};
get addresses(): string[] {
const dirPath = this.getPreviousAddressPath();
return this.storageModel.list(dirPath);
}
// init
constructor(storageModel: StorageModel) {
// create frontend
this.udn = new UDNFrontend();
this.storageModel = storageModel;
// setup handlers
this.udn.onmessage = (data: Message) => {
this.handleMessage(data);
};
this.udn.onconnect = () => {
this.handleConnectionChange();
};
this.udn.ondisconnect = () => {
this.handleConnectionChange();
};
this.udn.onmailboxcreate = (mailboxId: string) => {
console.log("created mailbox", mailboxId);
this.storeMailbox(mailboxId);
this.connectMailbox();
};
this.udn.onmailboxdelete = (mailboxId: string) => {
console.log(`mailbox ${mailboxId} deleted`);
this.requestNewMailbox();
};
this.udn.onmailboxconnect = (mailboxId: string) => {
console.log(`using mailbox ${mailboxId}`);
};
// reconnect
const reconnectAddressPath: string[] = this.getReconnectAddressPath();
const reconnectAddress: string | null =
storageModel.read(reconnectAddressPath);
if (reconnectAddress != null) {
this.connect(reconnectAddress);
}
}
}
// paths
export enum settingsModelStorageFiles {
username = "user-name",
firstDayOfWeek = "first-day-of-week",
username = "user-name",
firstDayOfWeek = "first-day-of-week",
}
+97 -95
View File
@@ -1,10 +1,10 @@
// this file is responsible for sending and handling files.
import {
HandlerManager,
generateRandomToken,
parse,
stringify,
HandlerManager,
generateRandomToken,
parse,
stringify,
} from "../Utility/utility";
import { decryptString, encryptString } from "../Utility/crypto";
@@ -14,118 +14,120 @@ import StorageModel from "./storageModel";
import { checkMatchesObjectStructure } from "../Utility/typeSafety";
export default class FileTransferModel {
storageModel: StorageModel;
connectionModel: ConnectionModel;
storageModel: StorageModel;
connectionModel: ConnectionModel;
// data
transferData: TransferData | undefined;
fileHandlerManager: HandlerManager<string> = new HandlerManager();
// data
transferData: TransferData | undefined;
fileHandlerManager: HandlerManager<string> = new HandlerManager();
// general
generateTransferData = (): TransferData => {
const transferData: TransferData = {
channel: generateRandomToken(2).substring(0, 4),
key: generateRandomToken(3).substring(0, 6),
// general
generateTransferData = (): TransferData => {
const transferData: TransferData = {
channel: generateRandomToken(2).substring(0, 4),
key: generateRandomToken(3).substring(0, 6),
};
this.transferData = transferData;
return transferData;
};
this.transferData = transferData;
return transferData;
};
prepareToReceive = (transferData: TransferData) => {
this.connectionModel.addChannel(transferData.channel);
this.transferData = transferData;
};
prepareToReceive = (transferData: TransferData) => {
this.connectionModel.addChannel(transferData.channel);
this.transferData = transferData;
};
// handlers
handleMessage = (data: Message): void => {
if (this.transferData == undefined) return;
if (data.messageBody == undefined) return;
if (data.messageChannel != this.transferData.channel) return;
// handlers
handleMessage = (data: Message): void => {
if (this.transferData == undefined) return;
if (data.messageBody == undefined) return;
if (data.messageChannel != this.transferData.channel) return;
this.handleFile(data.messageBody);
};
this.handleFile(data.messageBody);
};
handleFile = async (encryptedFileData: string): Promise<void> => {
if (this.transferData == undefined) return;
handleFile = async (encryptedFileData: string): Promise<void> => {
if (this.transferData == undefined) return;
const decrypted: string = await decryptString(
encryptedFileData,
this.transferData.key
);
console.log(decrypted);
const parsed: any = parse(decrypted);
const isFileData: boolean = checkMatchesObjectStructure(
parsed,
FileDataReference
);
if (isFileData == false) return;
const fileData: FileData = parsed;
this.storageModel.write(fileData.path, fileData.body);
const pathString: string = StorageModel.pathComponentsToString(
...fileData.path
);
this.fileHandlerManager.trigger(pathString);
};
// sending
sendFiles = (
directoryPaths: IteratorObject<string[]>,
callback: (path: string) => void
): void => {
for (const directoryPath of directoryPaths) {
this.storageModel.recurse(directoryPath, (filePath: string[]) => {
this.sendFile(filePath);
const pathString: string = StorageModel.pathComponentsToString(
...filePath
const decrypted: string = await decryptString(
encryptedFileData,
this.transferData.key,
);
callback(pathString);
});
}
};
console.log(decrypted);
sendFile = async (filePath: string[]): Promise<void> => {
if (this.transferData == undefined) return;
const fileContent: string | null = this.storageModel.read(filePath);
if (fileContent == null) return;
const parsed: any = parse(decrypted);
const isFileData: boolean = checkMatchesObjectStructure(
parsed,
FileDataReference,
);
if (isFileData == false) return;
const fileData: FileData = {
path: filePath,
body: fileContent,
const fileData: FileData = parsed;
this.storageModel.write(fileData.path, fileData.body);
const pathString: string = StorageModel.pathComponentsToString(
...fileData.path,
);
this.fileHandlerManager.trigger(pathString);
};
const stringifiedFileData: string = stringify(fileData);
const encryptedFileData: string = await encryptString(
stringifiedFileData,
this.transferData.key
);
this.connectionModel.sendPlainMessage(
this.transferData.channel,
encryptedFileData
);
};
// init
constructor(storageModel: StorageModel, connectionModel: ConnectionModel) {
this.storageModel = storageModel;
this.connectionModel = connectionModel;
// sending
sendFiles = (
directoryPaths: IteratorObject<string[]>,
callback: (path: string) => void,
): void => {
for (const directoryPath of directoryPaths) {
this.storageModel.recurse(directoryPath, (filePath: string[]) => {
this.sendFile(filePath);
const pathString: string = StorageModel.pathComponentsToString(
...filePath,
);
callback(pathString);
});
}
};
this.connectionModel.messageHandlerManager.addHandler(this.handleMessage);
}
sendFile = async (filePath: string[]): Promise<void> => {
if (this.transferData == undefined) return;
const fileContent: string | null = this.storageModel.read(filePath);
if (fileContent == null) return;
const fileData: FileData = {
path: filePath,
body: fileContent,
};
const stringifiedFileData: string = stringify(fileData);
const encryptedFileData: string = await encryptString(
stringifiedFileData,
this.transferData.key,
);
this.connectionModel.sendPlainMessage(
this.transferData.channel,
encryptedFileData,
);
};
// init
constructor(storageModel: StorageModel, connectionModel: ConnectionModel) {
this.storageModel = storageModel;
this.connectionModel = connectionModel;
this.connectionModel.messageHandlerManager.addHandler(
this.handleMessage,
);
}
}
export interface TransferData {
channel: string;
key: string;
channel: string;
key: string;
}
export interface FileData {
path: string[];
body: string;
path: string[];
body: string;
}
export const FileDataReference: FileData = {
path: [""],
body: "",
path: [""],
body: "",
};
+44 -44
View File
@@ -3,55 +3,55 @@
import StorageModel, { StorageModelSubPath, filePaths } from "./storageModel";
export default class SettingsModel {
storageModel: StorageModel;
storageModel: StorageModel;
// data
username: string;
firstDayOfWeek: string;
// data
username: string;
firstDayOfWeek: string;
// storage
setName(newValue: string): void {
this.username = newValue;
const path = StorageModel.getPath(
StorageModelSubPath.SettingsModel,
filePaths.settingsModel.username
);
this.storageModel.write(path, newValue);
}
// storage
setName(newValue: string): void {
this.username = newValue;
const path = StorageModel.getPath(
StorageModelSubPath.SettingsModel,
filePaths.settingsModel.username,
);
this.storageModel.write(path, newValue);
}
setFirstDayOfWeek(newValue: string): void {
this.firstDayOfWeek = newValue;
const path = StorageModel.getPath(
StorageModelSubPath.SettingsModel,
filePaths.settingsModel.firstDayOfWeek
);
this.storageModel.write(path, newValue);
}
setFirstDayOfWeek(newValue: string): void {
this.firstDayOfWeek = newValue;
const path = StorageModel.getPath(
StorageModelSubPath.SettingsModel,
filePaths.settingsModel.firstDayOfWeek,
);
this.storageModel.write(path, newValue);
}
// load
loadUsernam(): void {
const path = StorageModel.getPath(
StorageModelSubPath.SettingsModel,
filePaths.settingsModel.username
);
const content = this.storageModel.read(path);
this.username = content ?? "";
}
// load
loadUsernam(): void {
const path = StorageModel.getPath(
StorageModelSubPath.SettingsModel,
filePaths.settingsModel.username,
);
const content = this.storageModel.read(path);
this.username = content ?? "";
}
loadFirstDayofWeek(): void {
const path = StorageModel.getPath(
StorageModelSubPath.SettingsModel,
filePaths.settingsModel.firstDayOfWeek
);
const content = this.storageModel.read(path);
this.firstDayOfWeek = content ?? "0";
}
loadFirstDayofWeek(): void {
const path = StorageModel.getPath(
StorageModelSubPath.SettingsModel,
filePaths.settingsModel.firstDayOfWeek,
);
const content = this.storageModel.read(path);
this.firstDayOfWeek = content ?? "0";
}
// init
constructor(storageModel: StorageModel) {
this.storageModel = storageModel;
// init
constructor(storageModel: StorageModel) {
this.storageModel = storageModel;
this.loadUsernam();
this.loadFirstDayofWeek();
}
this.loadUsernam();
this.loadFirstDayofWeek();
}
}
+229 -219
View File
@@ -6,207 +6,217 @@ import { localeCompare, parseValidObject, stringify } from "../Utility/utility";
export const PATH_COMPONENT_SEPARATOR = "\\";
export default class StorageModel {
storageEntryTree: StorageEntry = {};
storageEntryTree: StorageEntry = {};
// read
read = (pathComponents: string[]): string | null => {
const pathString: string = StorageModel.pathComponentsToString(
...pathComponents
);
return localStorage.getItem(pathString);
};
// read
read = (pathComponents: string[]): string | null => {
const pathString: string = StorageModel.pathComponentsToString(
...pathComponents,
);
return localStorage.getItem(pathString);
};
list = (pathComponents: string[]): string[] => {
let currentParent: StorageEntry = this.storageEntryTree;
for (const component of pathComponents) {
const nextParent: StorageEntry | undefined = currentParent[component];
if (nextParent == undefined) return [];
currentParent = nextParent;
list = (pathComponents: string[]): string[] => {
let currentParent: StorageEntry = this.storageEntryTree;
for (const component of pathComponents) {
const nextParent: StorageEntry | undefined =
currentParent[component];
if (nextParent == undefined) return [];
currentParent = nextParent;
}
return [...Object.keys(currentParent).sort(localeCompare)];
};
// write
write = (pathComponents: string[], value: string): void => {
const pathString: string = StorageModel.pathComponentsToString(
...pathComponents,
);
localStorage.setItem(pathString, value);
this.updateTree(...pathComponents);
};
remove = (
pathComponents: string[],
shouldInitialize: boolean = true,
): void => {
const pathString: string = StorageModel.pathComponentsToString(
...pathComponents,
);
localStorage.removeItem(pathString);
if (shouldInitialize == true) {
this.initializeTree();
}
};
rename = (
sourcePathComponents: string[],
destinationPathComponents: string[],
shouldInitialize: boolean = true,
): boolean => {
const content: string | null = this.read(sourcePathComponents);
if (content == null) return false;
this.write(destinationPathComponents, content);
this.remove(sourcePathComponents);
if (shouldInitialize == true) {
this.initializeTree();
}
return true;
};
// recursion
recurse = (rootDirectory: string[], fn: (path: string[]) => void): void => {
loop_over_files: for (const key of Object.keys(localStorage)) {
// get path of current entity
const pathComponentsOfCurrentEntity: string[] =
StorageModel.stringToPathComponents(key);
// exit if entity does not match
loop_over_path_components: for (
let i = 0;
i < rootDirectory.length;
i++
) {
if (!pathComponentsOfCurrentEntity[i]) continue loop_over_files;
if (pathComponentsOfCurrentEntity[i] != rootDirectory[i])
continue loop_over_files;
}
// execute
fn(pathComponentsOfCurrentEntity);
}
this.initializeTree();
};
removeRecursively = (pathComponents: string[]): void => {
this.recurse(pathComponents, (path: string[]) =>
this.remove(path, false),
);
this.initializeTree();
};
renameRecursively = (
sourcePathComponents: string[],
destinationPathComponents: string[],
): void => {
this.recurse(sourcePathComponents, (path: string[]) => {
// assemble destinationPath
const relativePathOfCurrentEntity = path.slice(
sourcePathComponents.length,
);
const destinationPathComponentsOfCurrentEntity = [
...destinationPathComponents,
...relativePathOfCurrentEntity,
];
// rename
this.rename(path, destinationPathComponentsOfCurrentEntity, false);
});
this.initializeTree();
};
// stringifiable
writeStringifiable = (
pathComponents: string[],
value: ValidObject,
): void => {
const valueString: string = stringify(value);
this.write(pathComponents, valueString);
};
readStringifiable = <T extends ValidObject>(
pathComponents: string[],
reference: T,
): T | null => {
const valueString: string | null = this.read(pathComponents);
if (!valueString) return null;
const object: any | null = parseValidObject(valueString, reference);
if (object == null) return null;
return object;
};
// cleaning
removeJunk = (): void => {
this.recurse([], (path: string[]) => {
if (path[0] == DATA_VERSION) return;
this.remove(path);
});
};
// tree
initializeTree = (): void => {
console.log("initializing tree");
this.storageEntryTree = {};
for (const key of Object.keys(localStorage)) {
const components: string[] =
StorageModel.stringToPathComponents(key);
this.updateTree(...components);
}
};
updateTree = (...pathComponents: string[]): void => {
let currentParent: StorageEntry = this.storageEntryTree;
for (const pathPart of pathComponents) {
if (!currentParent[pathPart]) {
currentParent[pathPart] = {};
}
currentParent = currentParent[pathPart];
}
};
printTree = (): string => {
return stringify(this.storageEntryTree);
};
// init
constructor() {
this.initializeTree();
}
return [...Object.keys(currentParent).sort(localeCompare)];
};
// utility
static getFileName = (pathComponents: string[]): string => {
return pathComponents[pathComponents.length - 1] || "\\";
};
// write
write = (pathComponents: string[], value: string): void => {
const pathString: string = StorageModel.pathComponentsToString(
...pathComponents
);
localStorage.setItem(pathString, value);
this.updateTree(...pathComponents);
};
static getFileNameFromString = (pathString: string): string => {
const pathComponents: string[] =
this.stringToPathComponents(pathString);
return pathComponents[pathComponents.length - 1] || "\\";
};
remove = (
pathComponents: string[],
shouldInitialize: boolean = true
): void => {
const pathString: string = StorageModel.pathComponentsToString(
...pathComponents
);
localStorage.removeItem(pathString);
static pathComponentsToString = (...pathComponents: string[]): string => {
return pathComponents
.filter((x) => x != "")
.join(PATH_COMPONENT_SEPARATOR);
};
if (shouldInitialize == true) {
this.initializeTree();
static stringToPathComponents = (string: string): string[] => {
return string.split(PATH_COMPONENT_SEPARATOR).filter((x) => x != "");
};
static join = (...items: string[]): string => {
let allComponents: string[] = [];
for (const item of items) {
const parts = this.stringToPathComponents(item);
allComponents.push(...parts);
}
return StorageModel.pathComponentsToString(...allComponents);
};
static getPath(
locationName: StorageModelSubPath,
filePath: string[],
): string[] {
return [DATA_VERSION, locationName, ...filePath];
}
};
rename = (
sourcePathComponents: string[],
destinationPathComponents: string[],
shouldInitialize: boolean = true
): boolean => {
const content: string | null = this.read(sourcePathComponents);
if (content == null) return false;
this.write(destinationPathComponents, content);
this.remove(sourcePathComponents);
if (shouldInitialize == true) {
this.initializeTree();
}
return true;
};
// recursion
recurse = (rootDirectory: string[], fn: (path: string[]) => void): void => {
loop_over_files: for (const key of Object.keys(localStorage)) {
// get path of current entity
const pathComponentsOfCurrentEntity: string[] =
StorageModel.stringToPathComponents(key);
// exit if entity does not match
loop_over_path_components: for (
let i = 0;
i < rootDirectory.length;
i++
) {
if (!pathComponentsOfCurrentEntity[i]) continue loop_over_files;
if (pathComponentsOfCurrentEntity[i] != rootDirectory[i])
continue loop_over_files;
}
// execute
fn(pathComponentsOfCurrentEntity);
}
this.initializeTree();
};
removeRecursively = (pathComponents: string[]): void => {
this.recurse(pathComponents, (path: string[]) => this.remove(path, false));
this.initializeTree();
};
renameRecursively = (
sourcePathComponents: string[],
destinationPathComponents: string[]
): void => {
this.recurse(sourcePathComponents, (path: string[]) => {
// assemble destinationPath
const relativePathOfCurrentEntity = path.slice(
sourcePathComponents.length
);
const destinationPathComponentsOfCurrentEntity = [
...destinationPathComponents,
...relativePathOfCurrentEntity,
];
// rename
this.rename(path, destinationPathComponentsOfCurrentEntity, false);
});
this.initializeTree();
};
// stringifiable
writeStringifiable = (pathComponents: string[], value: ValidObject): void => {
const valueString: string = stringify(value);
this.write(pathComponents, valueString);
};
readStringifiable = <T extends ValidObject>(
pathComponents: string[],
reference: T
): T | null => {
const valueString: string | null = this.read(pathComponents);
if (!valueString) return null;
const object: any | null = parseValidObject(valueString, reference);
if (object == null) return null;
return object;
};
// cleaning
removeJunk = (): void => {
this.recurse([], (path: string[]) => {
if (path[0] == DATA_VERSION) return;
this.remove(path);
})
}
// tree
initializeTree = (): void => {
console.log("initializing tree");
this.storageEntryTree = {};
for (const key of Object.keys(localStorage)) {
const components: string[] = StorageModel.stringToPathComponents(key);
this.updateTree(...components);
}
};
updateTree = (...pathComponents: string[]): void => {
let currentParent: StorageEntry = this.storageEntryTree;
for (const pathPart of pathComponents) {
if (!currentParent[pathPart]) {
currentParent[pathPart] = {};
}
currentParent = currentParent[pathPart];
}
};
printTree = (): string => {
return stringify(this.storageEntryTree);
};
// init
constructor() {
this.initializeTree();
}
// utility
static getFileName = (pathComponents: string[]): string => {
return pathComponents[pathComponents.length - 1] || "\\";
};
static getFileNameFromString = (pathString: string): string => {
const pathComponents: string[] = this.stringToPathComponents(pathString);
return pathComponents[pathComponents.length - 1] || "\\";
};
static pathComponentsToString = (...pathComponents: string[]): string => {
return pathComponents.filter((x) => x != "").join(PATH_COMPONENT_SEPARATOR);
};
static stringToPathComponents = (string: string): string[] => {
return string.split(PATH_COMPONENT_SEPARATOR).filter((x) => x != "");
};
static join = (...items: string[]): string => {
let allComponents: string[] = [];
for (const item of items) {
const parts = this.stringToPathComponents(item);
allComponents.push(...parts);
}
return StorageModel.pathComponentsToString(...allComponents);
};
static getPath(
locationName: StorageModelSubPath,
filePath: string[]
): string[] {
return [DATA_VERSION, locationName, ...filePath];
}
}
// types
@@ -214,37 +224,37 @@ export type StorageEntry = { [key: string]: StorageEntry };
// locations
export enum StorageModelSubPath {
Chat = "chat",
Chat = "chat",
ConnectionModel = "connection",
SettingsModel = "settings",
ConnectionModel = "connection",
SettingsModel = "settings",
}
export const filePaths = {
connectionModel: {
socketAddress: ["socket-address"],
reconnectAddress: ["reconnect-address"],
outbox: ["outbox"],
mailboxes: ["mailboxes"],
connectionModel: {
socketAddress: ["socket-address"],
reconnectAddress: ["reconnect-address"],
outbox: ["outbox"],
mailboxes: ["mailboxes"],
previousAddresses: ["previous-addresses"],
},
previousAddresses: ["previous-addresses"],
},
chat: {
base: [],
chatBase: (id: string) => [id],
info: (id: string) => [...filePaths.chat.chatBase(id), "info"],
color: (id: string) => [...filePaths.chat.chatBase(id), "color"],
messages: (id: string) => [...filePaths.chat.chatBase(id), "messages"],
lastUsedPage: (id: string) => [
...filePaths.chat.chatBase(id),
"last-used-page",
],
files: (id: string) => [...filePaths.chat.chatBase(id), "files"],
},
chat: {
base: [],
chatBase: (id: string) => [id],
info: (id: string) => [...filePaths.chat.chatBase(id), "info"],
color: (id: string) => [...filePaths.chat.chatBase(id), "color"],
messages: (id: string) => [...filePaths.chat.chatBase(id), "messages"],
lastUsedPage: (id: string) => [
...filePaths.chat.chatBase(id),
"last-used-page",
],
files: (id: string) => [...filePaths.chat.chatBase(id), "files"],
},
settingsModel: {
username: ["user-name"],
firstDayOfWeek: ["first-day-of-week"],
},
settingsModel: {
username: ["user-name"],
firstDayOfWeek: ["first-day-of-week"],
},
};
+60 -60
View File
@@ -3,113 +3,113 @@ const ENCRYPTION_ALG = "AES-GCM";
// minimal
export async function encryptString(
plaintext: string,
passphrase: string
plaintext: string,
passphrase: string,
): Promise<string> {
if (!window.crypto.subtle) return plaintext;
if (!window.crypto.subtle) return plaintext;
const iv = generateIV();
const key = await importKey(passphrase, "encrypt");
const encryptedArray = await encrypt(iv, key, plaintext);
const iv = generateIV();
const key = await importKey(passphrase, "encrypt");
const encryptedArray = await encrypt(iv, key, plaintext);
const encryptionData = {
iv: uInt8ToArray(iv),
encryptedArray: uInt8ToArray(encryptedArray),
};
return btoa(JSON.stringify(encryptionData));
const encryptionData = {
iv: uInt8ToArray(iv),
encryptedArray: uInt8ToArray(encryptedArray),
};
return btoa(JSON.stringify(encryptionData));
}
export async function decryptString(
cyphertext: string,
passphrase: string
cyphertext: string,
passphrase: string,
): Promise<string> {
try {
const encrypionData = JSON.parse(atob(cyphertext));
const iv = arrayToUint8(encrypionData.iv);
const encryptedArray = arrayToUint8(encrypionData.encryptedArray);
try {
const encrypionData = JSON.parse(atob(cyphertext));
const iv = arrayToUint8(encrypionData.iv);
const encryptedArray = arrayToUint8(encrypionData.encryptedArray);
const key = await importKey(passphrase, "decrypt");
return await decrypt(iv, key, encryptedArray);
} catch {
return cyphertext;
}
const key = await importKey(passphrase, "decrypt");
return await decrypt(iv, key, encryptedArray);
} catch {
return cyphertext;
}
}
// process
export function encode(string: string): Uint8Array {
return new TextEncoder().encode(string);
return new TextEncoder().encode(string);
}
export function decode(array: Uint8Array): string {
return new TextDecoder("utf-8").decode(array);
return new TextDecoder("utf-8").decode(array);
}
export async function encrypt(
iv: Uint8Array,
key: CryptoKey,
message: string
iv: Uint8Array,
key: CryptoKey,
message: string,
): Promise<Uint8Array> {
const arrayBuffer = await window.crypto.subtle.encrypt(
{ name: ENCRYPTION_ALG, iv },
key,
encode(message)
);
return new Uint8Array(arrayBuffer);
const arrayBuffer = await window.crypto.subtle.encrypt(
{ name: ENCRYPTION_ALG, iv },
key,
encode(message),
);
return new Uint8Array(arrayBuffer);
}
export async function decrypt(
iv: Uint8Array,
key: CryptoKey,
cyphertext: Uint8Array
iv: Uint8Array,
key: CryptoKey,
cyphertext: Uint8Array,
): Promise<string> {
const arrayBuffer = await crypto.subtle.decrypt(
{ name: ENCRYPTION_ALG, iv },
key,
cyphertext
);
return arrayBufferToString(arrayBuffer);
const arrayBuffer = await crypto.subtle.decrypt(
{ name: ENCRYPTION_ALG, iv },
key,
cyphertext,
);
return arrayBufferToString(arrayBuffer);
}
export async function hash(encoded: Uint8Array): Promise<ArrayBuffer> {
return await crypto.subtle.digest("SHA-256", encoded);
return await crypto.subtle.digest("SHA-256", encoded);
}
// iv
export function generateIV(): Uint8Array {
return crypto.getRandomValues(new Uint8Array(IV_SIZE));
return crypto.getRandomValues(new Uint8Array(IV_SIZE));
}
export function splitMergedString(mergedString: string): [Uint8Array, string] {
const iv = mergedString.slice(0, IV_SIZE);
const encryptedString = mergedString.slice(IV_SIZE);
const iv = mergedString.slice(0, IV_SIZE);
const encryptedString = mergedString.slice(IV_SIZE);
return [JSON.parse(iv), encryptedString];
return [JSON.parse(iv), encryptedString];
}
// key
export async function importKey(
passphrase: string,
purpose: "encrypt" | "decrypt"
passphrase: string,
purpose: "encrypt" | "decrypt",
): Promise<CryptoKey> {
return await crypto.subtle.importKey(
"raw",
await hash(encode(passphrase)),
{ name: ENCRYPTION_ALG },
false,
[purpose]
);
return await crypto.subtle.importKey(
"raw",
await hash(encode(passphrase)),
{ name: ENCRYPTION_ALG },
false,
[purpose],
);
}
// conversion
export function arrayBufferToString(arrayBuffer: ArrayBuffer): string {
const uInt8Array = new Uint8Array(arrayBuffer);
return decode(uInt8Array);
const uInt8Array = new Uint8Array(arrayBuffer);
return decode(uInt8Array);
}
export function uInt8ToArray(uInt8Array: Uint8Array): number[] {
return Array.from(uInt8Array);
return Array.from(uInt8Array);
}
export function arrayToUint8(array: number[]): Uint8Array {
return new Uint8Array(array);
return new Uint8Array(array);
}
+35 -35
View File
@@ -1,48 +1,48 @@
export const DATA_VERSION = "v2";
export function checkIsValidObject(object: any): boolean {
return object.dataVersion == DATA_VERSION;
return object.dataVersion == DATA_VERSION;
}
export function checkMatchesObjectStructure(
objectToCheck: any,
reference: any
objectToCheck: any,
reference: any,
): boolean {
if (typeof reference != "object") {
return typeof objectToCheck == typeof reference;
}
for (const key of Object.keys(reference)) {
const requiredType = typeof reference[key];
const actualType = typeof objectToCheck[key];
if (requiredType != actualType) return false;
if (Array.isArray(reference[key]) || reference[key] instanceof Set) {
// only check if array is not empty
if (objectToCheck[key].length == 0) continue;
if (objectToCheck[key].size == 0) continue;
// check first item of array
const [firstOfObjectToCheck] = objectToCheck[key];
const [fisrtOfReference] = reference[key];
const doesFirstItemMatch = checkMatchesObjectStructure(
firstOfObjectToCheck,
fisrtOfReference
);
if (doesFirstItemMatch == false) return false;
} else if (requiredType == "object") {
// recurse into objects
const doesNestedObjectMatch = checkMatchesObjectStructure(
objectToCheck[key],
reference[key]
);
if (doesNestedObjectMatch == false) return false;
if (typeof reference != "object") {
return typeof objectToCheck == typeof reference;
}
}
return true;
for (const key of Object.keys(reference)) {
const requiredType = typeof reference[key];
const actualType = typeof objectToCheck[key];
if (requiredType != actualType) return false;
if (Array.isArray(reference[key]) || reference[key] instanceof Set) {
// only check if array is not empty
if (objectToCheck[key].length == 0) continue;
if (objectToCheck[key].size == 0) continue;
// check first item of array
const [firstOfObjectToCheck] = objectToCheck[key];
const [fisrtOfReference] = reference[key];
const doesFirstItemMatch = checkMatchesObjectStructure(
firstOfObjectToCheck,
fisrtOfReference,
);
if (doesFirstItemMatch == false) return false;
} else if (requiredType == "object") {
// recurse into objects
const doesNestedObjectMatch = checkMatchesObjectStructure(
objectToCheck[key],
reference[key],
);
if (doesNestedObjectMatch == false) return false;
}
}
return true;
}
export interface ValidObject {
dataVersion: "v2";
dataVersion: "v2";
}
+182 -181
View File
@@ -1,277 +1,278 @@
import { checkIsValidObject, checkMatchesObjectStructure } from "./typeSafety";
export interface Stringifiable {
toString(): string;
toString(): string;
}
// crypto
export function generateRandomToken(length: number): string {
const array = new Uint8Array(length);
crypto.getRandomValues(array);
return array.join("");
const array = new Uint8Array(length);
crypto.getRandomValues(array);
return array.join("");
}
// date
export function createTimestamp(): string {
return new Date().toISOString();
return new Date().toISOString();
}
// filters etc
export type StringEntryObject = { [key: string]: Stringifiable | undefined };
export function filterObjectsByStringEntries<T>(
reference: StringEntryObject,
converter: (object: T) => StringEntryObject,
objects: T[]
reference: StringEntryObject,
converter: (object: T) => StringEntryObject,
objects: T[],
): Set<T> {
const matches: Set<T> = new Set();
const matches: Set<T> = new Set();
object_loop: for (const object of objects) {
const doesMatch: boolean = checkDoesObjectMatchReference(
reference,
converter(object)
);
if (doesMatch) matches.add(object);
}
object_loop: for (const object of objects) {
const doesMatch: boolean = checkDoesObjectMatchReference(
reference,
converter(object),
);
if (doesMatch) matches.add(object);
}
return matches;
return matches;
}
export function checkDoesObjectMatchReference(
reference: StringEntryObject,
stringEntryObject: StringEntryObject
reference: StringEntryObject,
stringEntryObject: StringEntryObject,
): boolean {
reference_entry_loop: for (const referenceEntry of Object.entries(
reference
)) {
const [referenceKey, referenceValue] = referenceEntry;
const stringEntryObjectValue: Stringifiable | undefined =
stringEntryObject[referenceKey];
reference_entry_loop: for (const referenceEntry of Object.entries(
reference,
)) {
const [referenceKey, referenceValue] = referenceEntry;
const stringEntryObjectValue: Stringifiable | undefined =
stringEntryObject[referenceKey];
if (referenceValue == undefined) return false;
if (referenceValue == undefined) return false;
if (referenceValue[0] == "-") {
const strippedReferenceValue: string = referenceValue
.toString()
.substring(1);
// property may not exist
if (
strippedReferenceValue == "" &&
stringEntryObjectValue != undefined &&
stringEntryObjectValue != ""
) {
return false;
}
if (referenceValue[0] == "-") {
const strippedReferenceValue: string = referenceValue
.toString()
.substring(1);
// property may not exist
if (
strippedReferenceValue == "" &&
stringEntryObjectValue != undefined &&
stringEntryObjectValue != ""
) {
return false;
}
// property may not match
if (stringEntryObjectValue == strippedReferenceValue) {
return false;
}
} else {
// property must exist but be anything
if (
referenceValue == "" &&
(stringEntryObjectValue == undefined || stringEntryObjectValue == "")
) {
return false;
} else if (referenceValue == "") {
continue reference_entry_loop;
}
// property may not match
if (stringEntryObjectValue == strippedReferenceValue) {
return false;
}
} else {
// property must exist but be anything
if (
referenceValue == "" &&
(stringEntryObjectValue == undefined ||
stringEntryObjectValue == "")
) {
return false;
} else if (referenceValue == "") {
continue reference_entry_loop;
}
// property must match
if (stringEntryObjectValue != referenceValue) {
return false;
}
// property must match
if (stringEntryObjectValue != referenceValue) {
return false;
}
}
}
}
return true;
return true;
}
export function collectObjectValuesForKey<T>(
key: string,
converter: (object: T) => StringEntryObject,
objects: T[]
key: string,
converter: (object: T) => StringEntryObject,
objects: T[],
): string[] {
const values: Set<string> = new Set();
const values: Set<string> = new Set();
for (const object of objects) {
const stringEntryObject: StringEntryObject = converter(object);
const stringEntryObjectValue: Stringifiable | undefined =
stringEntryObject[key];
if (stringEntryObjectValue == undefined || stringEntryObjectValue == "")
continue;
for (const object of objects) {
const stringEntryObject: StringEntryObject = converter(object);
const stringEntryObjectValue: Stringifiable | undefined =
stringEntryObject[key];
if (stringEntryObjectValue == undefined || stringEntryObjectValue == "")
continue;
values.add(stringEntryObjectValue.toString());
}
values.add(stringEntryObjectValue.toString());
}
return [...values.values()];
return [...values.values()];
}
export function filterObjectsByWords<T>(
query: string,
getStringsOfObject: (object: T) => string[],
objects: T[]
query: string,
getStringsOfObject: (object: T) => string[],
objects: T[],
): Set<T> {
const matches: Set<T> = new Set();
const matches: Set<T> = new Set();
object_loop: for (const object of objects) {
const doesMatch: boolean = checkDoesObjectMatchSearch(
query,
getStringsOfObject,
object
);
if (doesMatch) matches.add(object);
}
object_loop: for (const object of objects) {
const doesMatch: boolean = checkDoesObjectMatchSearch(
query,
getStringsOfObject,
object,
);
if (doesMatch) matches.add(object);
}
return matches;
return matches;
}
export function checkDoesObjectMatchSearch<T>(
query: string,
getStringsOfObject: (object: T) => string[],
object: T
query: string,
getStringsOfObject: (object: T) => string[],
object: T,
): boolean {
if (query == "") return true;
if (query == "") return true;
const stringsInObject: string[] = getStringsOfObject(object);
const wordsInObject: string[] = [];
for (const string of stringsInObject) {
const lowercaseWordsInString = string
.toString()
.toLowerCase()
.split(" ")
.filter((word) => word != "");
wordsInObject.push(...lowercaseWordsInString);
}
const lowercaseWordsInQuery = query
.toLowerCase()
.split(" ")
.filter((word) => word != "");
for (const queryWord of lowercaseWordsInQuery) {
if (queryWord[0] == "-") {
// exclusion
const wordContent = queryWord.substring(1);
if (wordsInObject.includes(wordContent)) {
return false;
}
} else {
if (wordsInObject.includes(queryWord) == false) {
return false;
}
const stringsInObject: string[] = getStringsOfObject(object);
const wordsInObject: string[] = [];
for (const string of stringsInObject) {
const lowercaseWordsInString = string
.toString()
.toLowerCase()
.split(" ")
.filter((word) => word != "");
wordsInObject.push(...lowercaseWordsInString);
}
}
return true;
const lowercaseWordsInQuery = query
.toLowerCase()
.split(" ")
.filter((word) => word != "");
for (const queryWord of lowercaseWordsInQuery) {
if (queryWord[0] == "-") {
// exclusion
const wordContent = queryWord.substring(1);
if (wordsInObject.includes(wordContent)) {
return false;
}
} else {
if (wordsInObject.includes(queryWord) == false) {
return false;
}
}
}
return true;
}
// handlers
export type Handler<T> = (item: T) => void;
export class HandlerManager<T> {
handlers: Set<Handler<T>> = new Set();
handlers: Set<Handler<T>> = new Set();
// manage
addHandler = (handler: Handler<T>): void => {
this.handlers.add(handler);
};
// manage
addHandler = (handler: Handler<T>): void => {
this.handlers.add(handler);
};
deleteHandler = (handler: Handler<T>): void => {
this.handlers.delete(handler);
};
deleteHandler = (handler: Handler<T>): void => {
this.handlers.delete(handler);
};
// trigger
trigger = (item: T): void => {
for (const handler of this.handlers) {
handler(item);
}
};
// trigger
trigger = (item: T): void => {
for (const handler of this.handlers) {
handler(item);
}
};
}
// sorting
export class IndexManager<T> {
private itemToString: (item: T) => string;
private itemToString: (item: T) => string;
sortedStrings: string[] = [];
sortedStrings: string[] = [];
// methods
update = (items: T[]): void => {
this.sortedStrings = [];
// methods
update = (items: T[]): void => {
this.sortedStrings = [];
let strings: string[] = [];
for (const item of items) {
const string: string = this.itemToString(item);
strings.push(string);
let strings: string[] = [];
for (const item of items) {
const string: string = this.itemToString(item);
strings.push(string);
}
this.sortedStrings = strings.sort(localeCompare);
};
getIndex = (item: T): number => {
const string: string = this.itemToString(item);
const index: number = this.sortedStrings.indexOf(string);
return index;
};
// init
constructor(itemToString: (item: T) => string) {
this.itemToString = itemToString;
}
this.sortedStrings = strings.sort(localeCompare);
};
getIndex = (item: T): number => {
const string: string = this.itemToString(item);
const index: number = this.sortedStrings.indexOf(string);
return index;
};
// init
constructor(itemToString: (item: T) => string) {
this.itemToString = itemToString;
}
}
// storage
export function getLocalStorageItemAndClear(key: string): string | null {
const value: string | null = localStorage.getItem(key);
localStorage.removeItem(key);
if (value != null) localStorage.setItem(`_${key}`, value);
return value;
const value: string | null = localStorage.getItem(key);
localStorage.removeItem(key);
if (value != null) localStorage.setItem(`_${key}`, value);
return value;
}
// string & parsing
export function stringify(data: any): string {
return JSON.stringify(data, null, 4);
return JSON.stringify(data, null, 4);
}
export function padZero(string: string | undefined, length: number): string {
return (string ?? "").padStart(length, "0");
return (string ?? "").padStart(length, "0");
}
export function parse(string: string): any {
try {
return JSON.parse(string);
} catch {
return {};
}
try {
return JSON.parse(string);
} catch {
return {};
}
}
export function parseValidObject<T>(string: string, reference: T): T | null {
const parsed: any = parse(string);
if (checkIsValidObject(parsed) == false) return null;
const parsed: any = parse(string);
if (checkIsValidObject(parsed) == false) return null;
const doesMatchReference: boolean = checkMatchesObjectStructure(
parsed,
reference
);
if (doesMatchReference == false) return null;
const doesMatchReference: boolean = checkMatchesObjectStructure(
parsed,
reference,
);
if (doesMatchReference == false) return null;
return parsed;
return parsed;
}
export function parseOrFallback(inputString: string): any {
try {
return JSON.parse(inputString);
} catch {
return inputString;
}
try {
return JSON.parse(inputString);
} catch {
return inputString;
}
}
export function parseArray(inputString: string): any[] {
const parsed: any = parseOrFallback(inputString);
if (Array.isArray(parsed) == false) return [];
return parsed;
const parsed: any = parseOrFallback(inputString);
if (Array.isArray(parsed) == false) return [];
return parsed;
}
// sort
export function localeCompare(a: string, b: string): number {
return a.localeCompare(b);
return a.localeCompare(b);
}
+239 -228
View File
@@ -1,14 +1,18 @@
// this file is responsible for migrating data from v1 to v2.
import {
BoardInfoFileContent,
TaskFileContent,
BoardInfoFileContent,
TaskFileContent,
} from "../Model/Files/boardsAndTasksModel";
import ChatModel, {
ChatMessage,
ChatMessageStatus,
ChatMessage,
ChatMessageStatus,
} from "../Model/Chat/chatModel";
import { getLocalStorageItemAndClear, parseArray, parseOrFallback } from "../Model/Utility/utility";
import {
getLocalStorageItemAndClear,
parseArray,
parseOrFallback,
} from "../Model/Utility/utility";
import ChatListModel from "../Model/Chat/chatListModel";
import ConnectionModel from "../Model/Global/connectionModel";
@@ -19,263 +23,270 @@ import { translations } from "../View/translations";
import { v4 } from "uuid";
export default class v1Upgrader {
// general
migrateSettings = (): void => {
const name: string | null = getLocalStorageItemAndClear("sender-name");
if (name != null) {
const parsedName: string = parseOrFallback(name);
this.settingsModel.setName(parsedName);
}
// general
migrateSettings = (): void => {
const name: string | null = getLocalStorageItemAndClear("sender-name");
if (name != null) {
const parsedName: string = parseOrFallback(name);
this.settingsModel.setName(parsedName);
}
const firstDayOfWeek: string | null =
getLocalStorageItemAndClear("first-day-of-week");
if (firstDayOfWeek != null) {
const parsedFirstDayOfWeek: string = parseOrFallback(firstDayOfWeek);
this.settingsModel.setFirstDayOfWeek(parsedFirstDayOfWeek);
}
};
const firstDayOfWeek: string | null =
getLocalStorageItemAndClear("first-day-of-week");
if (firstDayOfWeek != null) {
const parsedFirstDayOfWeek: string =
parseOrFallback(firstDayOfWeek);
this.settingsModel.setFirstDayOfWeek(parsedFirstDayOfWeek);
}
};
migrateConnections = (): void => {
const previousAddressString: string | null =
getLocalStorageItemAndClear("previous-addresses");
if (previousAddressString == null) return;
migrateConnections = (): void => {
const previousAddressString: string | null =
getLocalStorageItemAndClear("previous-addresses");
if (previousAddressString == null) return;
const previousAddresses: any[] = parseArray(previousAddressString);
for (const address of previousAddresses) {
if (typeof address != "string") continue;
this.connectionModel.storeAddress(address);
}
};
const previousAddresses: any[] = parseArray(previousAddressString);
for (const address of previousAddresses) {
if (typeof address != "string") continue;
this.connectionModel.storeAddress(address);
}
};
// chats
migrateChats = (): void => {
const chatIdString: string | null = getLocalStorageItemAndClear("chat-ids");
if (chatIdString == null) return;
// chats
migrateChats = (): void => {
const chatIdString: string | null =
getLocalStorageItemAndClear("chat-ids");
if (chatIdString == null) return;
const chatIds: any[] = parseArray(chatIdString);
for (const chatId of chatIds) {
if (typeof chatId != "string") continue;
this.migrateChatById(chatId);
}
};
const chatIds: any[] = parseArray(chatIdString);
for (const chatId of chatIds) {
if (typeof chatId != "string") continue;
this.migrateChatById(chatId);
}
};
migrateChatById = (id: string): void => {
// get primary channel
const primaryChannel: string | null = getLocalStorageItemAndClear(
storageKeys.primaryChannel(id)
);
if (primaryChannel == null) return;
const parsedPriamryChannel: string = parseOrFallback(primaryChannel);
// create chat
const chatModel: ChatModel =
this.chatListModel.createChat(parsedPriamryChannel);
// secondary channels
const secondaryChannelString: string | null = getLocalStorageItemAndClear(
storageKeys.secondaryChannels(id)
);
if (secondaryChannelString != null) {
const potentialSecondaryChannels: any[] = parseArray(
secondaryChannelString
);
const confirmedSecondaryChannels: string[] = [];
for (const secondaryChannel of potentialSecondaryChannels) {
if (typeof secondaryChannel != "string") continue;
confirmedSecondaryChannels.push(secondaryChannel);
}
chatModel.setSecondaryChannels(confirmedSecondaryChannels);
}
// encryption key
const encryptionKey: string | null = getLocalStorageItemAndClear(
storageKeys.encyptionKey(id)
);
if (encryptionKey != null) {
const parsedEncryptionKey: string = parseOrFallback(encryptionKey);
chatModel.setEncryptionKey(parsedEncryptionKey);
}
// messages
const messagesString: string =
getLocalStorageItemAndClear(storageKeys.messages(id)) ?? "";
const messageOutboxString: string =
getLocalStorageItemAndClear(storageKeys.outbox(id)) ?? "";
const potentialMessages: any[] = parseArray(messagesString);
const potentialMessagesInOutbox: any[] = parseArray(messageOutboxString);
const addMessages = (
potentialMessages: any[],
status: ChatMessageStatus
) => {
for (const potentialMessage of potentialMessages) {
const isV1ChatMessage: boolean = checkMatchesObjectStructure(
potentialMessage,
V1ChatMessageReference
migrateChatById = (id: string): void => {
// get primary channel
const primaryChannel: string | null = getLocalStorageItemAndClear(
storageKeys.primaryChannel(id),
);
if (isV1ChatMessage == false) continue;
const v1ChatMessage: typeof V1ChatMessageReference = potentialMessage;
if (primaryChannel == null) return;
const parsedPriamryChannel: string = parseOrFallback(primaryChannel);
const convertedChatMessage: ChatMessage = {
dataVersion: "v2",
// create chat
const chatModel: ChatModel =
this.chatListModel.createChat(parsedPriamryChannel);
id: v4(),
// secondary channels
const secondaryChannelString: string | null =
getLocalStorageItemAndClear(storageKeys.secondaryChannels(id));
if (secondaryChannelString != null) {
const potentialSecondaryChannels: any[] = parseArray(
secondaryChannelString,
);
channel: v1ChatMessage.channel,
sender: v1ChatMessage.sender,
body: v1ChatMessage.body,
dateSent: v1ChatMessage.isoDate,
const confirmedSecondaryChannels: string[] = [];
for (const secondaryChannel of potentialSecondaryChannels) {
if (typeof secondaryChannel != "string") continue;
confirmedSecondaryChannels.push(secondaryChannel);
}
status,
stringifiedFile: "",
chatModel.setSecondaryChannels(confirmedSecondaryChannels);
}
// encryption key
const encryptionKey: string | null = getLocalStorageItemAndClear(
storageKeys.encyptionKey(id),
);
if (encryptionKey != null) {
const parsedEncryptionKey: string = parseOrFallback(encryptionKey);
chatModel.setEncryptionKey(parsedEncryptionKey);
}
// messages
const messagesString: string =
getLocalStorageItemAndClear(storageKeys.messages(id)) ?? "";
const messageOutboxString: string =
getLocalStorageItemAndClear(storageKeys.outbox(id)) ?? "";
const potentialMessages: any[] = parseArray(messagesString);
const potentialMessagesInOutbox: any[] =
parseArray(messageOutboxString);
const addMessages = (
potentialMessages: any[],
status: ChatMessageStatus,
) => {
for (const potentialMessage of potentialMessages) {
const isV1ChatMessage: boolean = checkMatchesObjectStructure(
potentialMessage,
V1ChatMessageReference,
);
if (isV1ChatMessage == false) continue;
const v1ChatMessage: typeof V1ChatMessageReference =
potentialMessage;
const convertedChatMessage: ChatMessage = {
dataVersion: "v2",
id: v4(),
channel: v1ChatMessage.channel,
sender: v1ChatMessage.sender,
body: v1ChatMessage.body,
dateSent: v1ChatMessage.isoDate,
status,
stringifiedFile: "",
};
chatModel.addMessage(convertedChatMessage);
if (status == ChatMessageStatus.Outbox) {
this.connectionModel.sendMessageOrStore(
convertedChatMessage,
);
}
}
};
chatModel.addMessage(convertedChatMessage);
if (status == ChatMessageStatus.Outbox) {
this.connectionModel.sendMessageOrStore(convertedChatMessage);
}
}
addMessages(potentialMessages, ChatMessageStatus.Received);
addMessages(potentialMessagesInOutbox, ChatMessageStatus.Outbox);
// objects
const objectsString: string =
getLocalStorageItemAndClear(storageKeys.objects(id)) ?? "";
const objectOutboxString: string =
getLocalStorageItemAndClear(storageKeys.itemOutbox(id)) ?? "";
const potentialObjects: any[] = parseArray(objectsString);
const potentialObjectsInOutbox: any[] = parseArray(objectOutboxString);
const addObjects = (potentialObjects: any[]): void => {
const board: BoardInfoFileContent =
chatModel.fileModel.boardsAndTasksModel.createBoard(
translations.updater.migrated,
);
chatModel.fileModel.boardsAndTasksModel.updateBoard(board);
for (const potentialObjectEntry of potentialObjects) {
const potentialObject = potentialObjectEntry[1];
const isV1MessageObject: boolean =
checkIsV1MessageObject(potentialObject);
if (isV1MessageObject == false) continue;
const objectId: string = potentialObject.id;
const objectName: string = potentialObject.title;
const contentVersions: any[] = Object.values(
potentialObject.contentVersions,
);
for (const potentialVersion of contentVersions) {
const isV1MessageObjectContent: boolean =
checkIsV1MessageObjectContent(potentialVersion);
if (isV1MessageObjectContent == false) continue;
const convertedTaskFileContent: TaskFileContent = {
dataVersion: "v2",
fileId: objectId,
fileContentId: FileModel.generateFileContentId(
potentialVersion.isoDateVersionCreated,
),
creationDate: potentialVersion.isoDateVersionCreated,
type: "task",
name: objectName ?? "",
boardId: board.fileId ?? "",
description: potentialVersion.noteContent ?? "",
category: potentialVersion.categoryName ?? "",
status: potentialVersion.status ?? "",
priority: potentialVersion.priority ?? "",
date: potentialVersion.date ?? "",
time: potentialVersion.time ?? "",
};
console.log(convertedTaskFileContent);
chatModel.fileModel.handleFileContent(
convertedTaskFileContent,
);
}
}
};
addObjects([...potentialObjects, ...potentialObjectsInOutbox]);
};
addMessages(potentialMessages, ChatMessageStatus.Received);
addMessages(potentialMessagesInOutbox, ChatMessageStatus.Outbox);
// objects
const objectsString: string =
getLocalStorageItemAndClear(storageKeys.objects(id)) ?? "";
const objectOutboxString: string =
getLocalStorageItemAndClear(storageKeys.itemOutbox(id)) ?? "";
const potentialObjects: any[] = parseArray(objectsString);
const potentialObjectsInOutbox: any[] = parseArray(objectOutboxString);
const addObjects = (potentialObjects: any[]): void => {
const board: BoardInfoFileContent =
chatModel.fileModel.boardsAndTasksModel.createBoard(
translations.updater.migrated
);
chatModel.fileModel.boardsAndTasksModel.updateBoard(board);
for (const potentialObjectEntry of potentialObjects) {
const potentialObject = potentialObjectEntry[1];
const isV1MessageObject: boolean =
checkIsV1MessageObject(potentialObject);
if (isV1MessageObject == false) continue;
const objectId: string = potentialObject.id;
const objectName: string = potentialObject.title;
const contentVersions: any[] = Object.values(
potentialObject.contentVersions
);
for (const potentialVersion of contentVersions) {
const isV1MessageObjectContent: boolean =
checkIsV1MessageObjectContent(potentialVersion);
if (isV1MessageObjectContent == false) continue;
const convertedTaskFileContent: TaskFileContent = {
dataVersion: "v2",
fileId: objectId,
fileContentId: FileModel.generateFileContentId(
potentialVersion.isoDateVersionCreated
),
creationDate: potentialVersion.isoDateVersionCreated,
type: "task",
name: objectName ?? "",
boardId: board.fileId ?? "",
description: potentialVersion.noteContent ?? "",
category: potentialVersion.categoryName ?? "",
status: potentialVersion.status ?? "",
priority: potentialVersion.priority ?? "",
date: potentialVersion.date ?? "",
time: potentialVersion.time ?? "",
};
console.log(convertedTaskFileContent);
chatModel.fileModel.handleFileContent(convertedTaskFileContent);
}
}
};
addObjects([...potentialObjects, ...potentialObjectsInOutbox]);
};
// init
constructor(
public settingsModel: SettingsModel,
public connectionModel: ConnectionModel,
public chatListModel: ChatListModel
) {
this.migrateSettings();
this.migrateConnections();
this.migrateChats();
}
// init
constructor(
public settingsModel: SettingsModel,
public connectionModel: ConnectionModel,
public chatListModel: ChatListModel,
) {
this.migrateSettings();
this.migrateConnections();
this.migrateChats();
}
}
const storageKeys = {
viewType(id: string): string {
return id + "view-type";
},
viewType(id: string): string {
return id + "view-type";
},
hasUnread(id: string): string {
return id + "has-unread-messages";
},
hasUnread(id: string): string {
return id + "has-unread-messages";
},
primaryChannel(id: string): string {
return id + "primary-channel";
},
primaryChannel(id: string): string {
return id + "primary-channel";
},
secondaryChannels(id: string): string {
return id + "secondary-channels";
},
secondaryChannels(id: string): string {
return id + "secondary-channels";
},
encyptionKey(id: string): string {
return id + "encryption-key";
},
encyptionKey(id: string): string {
return id + "encryption-key";
},
messages(id: string): string {
return id + "messages";
},
messages(id: string): string {
return id + "messages";
},
objects(id: string): string {
return id + "items";
},
objects(id: string): string {
return id + "items";
},
outbox(id: string): string {
return id + "outbox";
},
outbox(id: string): string {
return id + "outbox";
},
itemOutbox(id: string): string {
return id + "item-outbox";
},
itemOutbox(id: string): string {
return id + "item-outbox";
},
composingMessage(id: string): string {
return id + "composing-message";
},
composingMessage(id: string): string {
return id + "composing-message";
},
};
const V1ChatMessageReference = {
channel: "",
sender: "",
body: "",
isoDate: "",
channel: "",
sender: "",
body: "",
isoDate: "",
};
function checkIsV1MessageObject(object: any): boolean {
if (object.id == undefined) return false;
if (object.title == undefined) return false;
if (object.contentVersions == undefined) return false;
if (object.id == undefined) return false;
if (object.title == undefined) return false;
if (object.contentVersions == undefined) return false;
return true;
return true;
}
function checkIsV1MessageObjectContent(object: any): boolean {
if (object.id == undefined) return false;
if (object.isoDateVersionCreated == undefined) return false;
if (object.id == undefined) return false;
if (object.isoDateVersionCreated == undefined) return false;
return true;
}
return true;
}
+81 -72
View File
@@ -1,8 +1,8 @@
import * as React from "bloatless-react";
import {
PropertyValueList,
createPropertyValueIndexState,
PropertyValueList,
createPropertyValueIndexState,
} from "../Components/propertyValueList";
import BoardViewModel from "../../ViewModel/Pages/boardViewModel";
@@ -14,84 +14,93 @@ import { allowDrop } from "../utility";
import { translations } from "../translations";
export function BoardKanbanPage(boardViewModel: BoardViewModel) {
return PropertyValueList(
"category",
(taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.taskViewModels,
(
categories: React.ListState<string>,
sortedCategories: React.State<string[]>
) => {
const categoryNameConverter: React.StateItemConverter<string> = (
categoryName: string
) => {
const index: React.State<number> = createPropertyValueIndexState(
sortedCategories,
categoryName
);
return KanbanBoard(categoryName, index, boardViewModel);
};
return PropertyValueList(
"category",
(taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.taskViewModels,
(
categories: React.ListState<string>,
sortedCategories: React.State<string[]>,
) => {
const categoryNameConverter: React.StateItemConverter<string> = (
categoryName: string,
) => {
const index: React.State<number> =
createPropertyValueIndexState(
sortedCategories,
categoryName,
);
return KanbanBoard(categoryName, index, boardViewModel);
};
return (
<div
class="kanban-board-wrapper"
children:append={[categories, categoryNameConverter]}
></div>
);
}
);
return (
<div
class="kanban-board-wrapper"
children:append={[categories, categoryNameConverter]}
></div>
);
},
);
}
function KanbanBoard(
categoryName: string,
index: React.State<number>,
boardViewModel: BoardViewModel
categoryName: string,
index: React.State<number>,
boardViewModel: BoardViewModel,
) {
return FilteredList(
{ category: categoryName },
(taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.filteredTaskViewModels,
(taskViewModels: React.ListState<TaskViewModel>) => {
const viewModel: TaskCategoryBulkChangeViewModel =
new TaskCategoryBulkChangeViewModel(taskViewModels, categoryName);
return FilteredList(
{ category: categoryName },
(taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.filteredTaskViewModels,
(taskViewModels: React.ListState<TaskViewModel>) => {
const viewModel: TaskCategoryBulkChangeViewModel =
new TaskCategoryBulkChangeViewModel(
taskViewModels,
categoryName,
);
function drop() {
boardViewModel.handleDropWithinBoard(categoryName);
}
function drop() {
boardViewModel.handleDropWithinBoard(categoryName);
}
const view = (
<div class="flex-column flex-no" on:dragover={allowDrop} on:drop={drop}>
<div class="flex-row width-input">
<input
placeholder={
translations.chatPage.task.renameCategoryInputPlaceholder
}
bind:value={viewModel.inputValue}
on:enter={viewModel.set}
></input>
<button
class="primary"
on:click={viewModel.set}
toggle:disabled={viewModel.cannotSet}
>
<span class="icon">check</span>
</button>
</div>
const view = (
<div
class="flex-column flex-no"
on:dragover={allowDrop}
on:drop={drop}
>
<div class="flex-row width-input">
<input
placeholder={
translations.chatPage.task
.renameCategoryInputPlaceholder
}
bind:value={viewModel.inputValue}
on:enter={viewModel.set}
></input>
<button
class="primary"
on:click={viewModel.set}
toggle:disabled={viewModel.cannotSet}
>
<span class="icon">check</span>
</button>
</div>
<hr></hr>
<hr></hr>
<div
class="kanban-column"
children:append={[taskViewModels, TaskViewModelToEntry]}
></div>
</div>
);
<div
class="kanban-column"
children:append={[taskViewModels, TaskViewModelToEntry]}
></div>
</div>
);
index.subscribe((newIndex) => {
view.style.order = newIndex;
});
return view;
}
);
index.subscribe((newIndex) => {
view.style.order = newIndex;
});
return view;
},
);
}
+135 -115
View File
@@ -1,7 +1,7 @@
import * as React from "bloatless-react";
import BoardViewModel, {
BoardPageType,
BoardPageType,
} from "../../ViewModel/Pages/boardViewModel";
import { BoardKanbanPage } from "./boardKanbanPage";
@@ -14,120 +14,140 @@ import { TaskViewModelToEntry } from "../Components/taskEntry";
import { translations } from "../translations";
export function BoardPage(boardViewModel: BoardViewModel) {
boardViewModel.loadData();
boardViewModel.loadData();
// main content
const mainContent = React.createProxyState(
[boardViewModel.selectedPage],
() => {
switch (boardViewModel.selectedPage.value) {
case BoardPageType.Kanban: {
return BoardKanbanPage(boardViewModel);
}
case BoardPageType.StatusGrid: {
return BoardStatusGridPage(boardViewModel);
}
default: {
return (
<div
class="task-grid"
children:append={[
boardViewModel.filteredTaskViewModels,
// main content
const mainContent = React.createProxyState(
[boardViewModel.selectedPage],
() => {
switch (boardViewModel.selectedPage.value) {
case BoardPageType.Kanban: {
return BoardKanbanPage(boardViewModel);
}
case BoardPageType.StatusGrid: {
return BoardStatusGridPage(boardViewModel);
}
default: {
return (
<div
class="task-grid"
children:append={[
boardViewModel.filteredTaskViewModels,
TaskViewModelToEntry,
]}
></div>
);
}
}
},
);
// task modal
const taskSettingsModal = React.createProxyState(
[boardViewModel.selectedTaskViewModel],
() => {
if (boardViewModel.selectedTaskViewModel.value == undefined) {
return <div></div>;
} else {
return TaskSettingsModal(
boardViewModel.selectedTaskViewModel.value,
);
}
},
);
return (
<div class="pane">
<div class="toolbar">
<span>
<button
class="ghost board-close-button"
aria-label={
translations.chatPage.task
.closeBoardButtonAudioLabel
}
on:click={boardViewModel.close}
>
<span class="icon">arrow_back</span>
</button>
<button
class="ghost board-toggle-button inset-outline"
aria-label={
translations.chatPage.task
.toggleBoardButtonAudioLabel
}
on:click={
boardViewModel.taskPageViewModel.toggleBoardList
}
toggle:selected={
boardViewModel.taskPageViewModel.isShowingBoadList
}
>
<span class="icon">dock_to_right</span>
</button>
<button
class="ghost"
aria-label={
translations.chatPage.task.boardSettingsHeadline
}
on:click={boardViewModel.showSettings}
>
<span class="icon">settings</span>
</button>
</span>
<span class="scroll-h ribbon">
{BoardViewToggleButton(
translations.chatPage.task.listViewButtonAudioLabel,
"view_list",
BoardPageType.List,
boardViewModel,
)}
{BoardViewToggleButton(
translations.chatPage.task.kanbanViewButtonAudioLabel,
"view_kanban",
BoardPageType.Kanban,
boardViewModel,
)}
{BoardViewToggleButton(
translations.chatPage.task.statusViewButtonAudioLabel,
"grid_view",
BoardPageType.StatusGrid,
boardViewModel,
)}
</span>
<span>
<button
class="ghost"
aria-label={
translations.chatPage.task
.filterTasksButtonAudioLabel
}
on:click={boardViewModel.showFilterModal}
>
<span class="icon">filter_alt</span>
</button>
<button
class="ghost"
aria-label={
translations.chatPage.task
.createTaskButtonAudioLabel
}
on:click={boardViewModel.createTask}
>
<span class="icon">add</span>
</button>
</span>
</div>
<div class="content main-content" children:set={mainContent}></div>
{BoardSettingsModal(boardViewModel)}
{SearchModal(
boardViewModel.searchViewModel,
translations.chatPage.task.filterTasksHeadline,
TaskViewModelToEntry,
]}
></div>
);
}
}
}
);
// task modal
const taskSettingsModal = React.createProxyState(
[boardViewModel.selectedTaskViewModel],
() => {
if (boardViewModel.selectedTaskViewModel.value == undefined) {
return <div></div>;
} else {
return TaskSettingsModal(boardViewModel.selectedTaskViewModel.value);
}
}
);
return (
<div class="pane">
<div class="toolbar">
<span>
<button
class="ghost board-close-button"
aria-label={translations.chatPage.task.closeBoardButtonAudioLabel}
on:click={boardViewModel.close}
>
<span class="icon">arrow_back</span>
</button>
<button
class="ghost board-toggle-button inset-outline"
aria-label={translations.chatPage.task.toggleBoardButtonAudioLabel}
on:click={boardViewModel.taskPageViewModel.toggleBoardList}
toggle:selected={boardViewModel.taskPageViewModel.isShowingBoadList}
>
<span class="icon">dock_to_right</span>
</button>
<button
class="ghost"
aria-label={translations.chatPage.task.boardSettingsHeadline}
on:click={boardViewModel.showSettings}
>
<span class="icon">settings</span>
</button>
</span>
<span class="scroll-h ribbon">
{BoardViewToggleButton(
translations.chatPage.task.listViewButtonAudioLabel,
"view_list",
BoardPageType.List,
boardViewModel
)}
{BoardViewToggleButton(
translations.chatPage.task.kanbanViewButtonAudioLabel,
"view_kanban",
BoardPageType.Kanban,
boardViewModel
)}
{BoardViewToggleButton(
translations.chatPage.task.statusViewButtonAudioLabel,
"grid_view",
BoardPageType.StatusGrid,
boardViewModel
)}
</span>
<span>
<button
class="ghost"
aria-label={translations.chatPage.task.filterTasksButtonAudioLabel}
on:click={boardViewModel.showFilterModal}
>
<span class="icon">filter_alt</span>
</button>
<button
class="ghost"
aria-label={translations.chatPage.task.createTaskButtonAudioLabel}
on:click={boardViewModel.createTask}
>
<span class="icon">add</span>
</button>
</span>
</div>
<div class="content main-content" children:set={mainContent}></div>
{BoardSettingsModal(boardViewModel)}
{SearchModal(
boardViewModel.searchViewModel,
translations.chatPage.task.filterTasksHeadline,
TaskViewModelToEntry,
boardViewModel.isPresentingFilterModal
)}
<div children:set={taskSettingsModal}></div>
</div>
);
boardViewModel.isPresentingFilterModal,
)}
<div children:set={taskSettingsModal}></div>
</div>
);
}
+203 -188
View File
@@ -1,14 +1,14 @@
import * as React from "bloatless-react";
import {
PropertyValueList,
collectPropertyValuesToState,
createPropertyValueIndexState,
createSortedPropertyValueState,
PropertyValueList,
collectPropertyValuesToState,
createPropertyValueIndexState,
createSortedPropertyValueState,
} from "../Components/propertyValueList";
import {
TaskCategoryBulkChangeViewModel,
TaskStatusBulkChangeViewModel,
TaskCategoryBulkChangeViewModel,
TaskStatusBulkChangeViewModel,
} from "../../ViewModel/Utility/taskPropertyBulkChangeViewModel";
import BoardViewModel from "../../ViewModel/Pages/boardViewModel";
@@ -19,221 +19,236 @@ import { allowDrop } from "../utility";
import { translations } from "../translations";
export function BoardStatusGridPage(boardViewModel: BoardViewModel) {
const statuses: React.ListState<string> = new React.ListState();
const sortedStatuses: React.State<string[]> =
createSortedPropertyValueState(statuses);
const statuses: React.ListState<string> = new React.ListState();
const sortedStatuses: React.State<string[]> =
createSortedPropertyValueState(statuses);
boardViewModel.taskViewModels.subscribe(() => {
collectPropertyValuesToState(
"status",
(taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.taskViewModels,
statuses
);
});
boardViewModel.taskViewModels.subscribe(() => {
collectPropertyValuesToState(
"status",
(taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.taskViewModels,
statuses,
);
});
const statusNameCellConverter: React.StateItemConverter<string> = (
statusName: string
) => {
const index = createPropertyValueIndexState(sortedStatuses, statusName);
return StatusNameCell(statusName, index, boardViewModel);
};
const statusNameCellConverter: React.StateItemConverter<string> = (
statusName: string,
) => {
const index = createPropertyValueIndexState(sortedStatuses, statusName);
return StatusNameCell(statusName, index, boardViewModel);
};
return (
<div class="status-page-content">
<div
class="status-name-row"
children:append={[statuses, statusNameCellConverter]}
></div>
{PropertyValueList(
"category",
(taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.filteredTaskViewModels,
(
categories: React.ListState<string>,
sortedCategories: React.State<string[]>
) => {
const categoryRowConverter: React.StateItemConverter<string> = (
categoryName: string
) => {
const index: React.State<number> = createPropertyValueIndexState(
sortedCategories,
categoryName
);
return CategoryRow(
categoryName,
index,
statuses,
sortedStatuses,
boardViewModel
);
};
return (
return (
<div class="status-page-content">
<div
class="status-grid-wrapper"
children:append={[categories, categoryRowConverter]}
class="status-name-row"
children:append={[statuses, statusNameCellConverter]}
></div>
);
}
)}
</div>
);
{PropertyValueList(
"category",
(taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.filteredTaskViewModels,
(
categories: React.ListState<string>,
sortedCategories: React.State<string[]>,
) => {
const categoryRowConverter: React.StateItemConverter<
string
> = (categoryName: string) => {
const index: React.State<number> =
createPropertyValueIndexState(
sortedCategories,
categoryName,
);
return CategoryRow(
categoryName,
index,
statuses,
sortedStatuses,
boardViewModel,
);
};
return (
<div
class="status-grid-wrapper"
children:append={[categories, categoryRowConverter]}
></div>
);
},
)}
</div>
);
}
function StatusNameCell(
statusName: string,
index: React.State<number>,
boardViewModel: BoardViewModel
statusName: string,
index: React.State<number>,
boardViewModel: BoardViewModel,
) {
const taskViewModelsWithMatchingStatus: React.ListState<TaskViewModel> =
new React.ListState();
const taskViewModelsWithMatchingStatus: React.ListState<TaskViewModel> =
new React.ListState();
boardViewModel.filteredTaskViewModels.handleAddition(
(taskViewModel: TaskViewModel) => {
const doesMatchStatus: boolean = taskViewModel.task.status == statusName;
if (doesMatchStatus == false) return;
boardViewModel.filteredTaskViewModels.handleAddition(
(taskViewModel: TaskViewModel) => {
const doesMatchStatus: boolean =
taskViewModel.task.status == statusName;
if (doesMatchStatus == false) return;
taskViewModelsWithMatchingStatus.add(taskViewModel);
boardViewModel.filteredTaskViewModels.handleRemoval(taskViewModel, () => {
taskViewModelsWithMatchingStatus.remove(taskViewModel);
});
}
);
const viewModel: TaskStatusBulkChangeViewModel =
new TaskStatusBulkChangeViewModel(
taskViewModelsWithMatchingStatus,
statusName
taskViewModelsWithMatchingStatus.add(taskViewModel);
boardViewModel.filteredTaskViewModels.handleRemoval(
taskViewModel,
() => {
taskViewModelsWithMatchingStatus.remove(taskViewModel);
},
);
},
);
const view = (
<div class="flex-row">
<div class="property-input-wrapper">
<input
placeholder={
translations.chatPage.task.renameCategoryInputPlaceholder
}
bind:value={viewModel.inputValue}
on:enter={viewModel.set}
></input>
<button
class="primary"
on:click={viewModel.set}
toggle:disabled={viewModel.cannotSet}
>
<span class="icon">check</span>
</button>
</div>
</div>
);
const viewModel: TaskStatusBulkChangeViewModel =
new TaskStatusBulkChangeViewModel(
taskViewModelsWithMatchingStatus,
statusName,
);
index.subscribe((newIndex) => {
view.style.order = newIndex;
});
const view = (
<div class="flex-row">
<div class="property-input-wrapper">
<input
placeholder={
translations.chatPage.task
.renameCategoryInputPlaceholder
}
bind:value={viewModel.inputValue}
on:enter={viewModel.set}
></input>
<button
class="primary"
on:click={viewModel.set}
toggle:disabled={viewModel.cannotSet}
>
<span class="icon">check</span>
</button>
</div>
</div>
);
return view;
index.subscribe((newIndex) => {
view.style.order = newIndex;
});
return view;
}
function CategoryRow(
categoryName: string,
index: React.State<number>,
allStatuses: React.ListState<string>,
sortedStatuses: React.State<string[]>,
boardViewModel: BoardViewModel
categoryName: string,
index: React.State<number>,
allStatuses: React.ListState<string>,
sortedStatuses: React.State<string[]>,
boardViewModel: BoardViewModel,
) {
return FilteredList(
{ category: categoryName },
(taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.filteredTaskViewModels,
(taskViewModels: React.ListState<TaskViewModel>) => {
const statusNameConverter: React.StateItemConverter<string> = (
statusName: string
) => {
const index = createPropertyValueIndexState(sortedStatuses, statusName);
return CategoryStatusColumn(
categoryName,
statusName,
index,
boardViewModel,
taskViewModels
);
};
return FilteredList(
{ category: categoryName },
(taskViewModel: TaskViewModel) => taskViewModel.task,
boardViewModel.filteredTaskViewModels,
(taskViewModels: React.ListState<TaskViewModel>) => {
const statusNameConverter: React.StateItemConverter<string> = (
statusName: string,
) => {
const index = createPropertyValueIndexState(
sortedStatuses,
statusName,
);
return CategoryStatusColumn(
categoryName,
statusName,
index,
boardViewModel,
taskViewModels,
);
};
const viewModel: TaskCategoryBulkChangeViewModel =
new TaskCategoryBulkChangeViewModel(taskViewModels, categoryName);
const viewModel: TaskCategoryBulkChangeViewModel =
new TaskCategoryBulkChangeViewModel(
taskViewModels,
categoryName,
);
const view = (
<div class="flex-row flex-no large-gap">
<div class="property-input-wrapper">
<input
placeholder={
translations.chatPage.task.renameCategoryInputPlaceholder
}
bind:value={viewModel.inputValue}
on:enter={viewModel.set}
></input>
<button
class="primary"
on:click={viewModel.set}
toggle:disabled={viewModel.cannotSet}
>
<span class="icon">check</span>
</button>
</div>
const view = (
<div class="flex-row flex-no large-gap">
<div class="property-input-wrapper">
<input
placeholder={
translations.chatPage.task
.renameCategoryInputPlaceholder
}
bind:value={viewModel.inputValue}
on:enter={viewModel.set}
></input>
<button
class="primary"
on:click={viewModel.set}
toggle:disabled={viewModel.cannotSet}
>
<span class="icon">check</span>
</button>
</div>
<div
class="flex-row large-gap padding-right"
children:append={[allStatuses, statusNameConverter]}
></div>
</div>
);
<div
class="flex-row large-gap padding-right"
children:append={[allStatuses, statusNameConverter]}
></div>
</div>
);
index.subscribe((newIndex) => {
view.style.order = newIndex;
});
index.subscribe((newIndex) => {
view.style.order = newIndex;
});
return view;
}
);
return view;
},
);
}
function CategoryStatusColumn(
categoryName: string,
statusName: string,
index: React.State<number>,
boardViewModel: BoardViewModel,
taskViewModelsWithMatchingCategory: React.ListState<TaskViewModel>
categoryName: string,
statusName: string,
index: React.State<number>,
boardViewModel: BoardViewModel,
taskViewModelsWithMatchingCategory: React.ListState<TaskViewModel>,
) {
const taskViewModels: React.ListState<TaskViewModel> = new React.ListState();
const taskViewModels: React.ListState<TaskViewModel> =
new React.ListState();
taskViewModelsWithMatchingCategory.handleAddition((taskViewModel) => {
const doesMatchStatus: boolean = taskViewModel.status.value == statusName;
if (doesMatchStatus == false) return;
taskViewModelsWithMatchingCategory.handleAddition((taskViewModel) => {
const doesMatchStatus: boolean =
taskViewModel.status.value == statusName;
if (doesMatchStatus == false) return;
taskViewModels.add(taskViewModel);
taskViewModelsWithMatchingCategory.handleRemoval(taskViewModel, () => {
taskViewModels.remove(taskViewModel);
taskViewModels.add(taskViewModel);
taskViewModelsWithMatchingCategory.handleRemoval(taskViewModel, () => {
taskViewModels.remove(taskViewModel);
});
});
});
function drop() {
boardViewModel.handleDropWithinBoard(categoryName, statusName);
}
function drop() {
boardViewModel.handleDropWithinBoard(categoryName, statusName);
}
const view = (
<div
class="status-column gap"
on:dragover={allowDrop}
on:drop={drop}
children:append={[taskViewModels, TaskViewModelToEntry]}
></div>
);
const view = (
<div
class="status-column gap"
on:dragover={allowDrop}
on:drop={drop}
children:append={[taskViewModels, TaskViewModelToEntry]}
></div>
);
index.subscribe((newIndex) => {
view.style.order = newIndex;
});
index.subscribe((newIndex) => {
view.style.order = newIndex;
});
return view;
return view;
}
+50 -51
View File
@@ -1,74 +1,73 @@
#chat-page
> div
> #main
> #calendar-page
> .pane-wrapper
> .pane
> .toolbar
.input-wrapper {
display: flex;
> div
> #main
> #calendar-page
> .pane-wrapper
> .pane
> .toolbar
.input-wrapper {
display: flex;
padding: var(--gap-between-tiles);
gap: var(--gap-between-tiles);
padding: var(--gap-between-tiles);
gap: var(--gap-between-tiles);
}
#chat-page
> div
> #main
> #calendar-page
> .pane-wrapper
> .pane
> .toolbar
.input-wrapper
> input {
min-height: unset;
height: calc(var(--control-height) - 2 * var(--gap-between-tiles));
> div
> #main
> #calendar-page
> .pane-wrapper
> .pane
> .toolbar
.input-wrapper
> input {
min-height: unset;
height: calc(var(--control-height) - 2 * var(--gap-between-tiles));
appearance: textfield;
-moz-appearance: textfield;
appearance: textfield;
-moz-appearance: textfield;
padding-right: 0px;
padding-right: 0px;
}
#calendar-page .input-wrapper > input::-webkit-outer-spin-button,
#calendar-page .input-wrapper > input::-webkit-innner-spin-button {
appearance: none;
-webkit-appearance: none;
appearance: none;
-webkit-appearance: none;
margin: 0px;
margin: 0px;
}
#chat-page
> div
> #main
> #calendar-page
> .pane-wrapper
> .pane
> .toolbar
.input-wrapper
> .year-input {
width: 4.4rem;
> div
> #main
> #calendar-page
> .pane-wrapper
> .pane
> .toolbar
.input-wrapper
> .year-input {
width: 4.4rem;
}
#chat-page
> div
> #main
> #calendar-page
> .pane-wrapper
> .pane
> .toolbar
.input-wrapper
> .month-input {
width: 3.1rem;
> div
> #main
> #calendar-page
> .pane-wrapper
> .pane
> .toolbar
.input-wrapper
> .month-input {
width: 3.1rem;
}
@media (max-width: 1024px) {
#chat-page > div > #main > #calendar-page {
flex-direction: column;
}
#chat-page > div > #main > #calendar-page {
flex-direction: column;
}
#chat-page > div > #main > #calendar-page .grid-pane-wrapper {
border-bottom: 1px solid var(--border);
}
#chat-page > div > #main > #calendar-page .grid-pane-wrapper {
border-bottom: 1px solid var(--border);
}
}
+195 -160
View File
@@ -10,168 +10,203 @@ import { TaskViewModelToEntry } from "../Components/taskEntry";
import { translations } from "../translations";
export function CalendarPage(calendarPageViewModel: CalendarPageViewModel) {
calendarPageViewModel.loadData();
calendarPageViewModel.loadData();
const mainContent = React.createProxyState(
[calendarPageViewModel.monthGrid],
() => {
const monthGrid = calendarPageViewModel.monthGrid.value;
const mainContent = React.createProxyState(
[calendarPageViewModel.monthGrid],
() => {
const monthGrid = calendarPageViewModel.monthGrid.value;
if (monthGrid == undefined) {
return <div></div>;
} else {
function drop(date: string) {
calendarPageViewModel.handleDrop(
monthGrid!.year.toString(),
monthGrid!.month.toString(),
date
);
}
return MonthGrid(monthGrid, calendarPageViewModel.selectedDate, drop);
}
}
);
const sidePaneContentWrapper = React.createProxyState(
[
calendarPageViewModel.selectedYear,
calendarPageViewModel.selectedMonth,
calendarPageViewModel.selectedDate,
],
() => {
const listState = calendarPageViewModel.getEventsForDate();
if (listState == undefined) {
return <div></div>;
} else {
const sidePaneContent = React.createProxyState([listState], () => {
if (listState.value.size == 0) {
return (
<div class="width-100 height-100 flex-column justify-center align-center">
<span class="secondary slide-up">
{translations.chatPage.calendar.noEvents}
</span>
</div>
);
} else {
return (
<div
class="flex-column gap slide-up padding-bottom"
children:append={[listState, TaskViewModelToEntry]}
></div>
);
}
});
return (
<div
class="width-100 height-100"
children:set={sidePaneContent}
></div>
);
}
}
);
const taskSettingsModal = React.createProxyState(
[calendarPageViewModel.selectedTaskViewModel],
() => {
if (calendarPageViewModel.selectedTaskViewModel.value == undefined) {
return <div></div>;
} else {
return TaskSettingsModal(
calendarPageViewModel.selectedTaskViewModel.value
);
}
}
);
return (
<div id="calendar-page">
<div class="pane-wrapper grid-pane-wrapper">
<div class="pane">
<div class="toolbar">
<span>
<button
class="ghost"
aria-label={
translations.chatPage.calendar.todayButtonAudioLabel
if (monthGrid == undefined) {
return <div></div>;
} else {
function drop(date: string) {
calendarPageViewModel.handleDrop(
monthGrid!.year.toString(),
monthGrid!.month.toString(),
date,
);
}
on:click={calendarPageViewModel.showToday}
>
<span class="icon">today</span>
</button>
</span>
<span>
<button
class="ghost"
aria-label={
translations.chatPage.calendar.previousMonthButtonAudioLabel
}
on:click={calendarPageViewModel.showPreviousMonth}
>
<span class="icon">arrow_back</span>
</button>
<span class="input-wrapper">
<input
class="year-input"
type="number"
aria-label={
translations.chatPage.calendar.yearInputAudioLabel
}
placeholder={
translations.chatPage.calendar.yearInputPlaceholder
}
bind:value={calendarPageViewModel.selectedYear}
></input>
<input
class="month-input"
type="number"
aria-label={
translations.chatPage.calendar.monthInputAudioLabel
}
placeholder={
translations.chatPage.calendar.monthInputPlaceholder
}
bind:value={calendarPageViewModel.selectedMonth}
></input>
</span>
<button
class="ghost"
aria-label={
translations.chatPage.calendar.nextMonthButtonAudioLabel
}
on:click={calendarPageViewModel.showNextMonth}
>
<span class="icon">arrow_forward</span>
</button>
</span>
<span>
<button
class="ghost"
aria-label={
translations.chatPage.task.createTaskButtonAudioLabel
}
on:click={calendarPageViewModel.createEvent}
>
<span class="icon">add</span>
</button>
</span>
</div>
<div class="content padding-0" children:set={mainContent}></div>
return MonthGrid(
monthGrid,
calendarPageViewModel.selectedDate,
drop,
);
}
},
);
const sidePaneContentWrapper = React.createProxyState(
[
calendarPageViewModel.selectedYear,
calendarPageViewModel.selectedMonth,
calendarPageViewModel.selectedDate,
],
() => {
const listState = calendarPageViewModel.getEventsForDate();
if (listState == undefined) {
return <div></div>;
} else {
const sidePaneContent = React.createProxyState(
[listState],
() => {
if (listState.value.size == 0) {
return (
<div class="width-100 height-100 flex-column justify-center align-center">
<span class="secondary slide-up">
{
translations.chatPage.calendar
.noEvents
}
</span>
</div>
);
} else {
return (
<div
class="flex-column gap slide-up padding-bottom"
children:append={[
listState,
TaskViewModelToEntry,
]}
></div>
);
}
},
);
return (
<div
class="width-100 height-100"
children:set={sidePaneContent}
></div>
);
}
},
);
const taskSettingsModal = React.createProxyState(
[calendarPageViewModel.selectedTaskViewModel],
() => {
if (
calendarPageViewModel.selectedTaskViewModel.value == undefined
) {
return <div></div>;
} else {
return TaskSettingsModal(
calendarPageViewModel.selectedTaskViewModel.value,
);
}
},
);
return (
<div id="calendar-page">
<div class="pane-wrapper grid-pane-wrapper">
<div class="pane">
<div class="toolbar">
<span>
<button
class="ghost"
aria-label={
translations.chatPage.calendar
.todayButtonAudioLabel
}
on:click={calendarPageViewModel.showToday}
>
<span class="icon">today</span>
</button>
</span>
<span>
<button
class="ghost"
aria-label={
translations.chatPage.calendar
.previousMonthButtonAudioLabel
}
on:click={
calendarPageViewModel.showPreviousMonth
}
>
<span class="icon">arrow_back</span>
</button>
<span class="input-wrapper">
<input
class="year-input"
type="number"
aria-label={
translations.chatPage.calendar
.yearInputAudioLabel
}
placeholder={
translations.chatPage.calendar
.yearInputPlaceholder
}
bind:value={
calendarPageViewModel.selectedYear
}
></input>
<input
class="month-input"
type="number"
aria-label={
translations.chatPage.calendar
.monthInputAudioLabel
}
placeholder={
translations.chatPage.calendar
.monthInputPlaceholder
}
bind:value={
calendarPageViewModel.selectedMonth
}
></input>
</span>
<button
class="ghost"
aria-label={
translations.chatPage.calendar
.nextMonthButtonAudioLabel
}
on:click={calendarPageViewModel.showNextMonth}
>
<span class="icon">arrow_forward</span>
</button>
</span>
<span>
<button
class="ghost"
aria-label={
translations.chatPage.task
.createTaskButtonAudioLabel
}
on:click={calendarPageViewModel.createEvent}
>
<span class="icon">add</span>
</button>
</span>
</div>
<div
class="content padding-0"
children:set={mainContent}
></div>
</div>
</div>
<div
class="pane-wrapper side background"
set:color={calendarPageViewModel.chatViewModel.displayedColor}
>
<div class="pane">
<div
class="content"
children:set={sidePaneContentWrapper}
></div>
</div>
</div>
<div children:set={taskSettingsModal}></div>
</div>
</div>
<div
class="pane-wrapper side background"
set:color={calendarPageViewModel.chatViewModel.displayedColor}
>
<div class="pane">
<div class="content" children:set={sidePaneContentWrapper}></div>
</div>
</div>
<div children:set={taskSettingsModal}></div>
</div>
);
);
}
+78 -78
View File
@@ -1,168 +1,168 @@
/* layout */
#chat-page #main #message-page .content {
padding: 0px;
padding: 0px;
overflow: hidden;
overflow: hidden;
display: grid;
grid-template-rows: 1fr var(--control-height) var(--safe-area-bottom);
display: grid;
grid-template-rows: 1fr var(--control-height) var(--safe-area-bottom);
}
#message-page #message-container {
overflow: auto;
overflow: auto;
}
/* composer */
#message-page #composer {
display: flex;
justify-content: center;
display: flex;
justify-content: center;
}
#message-page #composer input {
margin: 0px;
margin: 0px;
width: 100%;
max-width: unset;
flex: 1;
border-top: 1px solid var(--border);
border-left: 1px solid var(--border);
}
@media (max-width: 700px) {
#message-page #composer div {
width: 100%;
max-width: unset;
display: flex;
}
flex: 1;
border-top: 1px solid var(--border);
border-left: 1px solid var(--border);
}
@media (max-width: 700px) {
#message-page #composer div {
width: 100%;
max-width: unset;
display: flex;
}
}
@media (min-width: 700px) {
#message-page #composer .content-width-constraint,
#message-page #composer .input-width-constraint {
display: flex;
justify-content: flex-end;
#message-page #composer .content-width-constraint,
#message-page #composer .input-width-constraint {
display: flex;
justify-content: flex-end;
width: 100%;
max-width: var(--max-content-width);
}
width: 100%;
max-width: var(--max-content-width);
}
#message-page #composer .content-width-constraint {
max-width: var(--max-content-width);
}
#message-page #composer .content-width-constraint {
max-width: var(--max-content-width);
}
#message-page #composer .input-width-constraint {
max-width: var(--max-input-width);
}
#message-page #composer .input-width-constraint {
max-width: var(--max-input-width);
}
}
/* message */
#message-page #message-container {
display: flex;
flex-direction: column;
align-items: center;
display: flex;
flex-direction: column;
align-items: center;
padding: var(--content-padding);
gap: var(--gap-between-tiles);
padding: var(--content-padding);
gap: var(--gap-between-tiles);
height: auto;
max-height: unset;
height: auto;
max-height: unset;
scroll-behavior: smooth;
scroll-behavior: smooth;
}
.message-bubble {
display: flex;
width: 100%;
max-width: var(--max-content-width);
display: flex;
width: 100%;
max-width: var(--max-content-width);
}
.message-bubble .tile.main {
display: flex;
flex-direction: row;
align-items: start;
display: flex;
flex-direction: row;
align-items: start;
padding: 0px;
padding: 0px;
width: 100%;
max-width: var(--max-input-width);
overflow: hidden;
width: 100%;
max-width: var(--max-input-width);
overflow: hidden;
}
.message-bubble .tile.main .text-container {
display: flex;
flex-direction: column;
display: flex;
flex-direction: column;
overflow: hidden;
overflow: hidden;
width: 100%;
flex: 1;
width: 100%;
flex: 1;
}
.message-bubble .tile.main .button-container {
display: flex;
display: flex;
width: var(--control-height);
max-width: var(--control-height);
width: var(--control-height);
max-width: var(--control-height);
}
.message-bubble .tile.main button {
justify-content: center;
justify-content: center;
padding: 0px;
width: 100%;
padding: 0px;
width: 100%;
--surface: transparent;
--surface: transparent;
}
.message-bubble .tile.main .text-container {
padding: var(--control-padding);
padding: var(--control-padding);
}
.message-bubble .tile.main .timestamp,
.message-bubble .tile.main .timestamp .icon,
.message-bubble .tile.main .sender-name {
font-size: 0.8rem;
opacity: 0.6;
font-size: 0.8rem;
opacity: 0.6;
}
.message-bubble .tile.main .timestamp {
display: flex;
align-items: center;
gap: var(--gap-between-tiles);
display: flex;
align-items: center;
gap: var(--gap-between-tiles);
}
.message-bubble .tile.main .timestamp .icon {
display: none;
display: none;
}
.message-bubble .tile.main .sender-name {
font-weight: bold;
font-weight: bold;
}
.message-bubble .tile.main .body {
word-break: break-word;
word-break: break-word;
}
.message-bubble[sentbyuser] {
justify-content: flex-end;
justify-content: flex-end;
}
.message-bubble[sentbyuser] {
align-items: flex-end;
align-items: flex-end;
}
.message-bubble[sentbyuser] .tile.main,
.message-bubble[sentbyuser] .tile.main .timestamp {
flex-direction: row-reverse;
flex-direction: row-reverse;
}
.message-bubble[sentbyuser] .tile.main .timestamp .icon {
display: flex;
display: flex;
}
.message-bubble[sentbyuser] .tile.main span {
text-align: right;
text-align: right;
}
+70 -58
View File
@@ -7,69 +7,81 @@ import MessagePageViewModel from "../../ViewModel/Pages/messagePageViewModel";
import { translations } from "../translations";
export function MessagePage(messagePageViewModel: MessagePageViewModel) {
messagePageViewModel.loadData();
messagePageViewModel.loadData();
const messageContainer = (
<div
id="message-container"
children:append={[
messagePageViewModel.chatMessageViewModels,
ChatMessageViewModelToView,
]}
></div>
);
const messageContainer = (
<div
id="message-container"
children:append={[
messagePageViewModel.chatMessageViewModels,
ChatMessageViewModelToView,
]}
></div>
);
function scrollDown() {
messageContainer.scrollTop = messageContainer.scrollHeight;
}
function scrollDownIfApplicable() {
const scrollFromBottom =
messageContainer.scrollHeight -
(messageContainer.scrollTop + messageContainer.offsetHeight);
if (scrollFromBottom > 400) return;
function scrollDown() {
messageContainer.scrollTop = messageContainer.scrollHeight;
}
function scrollDownIfApplicable() {
const scrollFromBottom =
messageContainer.scrollHeight -
(messageContainer.scrollTop + messageContainer.offsetHeight);
if (scrollFromBottom > 400) return;
scrollDown();
}
messagePageViewModel.chatMessageViewModels.subscribeSilent(
scrollDownIfApplicable
);
setTimeout(() => scrollDown(), 100);
scrollDown();
}
messagePageViewModel.chatMessageViewModels.subscribeSilent(
scrollDownIfApplicable,
);
setTimeout(() => scrollDown(), 100);
return (
<div id="message-page">
<div class="pane-wrapper">
<div class="pane">
<div class="toolbar">
<span class="title">{translations.chatPage.message.messagesHeadline}</span>
</div>
<div class="content">
{messageContainer}
<div id="composer">
<div class="content-width-constraint">
<div class="input-width-constraint">
<input
bind:value={messagePageViewModel.composingMessage}
on:enter={messagePageViewModel.sendMessage}
placeholder={
translations.chatPage.message.composerInputPlaceholder
}
></input>
<button
class="primary"
aria-label={
translations.chatPage.message.sendMessageButtonAudioLabel
}
on:click={messagePageViewModel.sendMessage}
toggle:disabled={messagePageViewModel.cannotSendMessage}
>
<span class="icon">send</span>
</button>
return (
<div id="message-page">
<div class="pane-wrapper">
<div class="pane">
<div class="toolbar">
<span class="title">
{translations.chatPage.message.messagesHeadline}
</span>
</div>
<div class="content">
{messageContainer}
<div id="composer">
<div class="content-width-constraint">
<div class="input-width-constraint">
<input
bind:value={
messagePageViewModel.composingMessage
}
on:enter={
messagePageViewModel.sendMessage
}
placeholder={
translations.chatPage.message
.composerInputPlaceholder
}
></input>
<button
class="primary"
aria-label={
translations.chatPage.message
.sendMessageButtonAudioLabel
}
on:click={
messagePageViewModel.sendMessage
}
toggle:disabled={
messagePageViewModel.cannotSendMessage
}
>
<span class="icon">send</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
);
}
+169 -133
View File
@@ -8,141 +8,177 @@ import SettingsPageViewModel from "../../ViewModel/Pages/settingsPageViewModel";
import { translations } from "../translations";
export function SettingsPage(settingsPageViewModel: SettingsPageViewModel) {
settingsPageViewModel.loadData();
settingsPageViewModel.loadData();
const secondaryChannelConverter: React.StateItemConverter<string> = (
secondaryChannel: string
) => {
return DeletableListItem(secondaryChannel, <span></span>, () => {
settingsPageViewModel.removeSecondaryChannel(secondaryChannel);
});
};
const secondaryChannelConverter: React.StateItemConverter<string> = (
secondaryChannel: string,
) => {
return DeletableListItem(secondaryChannel, <span></span>, () => {
settingsPageViewModel.removeSecondaryChannel(secondaryChannel);
});
};
return (
<div id="settings-page">
<div class="pane-wrapper">
<div class="pane">
<div class="toolbar">
<span class="title">
{translations.chatPage.settings.settingsHeadline}
</span>
</div>
<div class="content">
<label class="tile flex-no">
<span class="icon">forum</span>
<div>
<span>
{translations.chatPage.settings.primaryChannelLabel}
</span>
<input
bind:value={settingsPageViewModel.primaryChannelInput}
on:enter={settingsPageViewModel.setPrimaryChannel}
></input>
</div>
</label>
<div class="flex-row justify-end width-input">
<button
class="width-50"
aria-label={
translations.chatPage.settings
.setPrimaryChannelButtonAudioLabel
}
on:click={settingsPageViewModel.setPrimaryChannel}
toggle:disabled={settingsPageViewModel.cannotSetPrimaryChannel}
>
{translations.general.setButton}
<span class="icon">check</span>
</button>
return (
<div id="settings-page">
<div class="pane-wrapper">
<div class="pane">
<div class="toolbar">
<span class="title">
{translations.chatPage.settings.settingsHeadline}
</span>
</div>
<div class="content">
<label class="tile flex-no">
<span class="icon">forum</span>
<div>
<span>
{
translations.chatPage.settings
.primaryChannelLabel
}
</span>
<input
bind:value={
settingsPageViewModel.primaryChannelInput
}
on:enter={
settingsPageViewModel.setPrimaryChannel
}
></input>
</div>
</label>
<div class="flex-row justify-end width-input">
<button
class="width-50"
aria-label={
translations.chatPage.settings
.setPrimaryChannelButtonAudioLabel
}
on:click={
settingsPageViewModel.setPrimaryChannel
}
toggle:disabled={
settingsPageViewModel.cannotSetPrimaryChannel
}
>
{translations.general.setButton}
<span class="icon">check</span>
</button>
</div>
<hr></hr>
<div class="flex-row width-input margin-bottom">
<input
aria-label={
translations.chatPage.settings
.newSecondaryChannelAudioLabel
}
placeholder={
translations.chatPage.settings
.newSecondaryChannelPlaceholder
}
bind:value={
settingsPageViewModel.newSecondaryChannelInput
}
on:enter={
settingsPageViewModel.addSecondaryChannel
}
></input>
<button
class="primary"
aria-label={
translations.chatPage.settings
.addSecondaryChannelButtonAudioLabel
}
on:click={
settingsPageViewModel.addSecondaryChannel
}
toggle:disabled={
settingsPageViewModel.cannotAddSecondaryChannel
}
>
<span class="icon">add</span>
</button>
</div>
<div
class="flex-column gap width-input"
children:append={[
settingsPageViewModel.secondaryChannels,
secondaryChannelConverter,
]}
></div>
<hr></hr>
<label class="tile flex-no">
<span class="icon">key</span>
<div>
<span>
{
translations.chatPage.settings
.encryptionKeyLabel
}
</span>
<input
bind:value={
settingsPageViewModel.encryptionKeyInput
}
on:enter={
settingsPageViewModel.setEncryptionKey
}
set:type={
settingsPageViewModel.encryptionKeyInputType
}
></input>
</div>
</label>
<div class="flex-row justify-end width-input">
<button
class="width-50"
aria-label={
translations.chatPage.settings
.setEncryptionKeyButtonAudioLabel
}
on:click={
settingsPageViewModel.setEncryptionKey
}
toggle:disabled={
settingsPageViewModel.cannotSetEncryptionKey
}
>
{translations.general.setButton}
<span class="icon">check</span>
</button>
</div>
<label class="inline">
<input
type="checkbox"
bind:checked={
settingsPageViewModel.shouldShowEncryptionKey
}
></input>
{translations.chatPage.settings.showEncryptionKey}
</label>
<hr></hr>
{ColorPicker(settingsPageViewModel.color)}
<hr></hr>
<div class="width-input">
{DangerousActionButton(
translations.chatPage.settings.deleteChatButton,
"chat_error",
settingsPageViewModel.remove,
)}
</div>
</div>
</div>
</div>
<hr></hr>
<div class="flex-row width-input margin-bottom">
<input
aria-label={
translations.chatPage.settings.newSecondaryChannelAudioLabel
}
placeholder={
translations.chatPage.settings.newSecondaryChannelPlaceholder
}
bind:value={settingsPageViewModel.newSecondaryChannelInput}
on:enter={settingsPageViewModel.addSecondaryChannel}
></input>
<button
class="primary"
aria-label={
translations.chatPage.settings
.addSecondaryChannelButtonAudioLabel
}
on:click={settingsPageViewModel.addSecondaryChannel}
toggle:disabled={
settingsPageViewModel.cannotAddSecondaryChannel
}
>
<span class="icon">add</span>
</button>
</div>
<div
class="flex-column gap width-input"
children:append={[
settingsPageViewModel.secondaryChannels,
secondaryChannelConverter,
]}
></div>
<hr></hr>
<label class="tile flex-no">
<span class="icon">key</span>
<div>
<span>{translations.chatPage.settings.encryptionKeyLabel}</span>
<input
bind:value={settingsPageViewModel.encryptionKeyInput}
on:enter={settingsPageViewModel.setEncryptionKey}
set:type={settingsPageViewModel.encryptionKeyInputType}
></input>
</div>
</label>
<div class="flex-row justify-end width-input">
<button
class="width-50"
aria-label={
translations.chatPage.settings
.setEncryptionKeyButtonAudioLabel
}
on:click={settingsPageViewModel.setEncryptionKey}
toggle:disabled={settingsPageViewModel.cannotSetEncryptionKey}
>
{translations.general.setButton}
<span class="icon">check</span>
</button>
</div>
<label class="inline">
<input
type="checkbox"
bind:checked={settingsPageViewModel.shouldShowEncryptionKey}
></input>
{translations.chatPage.settings.showEncryptionKey}
</label>
<hr></hr>
{ColorPicker(settingsPageViewModel.color)}
<hr></hr>
<div class="width-input">
{DangerousActionButton(
translations.chatPage.settings.deleteChatButton,
"chat_error",
settingsPageViewModel.remove
)}
</div>
</div>
</div>
</div>
</div>
);
);
}
+63 -63
View File
@@ -1,53 +1,53 @@
#chat-page > div > #main > #task-page > .pane-wrapper > .pane > .main-content {
width: 100%;
max-width: 100%;
width: 100%;
max-width: 100%;
padding: 0px;
padding: 0px;
}
#chat-page
> div
> #main
> #task-page
> .pane-wrapper
> .pane
> .main-content
> div {
animation: slide-up var(--animation-duration);
> div
> #main
> #task-page
> .pane-wrapper
> .pane
> .main-content
> div {
animation: slide-up var(--animation-duration);
}
#chat-page > div > #main > #task-page .task-grid {
display: flex;
flex-direction: column;
gap: var(--gap-between-tiles);
display: flex;
flex-direction: column;
gap: var(--gap-between-tiles);
padding: var(--content-padding);
padding: var(--content-padding);
}
#chat-page > div > #main > #task-page .kanban-board-wrapper,
#chat-page > div > #main > #task-page .status-page-content {
display: flex;
gap: var(--large-gap);
display: flex;
gap: var(--large-gap);
width: 100%;
height: 100%;
width: 100%;
height: 100%;
overflow: auto;
overflow: auto;
padding: var(--content-padding);
padding: var(--content-padding);
}
#chat-page > div > #main > #task-page .kanban-board-wrapper .kanban-column,
#chat-page > div > #main > #task-page .status-grid-wrapper .status-column {
display: flex;
flex-direction: column;
display: flex;
flex-direction: column;
gap: var(--gap-between-tiles);
gap: var(--gap-between-tiles);
}
#chat-page > div > #main > #task-page .property-input-wrapper {
display: flex;
flex-direction: row;
display: flex;
flex-direction: row;
}
#chat-page > div > #main > #task-page .kanban-board-wrapper .kanban-column,
@@ -55,72 +55,72 @@
#chat-page > div > #main > #task-page .status-grid-wrapper .status-column,
#chat-page > div > #main > #task-page .status-name-row > *,
#chat-page > div > #main > #task-page .status-name-row::before {
width: 18rem;
min-width: 18rem;
max-width: 18rem;
width: 18rem;
min-width: 18rem;
max-width: 18rem;
}
#chat-page > div > #main > #task-page .status-name-row,
#chat-page > div > #main > #task-page .status-grid-wrapper {
display: flex;
gap: var(--large-gap);
display: flex;
gap: var(--large-gap);
}
#chat-page > div > #main > #task-page .status-page-content,
#chat-page > div > #main > #task-page .status-grid-wrapper {
flex-direction: column;
flex-direction: column;
}
#chat-page > div > #main > #task-page .status-name-row::before {
content: "";
display: flex;
content: "";
display: flex;
}
#chat-page > div > #main > #task-page .board-close-button {
display: none;
display: none;
}
#chat-page > div > #main > #task-page .board-toggle-button {
display: flex;
display: flex;
}
#chat-page
> div
> #main
> #task-page[isshowingboard][showingboardlist="false"]
#board-list {
display: none;
> div
> #main
> #task-page[isshowingboard][showingboardlist="false"]
#board-list {
display: none;
}
@media (min-width: 640px) {
#chat-page > div > #main > #task-page .task-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
}
#chat-page > div > #main > #task-page .task-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
}
}
@media (max-width: 1024px) {
#chat-page > div > #main > #task-page #board-list {
display: false;
}
#chat-page > div > #main > #task-page #board-list {
display: false;
}
#chat-page > div > #main > #task-page #board-content {
display: none;
}
#chat-page > div > #main > #task-page #board-content {
display: none;
}
#chat-page > div > #main > #task-page[isshowingboard] #board-list {
display: none;
}
#chat-page > div > #main > #task-page[isshowingboard] #board-list {
display: none;
}
#chat-page > div > #main > #task-page[isshowingboard] #board-content {
display: flex;
}
#chat-page > div > #main > #task-page[isshowingboard] #board-content {
display: flex;
}
#chat-page > div > #main > #task-page .board-close-button {
display: flex;
}
#chat-page > div > #main > #task-page .board-close-button {
display: flex;
}
#chat-page > div > #main > #task-page .board-toggle-button {
display: none;
}
#chat-page > div > #main > #task-page .board-toggle-button {
display: none;
}
}
+86 -76
View File
@@ -8,87 +8,97 @@ import TaskPageViewModel from "../../ViewModel/Pages/taskPageViewModel";
import { translations } from "../translations";
export function TaskPage(taskPageViewModel: TaskPageViewModel) {
taskPageViewModel.loadData();
taskPageViewModel.loadData();
const isShowingBoard = React.createProxyState(
[taskPageViewModel.selectedBoardId],
() => taskPageViewModel.selectedBoardId.value != undefined
);
const isShowingBoard = React.createProxyState(
[taskPageViewModel.selectedBoardId],
() => taskPageViewModel.selectedBoardId.value != undefined,
);
const paneContent = React.createProxyState(
[taskPageViewModel.selectedBoardId],
() => {
const selectedBoardId = taskPageViewModel.selectedBoardId.value;
if (selectedBoardId == undefined) {
return (
<div class="pane align-center justify-center">
<span class="secondary">
{translations.chatPage.task.noBoardSelected}
</span>
</div>
);
}
const selectedBoard =
taskPageViewModel.boardViewModels.value.get(selectedBoardId);
if (selectedBoard == undefined) {
return (
<div class="pane align-center justify-center">
<span class="secondary">
{translations.chatPage.task.boardNotFound}
</span>
</div>
);
}
const paneContent = React.createProxyState(
[taskPageViewModel.selectedBoardId],
() => {
const selectedBoardId = taskPageViewModel.selectedBoardId.value;
if (selectedBoardId == undefined) {
return (
<div class="pane align-center justify-center">
<span class="secondary">
{translations.chatPage.task.noBoardSelected}
</span>
</div>
);
}
const selectedBoard =
taskPageViewModel.boardViewModels.value.get(selectedBoardId);
if (selectedBoard == undefined) {
return (
<div class="pane align-center justify-center">
<span class="secondary">
{translations.chatPage.task.boardNotFound}
</span>
</div>
);
}
return BoardPage(selectedBoard);
}
);
return BoardPage(selectedBoard);
},
);
return (
<div id="task-page" toggle:isshowingboard={isShowingBoard} set:showingboardlist={taskPageViewModel.isShowingBoadList}>
<div
id="board-list"
class="pane-wrapper side background"
set:color={taskPageViewModel.chatViewModel.displayedColor}
>
<div class="pane">
<div class="toolbar">
<div class="flex-row width-input">
<input
class="no-outline"
bind:value={taskPageViewModel.newBoardNameInput}
on:enter={taskPageViewModel.createBoard}
placeholder={translations.chatPage.task.newBoardNamePlaceholder}
></input>
<button
class="primary"
aria-label={
translations.chatPage.task.createBoardButtonAudioLabel
}
on:click={taskPageViewModel.createBoard}
toggle:disabled={taskPageViewModel.cannotCreateBoard}
>
<span class="icon">add</span>
</button>
</div>
</div>
<div class="content">
return (
<div
id="task-page"
toggle:isshowingboard={isShowingBoard}
set:showingboardlist={taskPageViewModel.isShowingBoadList}
>
<div
class="grid gap"
style="grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr))"
children:append={[
taskPageViewModel.boardViewModels,
BoardViewModelToEntry,
]}
id="board-list"
class="pane-wrapper side background"
set:color={taskPageViewModel.chatViewModel.displayedColor}
>
<div class="pane">
<div class="toolbar">
<div class="flex-row width-input">
<input
class="no-outline"
bind:value={taskPageViewModel.newBoardNameInput}
on:enter={taskPageViewModel.createBoard}
placeholder={
translations.chatPage.task
.newBoardNamePlaceholder
}
></input>
<button
class="primary"
aria-label={
translations.chatPage.task
.createBoardButtonAudioLabel
}
on:click={taskPageViewModel.createBoard}
toggle:disabled={
taskPageViewModel.cannotCreateBoard
}
>
<span class="icon">add</span>
</button>
</div>
</div>
<div class="content">
<div
class="grid gap"
style="grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr))"
children:append={[
taskPageViewModel.boardViewModels,
BoardViewModelToEntry,
]}
></div>
</div>
</div>
</div>
<div
id="board-content"
class="pane-wrapper"
children:set={paneContent}
></div>
</div>
</div>
</div>
<div
id="board-content"
class="pane-wrapper"
children:set={paneContent}
></div>
</div>
);
);
}
+22 -19
View File
@@ -4,29 +4,32 @@ import BoardViewModel from "../../ViewModel/Pages/boardViewModel";
import { allowDrop } from "../utility";
export function BoardEntry(boardViewModel: BoardViewModel) {
const view = (
<button
set:color={boardViewModel.color}
class="tile colored-tile"
toggle:selected={boardViewModel.isSelected}
on:click={boardViewModel.select}
on:dragover={allowDrop}
on:drop={boardViewModel.handleDropBetweenBoards}
>
<span class="shadow" subscribe:innerText={boardViewModel.name}></span>
<b subscribe:innerText={boardViewModel.name}></b>
</button>
);
const view = (
<button
set:color={boardViewModel.color}
class="tile colored-tile"
toggle:selected={boardViewModel.isSelected}
on:click={boardViewModel.select}
on:dragover={allowDrop}
on:drop={boardViewModel.handleDropBetweenBoards}
>
<span
class="shadow"
subscribe:innerText={boardViewModel.name}
></span>
<b subscribe:innerText={boardViewModel.name}></b>
</button>
);
boardViewModel.index.subscribe((newIndex) => {
view.style.order = newIndex;
});
boardViewModel.index.subscribe((newIndex) => {
view.style.order = newIndex;
});
return view;
return view;
}
export const BoardViewModelToEntry: React.StateItemConverter<BoardViewModel> = (
boardViewModel: BoardViewModel
boardViewModel: BoardViewModel,
) => {
return BoardEntry(boardViewModel);
return BoardEntry(boardViewModel);
};
+13 -13
View File
@@ -1,25 +1,25 @@
import * as React from "bloatless-react";
import BoardViewModel, {
BoardPageType,
BoardPageType,
} from "../../ViewModel/Pages/boardViewModel";
import { RibbonButton } from "./ribbonButton";
export function BoardViewToggleButton(
label: string,
icon: string,
page: BoardPageType,
boardViewModel: BoardViewModel
label: string,
icon: string,
page: BoardPageType,
boardViewModel: BoardViewModel,
) {
function select() {
boardViewModel.selectedPage.value = page;
}
function select() {
boardViewModel.selectedPage.value = page;
}
const isSelected = React.createProxyState(
[boardViewModel.selectedPage],
() => boardViewModel.selectedPage.value == page
);
const isSelected = React.createProxyState(
[boardViewModel.selectedPage],
() => boardViewModel.selectedPage.value == page,
);
return RibbonButton(label, icon, isSelected, select);
return RibbonButton(label, icon, isSelected, select);
}
+27 -23
View File
@@ -3,33 +3,37 @@ import * as React from "bloatless-react";
import ChatViewModel from "../../ViewModel/Chat/chatViewModel";
export function ChatEntry(chatViewModel: ChatViewModel) {
const view = (
<button
class="tile colored-tile chat-entry"
set:color={chatViewModel.settingsPageViewModel.color}
style="height: 8rem"
on:click={chatViewModel.open}
toggle:unread={chatViewModel.hasUnreadMessages}
>
<span
class="shadow"
subscribe:innerText={chatViewModel.settingsPageViewModel.primaryChannel}
></span>
<h2
subscribe:innerText={chatViewModel.settingsPageViewModel.primaryChannel}
></h2>
</button>
);
const view = (
<button
class="tile colored-tile chat-entry"
set:color={chatViewModel.settingsPageViewModel.color}
style="height: 8rem"
on:click={chatViewModel.open}
toggle:unread={chatViewModel.hasUnreadMessages}
>
<span
class="shadow"
subscribe:innerText={
chatViewModel.settingsPageViewModel.primaryChannel
}
></span>
<h2
subscribe:innerText={
chatViewModel.settingsPageViewModel.primaryChannel
}
></h2>
</button>
);
chatViewModel.index.subscribe((newIndex) => {
view.style.order = newIndex;
});
chatViewModel.index.subscribe((newIndex) => {
view.style.order = newIndex;
});
return view;
return view;
}
export const ChatViewModelToChatEntry: React.StateItemConverter<
ChatViewModel
ChatViewModel
> = (chatViewModel: ChatViewModel) => {
return ChatEntry(chatViewModel);
return ChatEntry(chatViewModel);
};
+55 -51
View File
@@ -6,61 +6,65 @@ import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel";
import { translations } from "../translations";
export function ChatMessage(chatMessageViewModel: ChatMessageViewModel) {
const statusIcon = React.createProxyState(
[chatMessageViewModel.status],
() => {
switch (chatMessageViewModel.status.value) {
case ChatMessageStatus.Outbox:
return "hourglass_top";
case ChatMessageStatus.Sent:
return "check";
case ChatMessageStatus.Received:
return "done_all";
default:
return "warning";
}
}
);
return (
<div
class="message-bubble"
toggle:sentbyuser={chatMessageViewModel.sentByUser}
>
<div class="main tile">
<div class="text-container">
<span class="sender-name ellipsis">
{chatMessageViewModel.sender}
</span>
<span
class="body"
subscribe:innerText={chatMessageViewModel.body}
></span>
<span class="timestamp ellipsis">
<span class="icon" subscribe:innerText={statusIcon}></span>
{chatMessageViewModel.dateSent}
</span>
</div>
<div class="button-container">
<button
on:click={chatMessageViewModel.showInfoModal}
aria-label={
translations.chatPage.message.showMessageInfoButtonAudioLabel
const statusIcon = React.createProxyState(
[chatMessageViewModel.status],
() => {
switch (chatMessageViewModel.status.value) {
case ChatMessageStatus.Outbox:
return "hourglass_top";
case ChatMessageStatus.Sent:
return "check";
case ChatMessageStatus.Received:
return "done_all";
default:
return "warning";
}
>
<span class="icon">info</span>
</button>
</div>
</div>
},
);
{ChatMessageInfoModal(chatMessageViewModel)}
</div>
);
return (
<div
class="message-bubble"
toggle:sentbyuser={chatMessageViewModel.sentByUser}
>
<div class="main tile">
<div class="text-container">
<span class="sender-name ellipsis">
{chatMessageViewModel.sender}
</span>
<span
class="body"
subscribe:innerText={chatMessageViewModel.body}
></span>
<span class="timestamp ellipsis">
<span
class="icon"
subscribe:innerText={statusIcon}
></span>
{chatMessageViewModel.dateSent}
</span>
</div>
<div class="button-container">
<button
on:click={chatMessageViewModel.showInfoModal}
aria-label={
translations.chatPage.message
.showMessageInfoButtonAudioLabel
}
>
<span class="icon">info</span>
</button>
</div>
</div>
{ChatMessageInfoModal(chatMessageViewModel)}
</div>
);
}
export const ChatMessageViewModelToView: React.StateItemConverter<
ChatMessageViewModel
ChatMessageViewModel
> = (chatMessageViewModel: ChatMessageViewModel) => {
return ChatMessage(chatMessageViewModel);
return ChatMessage(chatMessageViewModel);
};
+13 -13
View File
@@ -1,25 +1,25 @@
import * as React from "bloatless-react";
import ChatViewModel, {
ChatPageType,
ChatPageType,
} from "../../ViewModel/Chat/chatViewModel";
import { RibbonButton } from "./ribbonButton";
export function ChatViewToggleButton(
label: string,
icon: string,
page: ChatPageType,
chatViewModel: ChatViewModel
label: string,
icon: string,
page: ChatPageType,
chatViewModel: ChatViewModel,
) {
function select() {
chatViewModel.selectedPage.value = page;
}
function select() {
chatViewModel.selectedPage.value = page;
}
const isSelected = React.createProxyState(
[chatViewModel.selectedPage],
() => chatViewModel.selectedPage.value == page
);
const isSelected = React.createProxyState(
[chatViewModel.selectedPage],
() => chatViewModel.selectedPage.value == page,
);
return RibbonButton(label, icon, isSelected, select);
return RibbonButton(label, icon, isSelected, select);
}
+22 -22
View File
@@ -3,28 +3,28 @@ import * as React from "bloatless-react";
import { Color } from "../../colors";
export function ColorPicker(selectedColor: React.State<Color>) {
return (
<div class="flex-row gap width-input">
{...Object.values(Color).map((color) => {
const isSelected = React.createProxyState(
[selectedColor],
() => selectedColor.value == color
);
return (
<div class="flex-row gap width-input">
{...Object.values(Color).map((color) => {
const isSelected = React.createProxyState(
[selectedColor],
() => selectedColor.value == color,
);
function setColor() {
selectedColor.value = color;
}
function setColor() {
selectedColor.value = color;
}
return (
<button
color={color}
class="fill-color width-100 flex"
style="height: 2rem"
toggle:selected={isSelected}
on:click={setColor}
></button>
);
})}
</div>
);
return (
<button
color={color}
class="fill-color width-100 flex"
style="height: 2rem"
toggle:selected={isSelected}
on:click={setColor}
></button>
);
})}
</div>
);
}
+38 -38
View File
@@ -3,45 +3,45 @@ import * as React from "bloatless-react";
import { translations } from "../translations";
export function DangerousActionButton(
label: string,
icon: string,
action: () => void
label: string,
icon: string,
action: () => void,
) {
const isActionRequested = new React.State(false);
const cannotConfirm = React.createProxyState(
[isActionRequested],
() => isActionRequested.value == false
);
const isActionRequested = new React.State(false);
const cannotConfirm = React.createProxyState(
[isActionRequested],
() => isActionRequested.value == false,
);
function requestAction() {
isActionRequested.value = true;
}
function abort() {
isActionRequested.value = false;
}
function requestAction() {
isActionRequested.value = true;
}
function abort() {
isActionRequested.value = false;
}
return (
<div class="flex-row">
<button class="flex" on:click={abort} toggle:hidden={cannotConfirm}>
{translations.general.abortButton}
<span class="icon">undo</span>
</button>
<button
class="danger flex"
on:click={requestAction}
toggle:hidden={isActionRequested}
>
{label}
<span class="icon">{icon}</span>
</button>
<button
class="danger flex"
on:click={action}
toggle:hidden={cannotConfirm}
>
{translations.general.confirmButton}
<span class="icon">warning</span>
</button>
</div>
);
return (
<div class="flex-row">
<button class="flex" on:click={abort} toggle:hidden={cannotConfirm}>
{translations.general.abortButton}
<span class="icon">undo</span>
</button>
<button
class="danger flex"
on:click={requestAction}
toggle:hidden={isActionRequested}
>
{label}
<span class="icon">{icon}</span>
</button>
<button
class="danger flex"
on:click={action}
toggle:hidden={cannotConfirm}
>
{translations.general.confirmButton}
<span class="icon">warning</span>
</button>
</div>
);
}
+18 -18
View File
@@ -3,25 +3,25 @@ import * as React from "bloatless-react";
import { translations } from "../translations";
export function DeletableListItem(
text: string,
primaryButton: HTMLElement,
ondelete: () => void
text: string,
primaryButton: HTMLElement,
ondelete: () => void,
) {
return (
<div class="tile flex-row justify-apart align-center padding-0">
<span class="padding-h ellipsis">{text}</span>
return (
<div class="tile flex-row justify-apart align-center padding-0">
<span class="padding-h ellipsis">{text}</span>
<div class="flex-row justify-end">
{primaryButton}
<div class="flex-row justify-end">
{primaryButton}
<button
class="danger"
aria-label={translations.general.deleteItemButtonAudioLabel}
on:click={ondelete}
>
<span class="icon">delete</span>
</button>
</div>
</div>
);
<button
class="danger"
aria-label={translations.general.deleteItemButtonAudioLabel}
on:click={ondelete}
>
<span class="icon">delete</span>
</button>
</div>
</div>
);
}
+56 -56
View File
@@ -1,79 +1,79 @@
import * as React from "bloatless-react";
import StorageModel, {
PATH_COMPONENT_SEPARATOR,
PATH_COMPONENT_SEPARATOR,
} from "../../Model/Global/storageModel";
import StorageViewModel from "../../ViewModel/Global/storageViewModel";
export function DirectoryItemList(
storageViewModel: StorageViewModel,
pathString: string = PATH_COMPONENT_SEPARATOR
storageViewModel: StorageViewModel,
pathString: string = PATH_COMPONENT_SEPARATOR,
) {
// converter
const StringToDirectoryItemList: React.StateItemConverter<string> = (
pathString: string
) => DirectoryItemList(storageViewModel, pathString);
// converter
const StringToDirectoryItemList: React.StateItemConverter<string> = (
pathString: string,
) => DirectoryItemList(storageViewModel, pathString);
// data
const path = StorageModel.stringToPathComponents(pathString);
const fileName = StorageModel.getFileName(path);
const items = new React.ListState<string>();
// data
const path = StorageModel.stringToPathComponents(pathString);
const fileName = StorageModel.getFileName(path);
const items = new React.ListState<string>();
const style = `text-indent: ${path.length}rem`;
const style = `text-indent: ${path.length}rem`;
// methods
function loadItems() {
items.clear();
// methods
function loadItems() {
items.clear();
const directoryItems = storageViewModel.storageModel.list(path);
const directoryItems = storageViewModel.storageModel.list(path);
for (const directoryItem of directoryItems) {
const itemPath = [...path, directoryItem];
const pathString = StorageModel.pathComponentsToString(...itemPath);
items.add(pathString);
for (const directoryItem of directoryItems) {
const itemPath = [...path, directoryItem];
const pathString = StorageModel.pathComponentsToString(...itemPath);
items.add(pathString);
}
}
}
function select() {
storageViewModel.selectedPath.value = pathString;
}
function select() {
storageViewModel.selectedPath.value = pathString;
}
// handlers
storageViewModel.lastDeletedItemPath.subscribe((lastDeletedItemPath) => {
if (!items.value.has(lastDeletedItemPath)) return;
// handlers
storageViewModel.lastDeletedItemPath.subscribe((lastDeletedItemPath) => {
if (!items.value.has(lastDeletedItemPath)) return;
select();
setTimeout(() => loadItems(), 50);
});
select();
setTimeout(() => loadItems(), 50);
});
// state
const isSelected = React.createProxyState(
[storageViewModel.selectedPath],
() => storageViewModel.selectedPath.value == pathString
);
// state
const isSelected = React.createProxyState(
[storageViewModel.selectedPath],
() => storageViewModel.selectedPath.value == pathString,
);
isSelected.subscribe(() => {
if (isSelected.value == false) return;
loadItems();
});
isSelected.subscribe(() => {
if (isSelected.value == false) return;
loadItems();
});
return (
<div class="flex-column">
<button
class="width-100 flex-1 clip"
toggle:selected={isSelected}
on:click={select}
>
<span class="ellipsis width-100 flex-1" style={style}>
{fileName}
</span>
</button>
return (
<div class="flex-column">
<button
class="width-100 flex-1 clip"
toggle:selected={isSelected}
on:click={select}
>
<span class="ellipsis width-100 flex-1" style={style}>
{fileName}
</span>
</button>
<div
class="flex-column"
children:append={[items, StringToDirectoryItemList]}
></div>
</div>
);
<div
class="flex-column"
children:append={[items, StringToDirectoryItemList]}
></div>
</div>
);
}
+33 -33
View File
@@ -1,62 +1,62 @@
.file-browser {
width: 100%;
height: 100%;
width: 100%;
height: 100%;
display: flex;
display: flex;
overflow-x: scroll;
overflow-y: hidden;
scroll-behavior: smooth;
scroll-snap-type: x mandatory;
overflow-x: scroll;
overflow-y: hidden;
scroll-behavior: smooth;
scroll-snap-type: x mandatory;
}
.file-browser > div {
display: flex;
flex-direction: column;
display: flex;
flex-direction: column;
width: 100%;
min-width: 100%;
flex: 0;
width: 100%;
min-width: 100%;
flex: 0;
scroll-snap-align: center;
scroll-snap-align: center;
}
.file-browser .scroll-area {
flex: 1;
width: 100%;
overflow-y: auto;
flex: 1;
width: 100%;
overflow-y: auto;
padding: var(--content-padding);
padding: var(--content-padding);
}
.file-browser .detail-button-wrapper {
display: flex;
justify-content: end;
display: flex;
justify-content: end;
border-top: 1px solid var(--border);
padding: var(--content-padding);
border-top: 1px solid var(--border);
padding: var(--content-padding);
}
.file-browser .detail-button-wrapper button {
max-width: 100%;
max-width: 100%;
}
.file-browser code {
width: 100%;
height: 100%;
width: 100%;
height: 100%;
font-size: .9rem;
font-size: 0.9rem;
}
@media (min-width: 640px) {
.file-browser > div {
width: 50%;
min-width: 50%;
.file-browser > div {
width: 50%;
min-width: 50%;
flex: 1;
}
flex: 1;
}
.file-browser .detail-button-wrapper {
display: none;
}
.file-browser .detail-button-wrapper {
display: none;
}
}
+73 -63
View File
@@ -9,75 +9,85 @@ import StorageViewModel from "../../ViewModel/Global/storageViewModel";
import { translations } from "../translations";
export function FileBrowser(storageViewModel: StorageViewModel) {
const detailView = React.createProxyState(
[storageViewModel.selectedPath],
() => {
if (storageViewModel.selectedPath.value == PATH_COMPONENT_SEPARATOR)
return (
<div>
<span class="secondary">{translations.storage.noItemSelected}</span>
const detailView = React.createProxyState(
[storageViewModel.selectedPath],
() => {
if (storageViewModel.selectedPath.value == PATH_COMPONENT_SEPARATOR)
return (
<div>
<span class="secondary">
{translations.storage.noItemSelected}
</span>
<hr></hr>
<hr></hr>
{DangerousActionButton(
translations.storage.removeJunkButton,
"delete_forever",
storageViewModel.removeJunk
)}
</div>
);
{DangerousActionButton(
translations.storage.removeJunkButton,
"delete_forever",
storageViewModel.removeJunk,
)}
</div>
);
return (
<div class="flex-column gap">
<div class="tile flex-no">
return (
<div class="flex-column gap">
<div class="tile flex-no">
<div>
<b>{translations.storage.path}</b>
<span
class="break-all"
subscribe:innerText={
storageViewModel.selectedPath
}
></span>
</div>
</div>
<div class="tile flex-no">
<div>
<b>{translations.storage.content}</b>
<code
subscribe:innerText={
storageViewModel.selectedFileContent
}
></code>
</div>
</div>
{DangerousActionButton(
translations.storage.deleteItem,
"delete_forever",
storageViewModel.deleteSelectedItem,
)}
</div>
);
},
);
const view = (
<div class="file-browser">
<div>
<b>{translations.storage.path}</b>
<span
class="break-all"
subscribe:innerText={storageViewModel.selectedPath}
></span>
<div class="scroll-area">
{DirectoryItemList(storageViewModel)}
</div>
<div class="detail-button-wrapper">
<button class="ghost" on:click={scrollToDetails}>
<span
class="ellipsis"
subscribe:innerText={
storageViewModel.selectedFileName
}
></span>
<span class="icon">arrow_forward</span>
</button>
</div>
</div>
</div>
<div class="tile flex-no">
<div>
<b>{translations.storage.content}</b>
<code
subscribe:innerText={storageViewModel.selectedFileContent}
></code>
</div>
</div>
{DangerousActionButton(
translations.storage.deleteItem,
"delete_forever",
storageViewModel.deleteSelectedItem
)}
<div class="scroll-area" children:set={detailView}></div>
</div>
);
);
function scrollToDetails() {
view.scrollLeft = view.scrollWidth;
}
);
const view = (
<div class="file-browser">
<div>
<div class="scroll-area">{DirectoryItemList(storageViewModel)}</div>
<div class="detail-button-wrapper">
<button class="ghost" on:click={scrollToDetails}>
<span
class="ellipsis"
subscribe:innerText={storageViewModel.selectedFileName}
></span>
<span class="icon">arrow_forward</span>
</button>
</div>
</div>
<div class="scroll-area" children:set={detailView}></div>
</div>
);
function scrollToDetails() {
view.scrollLeft = view.scrollWidth;
}
return view;
return view;
}
+18 -18
View File
@@ -1,30 +1,30 @@
import * as React from "bloatless-react";
import {
StringEntryObject,
checkDoesObjectMatchReference,
StringEntryObject,
checkDoesObjectMatchReference,
} from "../../Model/Utility/utility";
export function FilteredList<T>(
reference: StringEntryObject,
stringEntryObjectConverter: (object: T) => StringEntryObject,
objects: React.ListState<T> | React.MapState<T>,
viewBuilder: (matchingObjects: React.ListState<T>) => HTMLElement
reference: StringEntryObject,
stringEntryObjectConverter: (object: T) => StringEntryObject,
objects: React.ListState<T> | React.MapState<T>,
viewBuilder: (matchingObjects: React.ListState<T>) => HTMLElement,
) {
const matchingObjects: React.ListState<T> = new React.ListState();
const matchingObjects: React.ListState<T> = new React.ListState();
objects.handleAddition((newObject) => {
const doesMatch: boolean = checkDoesObjectMatchReference(
reference,
stringEntryObjectConverter(newObject)
);
if (doesMatch == false) return;
objects.handleAddition((newObject) => {
const doesMatch: boolean = checkDoesObjectMatchReference(
reference,
stringEntryObjectConverter(newObject),
);
if (doesMatch == false) return;
matchingObjects.add(newObject);
objects.handleRemoval(newObject, () => {
matchingObjects.remove(newObject);
matchingObjects.add(newObject);
objects.handleRemoval(newObject, () => {
matchingObjects.remove(newObject);
});
});
});
return viewBuilder(matchingObjects);
return viewBuilder(matchingObjects);
}
+55 -55
View File
@@ -1,106 +1,106 @@
.month-grid-wrapper {
display: flex;
flex-direction: column;
display: flex;
flex-direction: column;
gap: var(--gap-between-tiles);
gap: var(--gap-between-tiles);
height: 100%;
overflow: auto;
height: 100%;
overflow: auto;
}
.month-grid-wrapper .day-labels {
flex: 0;
flex: 0;
padding: var(--content-padding);
padding-bottom: 0px;
padding: var(--content-padding);
padding-bottom: 0px;
}
.month-grid-wrapper > div {
display: grid;
gap: var(--gap-between-tiles);
grid-template-columns: repeat(7, minmax(0px, 1fr));
display: grid;
gap: var(--gap-between-tiles);
grid-template-columns: repeat(7, minmax(0px, 1fr));
animation: slide-up var(--animation-duration);
animation: slide-up var(--animation-duration);
}
.month-grid {
overflow: visible;
overflow: visible;
padding: var(--content-padding);
padding-top: 0px;
padding: var(--content-padding);
padding-top: 0px;
flex: 1;
flex: 1;
grid-auto-rows: 1fr;
grid-auto-rows: 1fr;
}
.month-grid .tile {
animation: slide-up var(--animation-duration);
overflow: hidden;
animation: slide-up var(--animation-duration);
overflow: hidden;
align-items: start;
align-items: start;
}
.month-grid .tile[today] b {
color: var(--primary);
color: var(--primary);
}
.month-grid .tile > div {
width: 100%;
height: 100%;
width: 100%;
height: 100%;
}
.month-grid .tile .event-count {
display: none;
display: none;
background-color: var(--text-normal);
color: var(--background);
background-color: var(--text-normal);
color: var(--background);
opacity: 0.6;
border-radius: 100%;
opacity: 0.6;
border-radius: 100%;
width: 1rem;
height: 1rem;
font-size: 0.6rem;
font-weight: bold;
width: 1rem;
height: 1rem;
font-size: 0.6rem;
font-weight: bold;
align-items: center;
justify-content: center;
align-items: center;
justify-content: center;
}
.month-grid .tile .event-count[has-events] {
background-color: var(--primary);
opacity: 1;
background-color: var(--primary);
opacity: 1;
}
.month-grid .tile .event-list {
display: flex;
flex-direction: column;
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
height: 100%;
width: 100%;
gap: var(--gap-between-tiles);
gap: var(--gap-between-tiles);
}
.month-grid .tile .event-list span {
animation: slide-up var(--animation-duration);
animation: slide-up var(--animation-duration);
}
@media (max-width: 640px) and (max-height: 900px) {
.month-grid .tile {
padding: 0px;
}
.month-grid .tile {
padding: 0px;
}
.month-grid .tile > div {
align-items: center;
justify-content: center;
}
.month-grid .tile > div {
align-items: center;
justify-content: center;
}
.month-grid .tile .event-count {
display: flex;
}
.month-grid .tile .event-count {
display: flex;
}
.month-grid .tile .event-list {
display: none;
}
.month-grid .tile .event-list {
display: none;
}
}
+99 -97
View File
@@ -8,103 +8,105 @@ import { allowDrop } from "../utility";
import { localeCompare } from "../../Model/Utility/utility";
export function MonthGrid<T>(
monthGrid: MonthGrid<React.MapState<T>>,
selectedDate: React.State<number>,
handleDrop: (date: string) => void
monthGrid: MonthGrid<React.MapState<T>>,
selectedDate: React.State<number>,
handleDrop: (date: string) => void,
) {
const dayLabels: HTMLElement[] = [];
let currentWeekday = monthGrid.firstDayOfWeek;
while (dayLabels.length < 7) {
dayLabels.push(
<span class="secondary">
{translations.regional.weekdays.abbreviated[currentWeekday]}
</span>
const dayLabels: HTMLElement[] = [];
let currentWeekday = monthGrid.firstDayOfWeek;
while (dayLabels.length < 7) {
dayLabels.push(
<span class="secondary">
{translations.regional.weekdays.abbreviated[currentWeekday]}
</span>,
);
currentWeekday++;
if (currentWeekday == 7) currentWeekday = 0;
}
const offsetElements: HTMLElement[] = [];
for (let i = 0; i < monthGrid.offset; i++) {
offsetElements.push(<div></div>);
}
const converter: React.StateItemConverter<TaskViewModel> = (
taskViewModel,
) => {
const view = (
<span class="ellipsis secondary">{taskViewModel.task.name}</span>
);
taskViewModel.index.subscribe((newIndex) => {
view.style.order = newIndex;
});
return view;
};
return (
<div class="month-grid-wrapper">
<div class="day-labels">{...dayLabels}</div>
<div class="month-grid">
{...offsetElements}
{...Object.entries(monthGrid.days)
.sort((a, b) => localeCompare(a[0], b[0]))
.map((entry) => {
const [date, mapState] = entry;
const isSelected = React.createProxyState(
[selectedDate],
() => selectedDate.value == parseInt(date),
);
const isToday: boolean =
monthGrid.isCurrentMonth == true &&
parseInt(date) == new Date().getDate();
const eventCount: React.State<number> =
React.createProxyState(
[mapState],
() => mapState.value.size,
);
const hasEvents: React.State<boolean> =
React.createProxyState(
[eventCount],
() => eventCount.value != 0,
);
function select() {
selectedDate.value = parseInt(date);
}
function drop() {
handleDrop(date);
}
return (
<button
class="tile"
on:click={select}
toggle:selected={isSelected}
toggle:today={isToday}
on:dragover={allowDrop}
on:drop={drop}
>
<div>
<b>{date}</b>
<span
class="event-count"
toggle:has-events={hasEvents}
subscribe:innerText={eventCount}
></span>
<div
class="event-list"
children:append={[mapState, converter]}
></div>
</div>
</button>
);
})}
</div>
</div>
);
currentWeekday++;
if (currentWeekday == 7) currentWeekday = 0;
}
const offsetElements: HTMLElement[] = [];
for (let i = 0; i < monthGrid.offset; i++) {
offsetElements.push(<div></div>);
}
const converter: React.StateItemConverter<TaskViewModel> = (
taskViewModel
) => {
const view = (
<span class="ellipsis secondary">{taskViewModel.task.name}</span>
);
taskViewModel.index.subscribe((newIndex) => {
view.style.order = newIndex;
});
return view;
};
return (
<div class="month-grid-wrapper">
<div class="day-labels">{...dayLabels}</div>
<div class="month-grid">
{...offsetElements}
{...Object.entries(monthGrid.days)
.sort((a, b) => localeCompare(a[0], b[0]))
.map((entry) => {
const [date, mapState] = entry;
const isSelected = React.createProxyState(
[selectedDate],
() => selectedDate.value == parseInt(date)
);
const isToday: boolean =
monthGrid.isCurrentMonth == true &&
parseInt(date) == new Date().getDate();
const eventCount: React.State<number> = React.createProxyState(
[mapState],
() => mapState.value.size
);
const hasEvents: React.State<boolean> = React.createProxyState(
[eventCount],
() => eventCount.value != 0
);
function select() {
selectedDate.value = parseInt(date);
}
function drop() {
handleDrop(date);
}
return (
<button
class="tile"
on:click={select}
toggle:selected={isSelected}
toggle:today={isToday}
on:dragover={allowDrop}
on:drop={drop}
>
<div>
<b>{date}</b>
<span
class="event-count"
toggle:has-events={hasEvents}
subscribe:innerText={eventCount}
></span>
<div
class="event-list"
children:append={[mapState, converter]}
></div>
</div>
</button>
);
})}
</div>
</div>
);
}
+14 -14
View File
@@ -1,32 +1,32 @@
import * as React from "bloatless-react";
export function Option(text: string, value: string, selectedOnCreate: boolean) {
return (
<option value={value} toggle:selected={selectedOnCreate}>
{text}
</option>
);
return (
<option value={value} toggle:selected={selectedOnCreate}>
{text}
</option>
);
}
export const StringToOption: React.StateItemConverter<string> = (
string: string
string: string,
) => {
return Option(string, string, false);
return Option(string, string, false);
};
export const EntryToOption: React.StateItemConverter<Entry> = (
entry: Entry
entry: Entry,
) => {
return Option(entry[1], entry[0], false);
return Option(entry[1], entry[0], false);
};
export const VersionIdToOption: React.StateItemConverter<string> = (
versionId: string
versionId: string,
) => {
const [date, rest] = versionId.split("T");
const [time] = rest.split(".");
const readableName = `${date} ${time}`;
return Option(readableName, versionId, false);
const [date, rest] = versionId.split("T");
const [time] = rest.split(".");
const readableName = `${date} ${time}`;
return Option(readableName, versionId, false);
};
export type Entry = [string, string];
+49 -48
View File
@@ -1,70 +1,71 @@
import * as React from "bloatless-react";
import {
StringEntryObject,
collectObjectValuesForKey,
localeCompare,
StringEntryObject,
collectObjectValuesForKey,
localeCompare,
} from "../../Model/Utility/utility";
export function PropertyValueList<T>(
propertyKey: string,
stringEntryObjectConverter: (object: T) => StringEntryObject,
objects: React.ListState<T> | React.MapState<T>,
viewBuilder: (
keys: React.ListState<string>,
sortedKeys: React.State<string[]>
) => HTMLElement
propertyKey: string,
stringEntryObjectConverter: (object: T) => StringEntryObject,
objects: React.ListState<T> | React.MapState<T>,
viewBuilder: (
keys: React.ListState<string>,
sortedKeys: React.State<string[]>,
) => HTMLElement,
) {
const propertyValues: React.ListState<string> = new React.ListState();
const sortedPropertyValues: React.State<string[]> = createSortedPropertyValueState(propertyValues)
objects.subscribe(() => {
collectPropertyValuesToState(
propertyKey,
stringEntryObjectConverter,
objects,
propertyValues
);
});
const propertyValues: React.ListState<string> = new React.ListState();
const sortedPropertyValues: React.State<string[]> =
createSortedPropertyValueState(propertyValues);
objects.subscribe(() => {
collectPropertyValuesToState(
propertyKey,
stringEntryObjectConverter,
objects,
propertyValues,
);
});
return viewBuilder(propertyValues, sortedPropertyValues);
return viewBuilder(propertyValues, sortedPropertyValues);
}
export function collectPropertyValuesToState<T>(
propertyKey: string,
stringEntryObjectConverter: (object: T) => StringEntryObject,
objects: React.ListState<T> | React.MapState<T>,
propertyValues: React.ListState<string>
propertyKey: string,
stringEntryObjectConverter: (object: T) => StringEntryObject,
objects: React.ListState<T> | React.MapState<T>,
propertyValues: React.ListState<string>,
) {
const values: string[] = collectObjectValuesForKey(
propertyKey,
stringEntryObjectConverter,
[...objects.value.values()]
);
for (const existingValue of values) {
if (propertyValues.value.has(existingValue)) continue;
propertyValues.add(existingValue);
}
for (const displayedValue of propertyValues.value.values()) {
if (values.includes(displayedValue) == false) {
propertyValues.remove(displayedValue);
const values: string[] = collectObjectValuesForKey(
propertyKey,
stringEntryObjectConverter,
[...objects.value.values()],
);
for (const existingValue of values) {
if (propertyValues.value.has(existingValue)) continue;
propertyValues.add(existingValue);
}
for (const displayedValue of propertyValues.value.values()) {
if (values.includes(displayedValue) == false) {
propertyValues.remove(displayedValue);
}
}
}
}
export function createSortedPropertyValueState(
propertyValues: React.ListState<string>
propertyValues: React.ListState<string>,
): React.State<string[]> {
return React.createProxyState([propertyValues], () =>
[...propertyValues.value.values()].sort(localeCompare)
);
return React.createProxyState([propertyValues], () =>
[...propertyValues.value.values()].sort(localeCompare),
);
}
export function createPropertyValueIndexState(
sortedKeys: React.State<string[]>,
key: string
sortedKeys: React.State<string[]>,
key: string,
): React.State<number> {
return React.createProxyState([sortedKeys], () =>
sortedKeys.value.indexOf(key)
);
return React.createProxyState([sortedKeys], () =>
sortedKeys.value.indexOf(key),
);
}
+15 -15
View File
@@ -1,23 +1,23 @@
import * as React from "bloatless-react";
import BoardViewModel, {
BoardPageType,
BoardPageType,
} from "../../ViewModel/Pages/boardViewModel";
export function RibbonButton(
label: string,
icon: string,
isSelected: React.State<boolean>,
select: () => void
label: string,
icon: string,
isSelected: React.State<boolean>,
select: () => void,
) {
return (
<button
class="ribbon-button"
aria-label={label}
toggle:selected={isSelected}
on:click={select}
>
<span class="icon">{icon}</span>
</button>
);
return (
<button
class="ribbon-button"
aria-label={label}
toggle:selected={isSelected}
on:click={select}
>
<span class="icon">{icon}</span>
</button>
);
}
+46 -43
View File
@@ -3,55 +3,58 @@ import * as React from "bloatless-react";
import TaskViewModel from "../../ViewModel/Pages/taskViewModel";
export function TaskEntry(taskViewModel: TaskViewModel) {
const details: { [icon: string]: string } = {
description: taskViewModel.description.value || "---",
priority_high: taskViewModel.priority.value || "---",
category: taskViewModel.category.value || "---",
clock_loader_40: taskViewModel.status.value || "---",
calendar_month: taskViewModel.date.value || "---",
schedule: taskViewModel.time.value || "---",
};
const details: { [icon: string]: string } = {
description: taskViewModel.description.value || "---",
priority_high: taskViewModel.priority.value || "---",
category: taskViewModel.category.value || "---",
clock_loader_40: taskViewModel.status.value || "---",
calendar_month: taskViewModel.date.value || "---",
schedule: taskViewModel.time.value || "---",
};
const view = (
<button
draggable="true"
class="tile flex-no"
style="user-select: none; -webkit-user-select: none"
on:click={taskViewModel.open}
on:dragstart={taskViewModel.dragStart}
>
<div>
<b class="ellipsis" subscribe:innerText={taskViewModel.name}></b>
<hr></hr>
<div
class="grid secondary"
style="grid-template-columns: repeat(2, 1fr); column-gap: 1rem; row-gap: .5rem"
const view = (
<button
draggable="true"
class="tile flex-no"
style="user-select: none; -webkit-user-select: none"
on:click={taskViewModel.open}
on:dragstart={taskViewModel.dragStart}
>
{...Object.entries(details).map((entry) => (
<span
class="flex-row align-center width-100 flex-no clip"
style="gap: 1rem"
>
<span class="icon" style="font-size: 1.1rem">
{entry[0]}
</span>
<span class="ellipsis">{entry[1]}</span>
</span>
))}
</div>
</div>
</button>
);
<div>
<b
class="ellipsis"
subscribe:innerText={taskViewModel.name}
></b>
<hr></hr>
<div
class="grid secondary"
style="grid-template-columns: repeat(2, 1fr); column-gap: 1rem; row-gap: .5rem"
>
{...Object.entries(details).map((entry) => (
<span
class="flex-row align-center width-100 flex-no clip"
style="gap: 1rem"
>
<span class="icon" style="font-size: 1.1rem">
{entry[0]}
</span>
<span class="ellipsis">{entry[1]}</span>
</span>
))}
</div>
</div>
</button>
);
taskViewModel.index.subscribe((newIndex) => {
view.style.order = newIndex;
});
taskViewModel.index.subscribe((newIndex) => {
view.style.order = newIndex;
});
return view;
return view;
}
export const TaskViewModelToEntry: React.StateItemConverter<TaskViewModel> = (
taskViewModel: TaskViewModel
taskViewModel: TaskViewModel,
) => {
return TaskEntry(taskViewModel);
return TaskEntry(taskViewModel);
};
+2 -2
View File
@@ -1,7 +1,7 @@
import * as React from "bloatless-react";
export const StringToTextSpan: React.StateItemConverter<string> = (
string: string
string: string,
) => {
return <span class="ellipsis">{string}</span>
return <span class="ellipsis">{string}</span>;
};
+41 -36
View File
@@ -6,42 +6,47 @@ import { DangerousActionButton } from "../Components/dangerousActionButton";
import { translations } from "../translations";
export function BoardSettingsModal(boardViewModel: BoardViewModel) {
return (
<div class="modal" toggle:open={boardViewModel.isPresentingSettingsModal}>
<div>
<main>
<h2>{translations.chatPage.task.boardSettingsHeadline}</h2>
<label class="tile flex-no">
<span class="icon">label</span>
return (
<div
class="modal"
toggle:open={boardViewModel.isPresentingSettingsModal}
>
<div>
<span>{translations.chatPage.task.boardNameInputLabel}</span>
<input
on:enter={boardViewModel.saveSettings}
bind:value={boardViewModel.name}
></input>
<main>
<h2>{translations.chatPage.task.boardSettingsHeadline}</h2>
<label class="tile flex-no">
<span class="icon">label</span>
<div>
<span>
{translations.chatPage.task.boardNameInputLabel}
</span>
<input
on:enter={boardViewModel.saveSettings}
bind:value={boardViewModel.name}
></input>
</div>
</label>
<hr></hr>
{ColorPicker(boardViewModel.color)}
<hr></hr>
<div class="width-input">
{DangerousActionButton(
translations.chatPage.task.deleteBoardButton,
"delete_forever",
boardViewModel.deleteBoard,
)}
</div>
</main>
<button on:click={boardViewModel.hideSettings}>
{translations.general.closeButton}
<span class="icon">close</span>
</button>
</div>
</label>
<hr></hr>
{ColorPicker(boardViewModel.color)}
<hr></hr>
<div class="width-input">
{DangerousActionButton(
translations.chatPage.task.deleteBoardButton,
"delete_forever",
boardViewModel.deleteBoard
)}
</div>
</main>
<button on:click={boardViewModel.hideSettings}>
{translations.general.closeButton}
<span class="icon">close</span>
</button>
</div>
</div>
);
</div>
);
}
+88 -66
View File
@@ -4,73 +4,95 @@ import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel";
import { translations } from "../translations";
export function ChatMessageInfoModal(
chatMessageViewModel: ChatMessageViewModel
chatMessageViewModel: ChatMessageViewModel,
) {
return (
<div class="modal" toggle:open={chatMessageViewModel.isPresentingInfoModal}>
<div>
<main>
<h2>{translations.chatPage.message.messageInfoHeadline}</h2>
return (
<div
class="modal"
toggle:open={chatMessageViewModel.isPresentingInfoModal}
>
<div>
<main>
<h2>{translations.chatPage.message.messageInfoHeadline}</h2>
<div class="flex-column gap">
<div class="tile">
<span class="icon">account_circle</span>
<div>
<span>{translations.chatPage.message.sentBy}</span>
<b class="break-word">{chatMessageViewModel.sender}</b>
</div>
<div class="flex-column gap">
<div class="tile">
<span class="icon">account_circle</span>
<div>
<span>
{translations.chatPage.message.sentBy}
</span>
<b class="break-word">
{chatMessageViewModel.sender}
</b>
</div>
</div>
<div class="tile">
<span class="icon">schedule</span>
<div>
<span>
{translations.chatPage.message.timeSent}
</span>
<b class="break-word">
{chatMessageViewModel.dateSent}
</b>
</div>
</div>
<div class="tile">
<span class="icon">forum</span>
<div>
<span>
{translations.chatPage.message.channel}
</span>
<b class="break-word">
{chatMessageViewModel.channel}
</b>
</div>
</div>
<div class="tile">
<span class="icon">description</span>
<div>
<span>
{
translations.chatPage.message
.messageContent
}
</span>
<b
class="break-word"
subscribe:innerText={
chatMessageViewModel.body
}
></b>
</div>
</div>
</div>
<hr></hr>
<div class="flex-column gap">
<button on:click={chatMessageViewModel.copyMessage}>
{translations.chatPage.message.copyMessageButton}
<span class="icon">content_copy</span>
</button>
<button on:click={chatMessageViewModel.resendMessage}>
{translations.chatPage.message.resendMessageButton}
<span class="icon">redo</span>
</button>
<button on:click={chatMessageViewModel.decryptMessage}>
{translations.chatPage.message.decryptMessageButton}
<span class="icon">key</span>
</button>
</div>
</main>
<button on:click={chatMessageViewModel.hideInfoModal}>
{translations.general.closeButton}
<span class="icon">close</span>
</button>
</div>
<div class="tile">
<span class="icon">schedule</span>
<div>
<span>{translations.chatPage.message.timeSent}</span>
<b class="break-word">{chatMessageViewModel.dateSent}</b>
</div>
</div>
<div class="tile">
<span class="icon">forum</span>
<div>
<span>{translations.chatPage.message.channel}</span>
<b class="break-word">{chatMessageViewModel.channel}</b>
</div>
</div>
<div class="tile">
<span class="icon">description</span>
<div>
<span>{translations.chatPage.message.messageContent}</span>
<b
class="break-word"
subscribe:innerText={chatMessageViewModel.body}
></b>
</div>
</div>
</div>
<hr></hr>
<div class="flex-column gap">
<button on:click={chatMessageViewModel.copyMessage}>
{translations.chatPage.message.copyMessageButton}
<span class="icon">content_copy</span>
</button>
<button on:click={chatMessageViewModel.resendMessage}>
{translations.chatPage.message.resendMessageButton}
<span class="icon">redo</span>
</button>
<button on:click={chatMessageViewModel.decryptMessage}>
{translations.chatPage.message.decryptMessageButton}
<span class="icon">key</span>
</button>
</div>
</main>
<button on:click={chatMessageViewModel.hideInfoModal}>
{translations.general.closeButton}
<span class="icon">close</span>
</button>
</div>
</div>
);
</div>
);
}
+56 -52
View File
@@ -5,58 +5,62 @@ import { DeletableListItem } from "../Components/deletableListItem";
import { translations } from "../translations";
export function ConnectionModal(connectionViewModel: ConnectionViewModel) {
const previousAddressConverter: React.StateItemConverter<string> = (
address: string
) => {
function connnect() {
connectionViewModel.connectToAddress(address);
}
const previousAddressConverter: React.StateItemConverter<string> = (
address: string,
) => {
function connnect() {
connectionViewModel.connectToAddress(address);
}
const cannotConnect = React.createProxyState(
[connectionViewModel.isConnected],
() =>
connectionViewModel.isConnected.value == true &&
connectionViewModel.connectionModel.address == address
const cannotConnect = React.createProxyState(
[connectionViewModel.isConnected],
() =>
connectionViewModel.isConnected.value == true &&
connectionViewModel.connectionModel.address == address,
);
return DeletableListItem(
address,
<button
class="primary"
on:click={connnect}
toggle:disabled={cannotConnect}
aria-label={
translations.connectionModal.connectButtonAudioLabel
}
>
<span class="icon">link</span>
</button>,
() => {
connectionViewModel.removePreviousAddress(address);
},
);
};
return (
<div
class="modal"
toggle:open={connectionViewModel.isShowingConnectionModal}
>
<div>
<main>
<h2>
{translations.connectionModal.connectionModalHeadline}
</h2>
<div
class="flex-column gap"
children:append={[
connectionViewModel.previousAddresses,
previousAddressConverter,
]}
></div>
</main>
<button on:click={connectionViewModel.hideConnectionModal}>
{translations.general.closeButton}
<span class="icon">close</span>
</button>
</div>
</div>
);
return DeletableListItem(
address,
<button
class="primary"
on:click={connnect}
toggle:disabled={cannotConnect}
aria-label={translations.connectionModal.connectButtonAudioLabel}
>
<span class="icon">link</span>
</button>,
() => {
connectionViewModel.removePreviousAddress(address);
}
);
};
return (
<div
class="modal"
toggle:open={connectionViewModel.isShowingConnectionModal}
>
<div>
<main>
<h2>{translations.connectionModal.connectionModalHeadline}</h2>
<div
class="flex-column gap"
children:append={[
connectionViewModel.previousAddresses,
previousAddressConverter,
]}
></div>
</main>
<button on:click={connectionViewModel.hideConnectionModal}>
{translations.general.closeButton}
<span class="icon">close</span>
</button>
</div>
</div>
);
}
+429 -351
View File
@@ -1,8 +1,8 @@
import * as React from "bloatless-react";
import FileTransferViewModel, {
FileTransferModal,
FileTransferOption,
FileTransferModal,
FileTransferOption,
} from "../../ViewModel/Global/fileTransferViewModel";
import ConnectionViewModel from "../../ViewModel/Global/connectionViewModel";
@@ -12,406 +12,484 @@ import { reload } from "../utility";
import { translations } from "../translations";
export function DataTransferModalWrapper(
connectionViewModel: ConnectionViewModel,
fileTransferViewModel: FileTransferViewModel
connectionViewModel: ConnectionViewModel,
fileTransferViewModel: FileTransferViewModel,
) {
return (
<div>
{DirectionSelectionModal(connectionViewModel, fileTransferViewModel)}
{FileSelectionModal(fileTransferViewModel)}
{TransferDataDisplayModal(fileTransferViewModel)}
{TransferDisplayModal(fileTransferViewModel)}
{TransferDataInputModal(fileTransferViewModel)}
{DataReceptionModal(fileTransferViewModel)}
</div>
);
return (
<div>
{DirectionSelectionModal(
connectionViewModel,
fileTransferViewModel,
)}
{FileSelectionModal(fileTransferViewModel)}
{TransferDataDisplayModal(fileTransferViewModel)}
{TransferDisplayModal(fileTransferViewModel)}
{TransferDataInputModal(fileTransferViewModel)}
{DataReceptionModal(fileTransferViewModel)}
</div>
);
}
function DirectionSelectionModal(
connectionViewModel: ConnectionViewModel,
fileTransferViewModel: FileTransferViewModel
connectionViewModel: ConnectionViewModel,
fileTransferViewModel: FileTransferViewModel,
) {
const isPresented = React.createProxyState(
[fileTransferViewModel.presentedModal],
() =>
fileTransferViewModel.presentedModal.value ==
FileTransferModal.DirectionSelection
);
const isPresented = React.createProxyState(
[fileTransferViewModel.presentedModal],
() =>
fileTransferViewModel.presentedModal.value ==
FileTransferModal.DirectionSelection,
);
const isDisconnected = React.createProxyState(
[connectionViewModel.isConnected],
() => connectionViewModel.isConnected.value == false
);
const isDisconnected = React.createProxyState(
[connectionViewModel.isConnected],
() => connectionViewModel.isConnected.value == false,
);
return (
<div class="modal" toggle:open={isPresented}>
<div>
<main>
<h2>{translations.dataTransferModal.transferDataHeadline}</h2>
return (
<div class="modal" toggle:open={isPresented}>
<div>
<main>
<h2>
{translations.dataTransferModal.transferDataHeadline}
</h2>
<p class="error" toggle:hidden={connectionViewModel.isConnected}>
{translations.dataTransferModal.notConnectedError}
</p>
<p
class="error"
toggle:hidden={connectionViewModel.isConnected}
>
{translations.dataTransferModal.notConnectedError}
</p>
<div
class="flex-column gap content-margin-bottom"
toggle:hidden={isDisconnected}
>
<button
class="tile"
on:click={fileTransferViewModel.showFileSelectionModal}
>
<span class="icon">upload</span>
<div>
<b>{translations.dataTransferModal.fromThisDeviceButton}</b>
</div>
<span class="icon">arrow_forward</span>
</button>
<button
class="tile"
on:click={fileTransferViewModel.showTransferDataInputModal}
>
<span class="icon">download</span>
<div>
<b>{translations.dataTransferModal.toThisDeviceButton}</b>
</div>
<span class="icon">arrow_forward</span>
</button>
</div>
</main>
<button on:click={fileTransferViewModel.hideModal}>
{translations.general.closeButton}
<span class="icon">close</span>
</button>
</div>
</div>
);
<div
class="flex-column gap content-margin-bottom"
toggle:hidden={isDisconnected}
>
<button
class="tile"
on:click={
fileTransferViewModel.showFileSelectionModal
}
>
<span class="icon">upload</span>
<div>
<b>
{
translations.dataTransferModal
.fromThisDeviceButton
}
</b>
</div>
<span class="icon">arrow_forward</span>
</button>
<button
class="tile"
on:click={
fileTransferViewModel.showTransferDataInputModal
}
>
<span class="icon">download</span>
<div>
<b>
{
translations.dataTransferModal
.toThisDeviceButton
}
</b>
</div>
<span class="icon">arrow_forward</span>
</button>
</div>
</main>
<button on:click={fileTransferViewModel.hideModal}>
{translations.general.closeButton}
<span class="icon">close</span>
</button>
</div>
</div>
);
}
function FileSelectionModal(fileTransferViewModel: FileTransferViewModel) {
// option
const OptionConverter: React.StateItemConverter<FileTransferOption> = (
fileOption: FileTransferOption
) => {
return OptionEntry(fileOption, fileTransferViewModel);
};
// option
const OptionConverter: React.StateItemConverter<FileTransferOption> = (
fileOption: FileTransferOption,
) => {
return OptionEntry(fileOption, fileTransferViewModel);
};
function OptionEntry(
fileOption: FileTransferOption,
fileTransferViewModel: FileTransferViewModel
) {
const isSelected = new React.State(false);
if (fileTransferViewModel.selectedPaths.value.has(fileOption.path)) {
isSelected.value = true;
}
isSelected.subscribeSilent((isSelected) => {
if (isSelected == true) {
fileTransferViewModel.selectedPaths.add(fileOption.path);
} else {
fileTransferViewModel.selectedPaths.remove(fileOption.path);
}
});
function toggle() {
isSelected.value = !isSelected.value;
function OptionEntry(
fileOption: FileTransferOption,
fileTransferViewModel: FileTransferViewModel,
) {
const isSelected = new React.State(false);
if (fileTransferViewModel.selectedPaths.value.has(fileOption.path)) {
isSelected.value = true;
}
isSelected.subscribeSilent((isSelected) => {
if (isSelected == true) {
fileTransferViewModel.selectedPaths.add(fileOption.path);
} else {
fileTransferViewModel.selectedPaths.remove(fileOption.path);
}
});
function toggle() {
isSelected.value = !isSelected.value;
}
return (
<button class="tile" toggle:selected={isSelected} on:click={toggle}>
<div>
<b class="ellipsis">{fileOption.label}</b>
<span class="secondary ellipsis">
{StorageModel.pathComponentsToString(
...fileOption.path,
)}
</span>
</div>
</button>
);
}
// state
const isPresented = React.createProxyState(
[fileTransferViewModel.presentedModal],
() =>
fileTransferViewModel.presentedModal.value ==
FileTransferModal.FileSelection,
);
return (
<button class="tile" toggle:selected={isSelected} on:click={toggle}>
<div>
<b class="ellipsis">{fileOption.label}</b>
<span class="secondary ellipsis">
{StorageModel.pathComponentsToString(...fileOption.path)}
</span>
<div class="modal" toggle:open={isPresented}>
<div>
<main>
<h2>
{translations.dataTransferModal.transferDataHeadline}
</h2>
<span class="secondary">
{translations.dataTransferModal.selectionDescription}
</span>
<hr></hr>
<h3>{translations.dataTransferModal.generalHeadline}</h3>
<div
class="flex-column gap content-margin-bottom"
children:append={[
fileTransferViewModel.generalFileOptions,
OptionConverter,
]}
></div>
<h3>{translations.dataTransferModal.chatsHeadline}</h3>
<div
class="flex-column gap"
children:append={[
fileTransferViewModel.chatFileOptions,
OptionConverter,
]}
></div>
</main>
<div class="flex-row width-100">
<button
class="flex"
on:click={
fileTransferViewModel.showDirectionSelectionModal
}
>
{translations.general.backButton}
</button>
<button
class="primary flex"
on:click={fileTransferViewModel.showTransferDataModal}
toggle:disabled={
fileTransferViewModel.hasNoPathsSelected
}
>
{translations.general.continueButton}
<span class="icon">arrow_forward</span>
</button>
</div>
</div>
</div>
</button>
);
}
// state
const isPresented = React.createProxyState(
[fileTransferViewModel.presentedModal],
() =>
fileTransferViewModel.presentedModal.value ==
FileTransferModal.FileSelection
);
return (
<div class="modal" toggle:open={isPresented}>
<div>
<main>
<h2>{translations.dataTransferModal.transferDataHeadline}</h2>
<span class="secondary">
{translations.dataTransferModal.selectionDescription}
</span>
<hr></hr>
<h3>{translations.dataTransferModal.generalHeadline}</h3>
<div
class="flex-column gap content-margin-bottom"
children:append={[
fileTransferViewModel.generalFileOptions,
OptionConverter,
]}
></div>
<h3>{translations.dataTransferModal.chatsHeadline}</h3>
<div
class="flex-column gap"
children:append={[
fileTransferViewModel.chatFileOptions,
OptionConverter,
]}
></div>
</main>
<div class="flex-row width-100">
<button
class="flex"
on:click={fileTransferViewModel.showDirectionSelectionModal}
>
{translations.general.backButton}
</button>
<button
class="primary flex"
on:click={fileTransferViewModel.showTransferDataModal}
toggle:disabled={fileTransferViewModel.hasNoPathsSelected}
>
{translations.general.continueButton}
<span class="icon">arrow_forward</span>
</button>
</div>
</div>
</div>
);
}
function TransferDataDisplayModal(
fileTransferViewModel: FileTransferViewModel
fileTransferViewModel: FileTransferViewModel,
) {
const isPresented = React.createProxyState(
[fileTransferViewModel.presentedModal],
() =>
fileTransferViewModel.presentedModal.value ==
FileTransferModal.TransferDataDisplay
);
const isPresented = React.createProxyState(
[fileTransferViewModel.presentedModal],
() =>
fileTransferViewModel.presentedModal.value ==
FileTransferModal.TransferDataDisplay,
);
return (
<div class="modal" toggle:open={isPresented}>
<div>
<main>
<h2>{translations.dataTransferModal.transferDataHeadline}</h2>
return (
<div class="modal" toggle:open={isPresented}>
<div>
<main>
<h2>
{translations.dataTransferModal.transferDataHeadline}
</h2>
<span class="secondary">
{translations.dataTransferModal.dataEntryDescription}
</span>
<span class="secondary">
{translations.dataTransferModal.dataEntryDescription}
</span>
<hr></hr>
<hr></hr>
<div class="flex-column gap content-margin-bottom">
<div class="tile">
<span class="icon">forum</span>
<div>
<span class="secondary">
{translations.dataTransferModal.transferChannelHeadline}
</span>
<b
subscribe:innerText={fileTransferViewModel.transferChannel}
></b>
</div>
<div class="flex-column gap content-margin-bottom">
<div class="tile">
<span class="icon">forum</span>
<div>
<span class="secondary">
{
translations.dataTransferModal
.transferChannelHeadline
}
</span>
<b
subscribe:innerText={
fileTransferViewModel.transferChannel
}
></b>
</div>
</div>
<div class="tile">
<span class="icon">key</span>
<div>
<span class="secondary">
{
translations.dataTransferModal
.transferKeyHeadline
}
</span>
<b
subscribe:innerText={
fileTransferViewModel.transferKey
}
></b>
</div>
</div>
</div>
</main>
<div class="flex-row width-100">
<button
class="flex"
on:click={fileTransferViewModel.showFileSelectionModal}
>
{translations.general.backButton}
</button>
<button
class="primary flex"
on:click={fileTransferViewModel.initiateTransfer}
>
{translations.dataTransferModal.sendButton}
<span class="icon">arrow_forward</span>
</button>
</div>
</div>
<div class="tile">
<span class="icon">key</span>
<div>
<span class="secondary">
{translations.dataTransferModal.transferKeyHeadline}
</span>
<b subscribe:innerText={fileTransferViewModel.transferKey}></b>
</div>
</div>
</div>
</main>
<div class="flex-row width-100">
<button
class="flex"
on:click={fileTransferViewModel.showFileSelectionModal}
>
{translations.general.backButton}
</button>
<button
class="primary flex"
on:click={fileTransferViewModel.initiateTransfer}
>
{translations.dataTransferModal.sendButton}
<span class="icon">arrow_forward</span>
</button>
</div>
</div>
</div>
);
);
}
function TransferDisplayModal(fileTransferViewModel: FileTransferViewModel) {
const isPresented = React.createProxyState(
[fileTransferViewModel.presentedModal],
() =>
fileTransferViewModel.presentedModal.value ==
FileTransferModal.TransferDisplay
);
const isPresented = React.createProxyState(
[fileTransferViewModel.presentedModal],
() =>
fileTransferViewModel.presentedModal.value ==
FileTransferModal.TransferDisplay,
);
return (
<div class="modal" toggle:open={isPresented}>
<div>
<main>
<h2>{translations.dataTransferModal.transferDataHeadline}</h2>
return (
<div class="modal" toggle:open={isPresented}>
<div>
<main>
<h2>
{translations.dataTransferModal.transferDataHeadline}
</h2>
<p
class="secondary"
subscribe:innerText={fileTransferViewModel.filesSentText}
></p>
<p
class="secondary"
subscribe:innerText={
fileTransferViewModel.filesSentText
}
></p>
<p
class="secondary"
toggle:hidden={fileTransferViewModel.didNotFinishSending}
>
{translations.dataTransferModal.allFilesSent}
</p>
<p
class="secondary"
toggle:hidden={
fileTransferViewModel.didNotFinishSending
}
>
{translations.dataTransferModal.allFilesSent}
</p>
<hr></hr>
<hr></hr>
<div
class="tile flex-column align-start"
children:append={[
fileTransferViewModel.filePathsSent,
StringToTextSpan,
]}
></div>
</main>
<div class="flex-row width-100">
<button
class="flex"
on:click={fileTransferViewModel.initiateTransfer}
>
{translations.dataTransferModal.sendAgainButton}
<span class="icon">restart_alt</span>
</button>
<button
class="flex"
on:click={fileTransferViewModel.hideModal}
toggle:disabled={fileTransferViewModel.didNotFinishSending}
>
{translations.general.closeButton}
<span class="icon">close</span>
</button>
<div
class="tile flex-column align-start"
children:append={[
fileTransferViewModel.filePathsSent,
StringToTextSpan,
]}
></div>
</main>
<div class="flex-row width-100">
<button
class="flex"
on:click={fileTransferViewModel.initiateTransfer}
>
{translations.dataTransferModal.sendAgainButton}
<span class="icon">restart_alt</span>
</button>
<button
class="flex"
on:click={fileTransferViewModel.hideModal}
toggle:disabled={
fileTransferViewModel.didNotFinishSending
}
>
{translations.general.closeButton}
<span class="icon">close</span>
</button>
</div>
</div>
</div>
</div>
</div>
);
);
}
function TransferDataInputModal(fileTransferViewModel: FileTransferViewModel) {
const isPresented = React.createProxyState(
[fileTransferViewModel.presentedModal],
() =>
fileTransferViewModel.presentedModal.value ==
FileTransferModal.TransferDataInput
);
const isPresented = React.createProxyState(
[fileTransferViewModel.presentedModal],
() =>
fileTransferViewModel.presentedModal.value ==
FileTransferModal.TransferDataInput,
);
return (
<div class="modal" toggle:open={isPresented}>
<div>
<main>
<h2>{translations.dataTransferModal.transferDataHeadline}</h2>
return (
<div class="modal" toggle:open={isPresented}>
<div>
<main>
<h2>
{translations.dataTransferModal.transferDataHeadline}
</h2>
<span class="secondary">
{translations.dataTransferModal.dataEntryInputDescription}
</span>
<span class="secondary">
{
translations.dataTransferModal
.dataEntryInputDescription
}
</span>
<hr></hr>
<hr></hr>
<div class="flex-column gap content-margin-bottom">
<label class="tile">
<span class="icon">forum</span>
<div>
<span class="secondary">
{translations.dataTransferModal.transferChannelHeadline}
</span>
<input
bind:value={fileTransferViewModel.receivingTransferChannel}
></input>
</div>
</label>
<label class="tile">
<span class="icon">key</span>
<div>
<span class="secondary">
{translations.dataTransferModal.transferKeyHeadline}
</span>
<input
bind:value={fileTransferViewModel.receivingTransferKey}
></input>
</div>
</label>
</div>
</main>
<div class="flex-row width-100">
<button
class="flex"
on:click={fileTransferViewModel.showDirectionSelectionModal}
>
{translations.general.backButton}
</button>
<button
class="primary flex"
on:click={fileTransferViewModel.prepareReceivingData}
toggle:disabled={fileTransferViewModel.cannotPrepareToReceive}
>
{translations.general.continueButton}
<span class="icon">arrow_forward</span>
</button>
<div class="flex-column gap content-margin-bottom">
<label class="tile">
<span class="icon">forum</span>
<div>
<span class="secondary">
{
translations.dataTransferModal
.transferChannelHeadline
}
</span>
<input
bind:value={
fileTransferViewModel.receivingTransferChannel
}
></input>
</div>
</label>
<label class="tile">
<span class="icon">key</span>
<div>
<span class="secondary">
{
translations.dataTransferModal
.transferKeyHeadline
}
</span>
<input
bind:value={
fileTransferViewModel.receivingTransferKey
}
></input>
</div>
</label>
</div>
</main>
<div class="flex-row width-100">
<button
class="flex"
on:click={
fileTransferViewModel.showDirectionSelectionModal
}
>
{translations.general.backButton}
</button>
<button
class="primary flex"
on:click={fileTransferViewModel.prepareReceivingData}
toggle:disabled={
fileTransferViewModel.cannotPrepareToReceive
}
>
{translations.general.continueButton}
<span class="icon">arrow_forward</span>
</button>
</div>
</div>
</div>
</div>
</div>
);
);
}
function DataReceptionModal(fileTransferViewModel: FileTransferViewModel) {
const isPresented = React.createProxyState(
[fileTransferViewModel.presentedModal],
() =>
fileTransferViewModel.presentedModal.value ==
FileTransferModal.ReceptionDisplay
);
const isPresented = React.createProxyState(
[fileTransferViewModel.presentedModal],
() =>
fileTransferViewModel.presentedModal.value ==
FileTransferModal.ReceptionDisplay,
);
return (
<div class="modal" toggle:open={isPresented}>
<div>
<main>
<h2>{translations.dataTransferModal.transferDataHeadline}</h2>
return (
<div class="modal" toggle:open={isPresented}>
<div>
<main>
<h2>
{translations.dataTransferModal.transferDataHeadline}
</h2>
<p class="secondary">
{translations.dataTransferModal.readyToReceiveDescription}
</p>
<p class="secondary">
{
translations.dataTransferModal
.readyToReceiveDescription
}
</p>
<p
class="secondary"
subscribe:innerText={fileTransferViewModel.filesReceivedText}
></p>
<p
class="secondary"
subscribe:innerText={
fileTransferViewModel.filesReceivedText
}
></p>
<hr></hr>
<hr></hr>
<div
class="tile flex-column align-start"
children:append={[
fileTransferViewModel.filePathsReceived,
StringToTextSpan,
]}
></div>
</main>
<button on:click={reload}>
{translations.general.reloadAppButton}
<span class="icon">refresh</span>
</button>
</div>
</div>
);
<div
class="tile flex-column align-start"
children:append={[
fileTransferViewModel.filePathsReceived,
StringToTextSpan,
]}
></div>
</main>
<button on:click={reload}>
{translations.general.reloadAppButton}
<span class="icon">refresh</span>
</button>
</div>
</div>
);
}
+56 -48
View File
@@ -6,57 +6,65 @@ import { translations } from "../translations";
import { v4 } from "uuid";
export function SearchModal<T>(
searchViewModel: SearchViewModel<T>,
headline: string,
converter: React.StateItemConverter<T>,
isOpen: React.State<boolean>
searchViewModel: SearchViewModel<T>,
headline: string,
converter: React.StateItemConverter<T>,
isOpen: React.State<boolean>,
) {
function close() {
isOpen.value = false;
}
function close() {
isOpen.value = false;
}
const suggestionId = v4();
const suggestionId = v4();
return (
<div class="modal" toggle:open={isOpen}>
<div style="max-width: 64rem">
<main>
<h2>{headline}</h2>
<div class="flex-row width-input">
<input
placeholder={translations.general.searchLabel}
bind:value={searchViewModel.searchInput}
on:enter={searchViewModel.applySearch}
list={suggestionId}
></input>
<datalist
hidden
id={suggestionId}
children:append={[searchViewModel.suggestions, StringToOption]}
></datalist>
<button
class="primary"
aria-label={translations.general.searchButtonAudioLabel}
on:click={searchViewModel.applySearch}
toggle:disabled={searchViewModel.cannotApplySearch}
>
<span class="icon">search</span>
</button>
</div>
return (
<div class="modal" toggle:open={isOpen}>
<div style="max-width: 64rem">
<main>
<h2>{headline}</h2>
<div class="flex-row width-input">
<input
placeholder={translations.general.searchLabel}
bind:value={searchViewModel.searchInput}
on:enter={searchViewModel.applySearch}
list={suggestionId}
></input>
<datalist
hidden
id={suggestionId}
children:append={[
searchViewModel.suggestions,
StringToOption,
]}
></datalist>
<button
class="primary"
aria-label={
translations.general.searchButtonAudioLabel
}
on:click={searchViewModel.applySearch}
toggle:disabled={searchViewModel.cannotApplySearch}
>
<span class="icon">search</span>
</button>
</div>
<hr></hr>
<hr></hr>
<div
class="grid gap"
style="grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr))"
children:append={[searchViewModel.matchingObjects, converter]}
></div>
</main>
<button on:click={close}>
{translations.general.closeButton}
<span class="icon">close</span>
</button>
</div>
</div>
);
<div
class="grid gap"
style="grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr))"
children:append={[
searchViewModel.matchingObjects,
converter,
]}
></div>
</main>
<button on:click={close}>
{translations.general.closeButton}
<span class="icon">close</span>
</button>
</div>
</div>
);
}
+11 -11
View File
@@ -5,15 +5,15 @@ import StorageViewModel from "../../ViewModel/Global/storageViewModel";
import { translations } from "../translations";
export function StorageModal(storageViewModel: StorageViewModel) {
return (
<div class="modal" toggle:open={storageViewModel.isShowingStorageModal}>
<div style="max-width: 64rem">
<main class="padding-0">{FileBrowser(storageViewModel)}</main>
<button on:click={storageViewModel.hideStorageModal}>
{translations.general.closeButton}
<span class="icon">close</span>
</button>
</div>
</div>
);
return (
<div class="modal" toggle:open={storageViewModel.isShowingStorageModal}>
<div style="max-width: 64rem">
<main class="padding-0">{FileBrowser(storageViewModel)}</main>
<button on:click={storageViewModel.hideStorageModal}>
{translations.general.closeButton}
<span class="icon">close</span>
</button>
</div>
</div>
);
}
+190 -152
View File
@@ -1,11 +1,11 @@
import * as React from "bloatless-react";
import {
Entry,
EntryToOption,
Option,
StringToOption,
VersionIdToOption,
Entry,
EntryToOption,
Option,
StringToOption,
VersionIdToOption,
} from "../Components/option";
import { DangerousActionButton } from "../Components/dangerousActionButton";
@@ -14,156 +14,194 @@ import { translations } from "../translations";
import { v4 } from "uuid";
export function TaskSettingsModal(taskViewModel: TaskViewModel) {
const categorySuggestionId = v4();
const statusSuggestionId = v4();
const categorySuggestionId = v4();
const statusSuggestionId = v4();
const BoardOptionConverter: React.StateItemConverter<Entry> = (
entry: Entry
) => {
const isSelected = entry[0] == taskViewModel.task.boardId;
return Option(entry[1], entry[0], isSelected);
};
const BoardOptionConverter: React.StateItemConverter<Entry> = (
entry: Entry,
) => {
const isSelected = entry[0] == taskViewModel.task.boardId;
return Option(entry[1], entry[0], isSelected);
};
return (
<div class="modal" open>
<div>
<main>
<h2>{translations.chatPage.task.taskSettingsHeadline}</h2>
<label class="tile flex-no">
<span class="icon">history</span>
return (
<div class="modal" open>
<div>
<span>{translations.general.fileVersionLabel}</span>
<select
bind:value={taskViewModel.selectedVersionId}
children:append={[taskViewModel.versionIds, VersionIdToOption]}
></select>
<span class="icon">arrow_drop_down</span>
<main>
<h2>{translations.chatPage.task.taskSettingsHeadline}</h2>
<label class="tile flex-no">
<span class="icon">history</span>
<div>
<span>{translations.general.fileVersionLabel}</span>
<select
bind:value={taskViewModel.selectedVersionId}
children:append={[
taskViewModel.versionIds,
VersionIdToOption,
]}
></select>
<span class="icon">arrow_drop_down</span>
</div>
</label>
<hr></hr>
<label class="tile flex-no">
<span class="icon">label</span>
<div>
<span>
{translations.chatPage.task.taskNameLabel}
</span>
<input bind:value={taskViewModel.name}></input>
</div>
</label>
<label class="tile flex-no">
<span class="icon">category</span>
<div>
<span>
{translations.chatPage.task.taskBoardLabel}
</span>
<select
bind:value={taskViewModel.boardId}
children:append={[
taskViewModel.chatViewModel
.taskBoardSuggestions,
BoardOptionConverter,
]}
></select>
<span class="icon">arrow_drop_down</span>
</div>
</label>
<label class="tile flex-no">
<span class="icon">description</span>
<div>
<span>
{
translations.chatPage.task
.taskDescriptionLabel
}
</span>
<textarea
rows="10"
bind:value={taskViewModel.description}
></textarea>
</div>
</label>
<hr></hr>
<label class="tile flex-no">
<span class="icon">category</span>
<div>
<span>
{translations.chatPage.task.taskCategoryLabel}
</span>
<input
bind:value={taskViewModel.category}
list={categorySuggestionId}
></input>
</div>
</label>
<datalist
hidden
id={categorySuggestionId}
children:append={[
taskViewModel.coreViewModel.taskCategorySuggestions,
StringToOption,
]}
></datalist>
<label class="tile flex-no">
<span class="icon">clock_loader_40</span>
<div>
<span>
{translations.chatPage.task.taskStatusLabel}
</span>
<input
bind:value={taskViewModel.status}
list={statusSuggestionId}
></input>
</div>
</label>
<datalist
hidden
id={statusSuggestionId}
children:append={[
taskViewModel.coreViewModel.taskStatusSuggestions,
StringToOption,
]}
></datalist>
<label class="tile flex-no">
<span class="icon">priority_high</span>
<div>
<span>
{translations.chatPage.task.taskPriorityLabel}
</span>
<input
type="number"
bind:value={taskViewModel.priority}
></input>
</div>
</label>
<hr></hr>
<label class="tile flex-no">
<span class="icon">calendar_month</span>
<div>
<span>
{translations.chatPage.task.taskDateLabel}
</span>
<input
type="date"
bind:value={taskViewModel.date}
></input>
</div>
</label>
<label class="tile flex-no">
<span class="icon">schedule</span>
<div>
<span>
{translations.chatPage.task.taskTimeLabel}
</span>
<input
type="time"
bind:value={taskViewModel.time}
></input>
</div>
</label>
<hr></hr>
<div class="width-input">
{DangerousActionButton(
translations.chatPage.task.deleteTaskButton,
"delete_forever",
taskViewModel.deleteTask,
)}
</div>
</main>
<div class="flex-row width-100">
<button
class="flex"
on:click={taskViewModel.closeAndDiscard}
>
{translations.general.closeButton}
</button>
<button
class="flex primary"
on:click={taskViewModel.closeAndSave}
>
{translations.general.saveButton}
<span class="icon">save</span>
</button>
</div>
</div>
</label>
<hr></hr>
<label class="tile flex-no">
<span class="icon">label</span>
<div>
<span>{translations.chatPage.task.taskNameLabel}</span>
<input bind:value={taskViewModel.name}></input>
</div>
</label>
<label class="tile flex-no">
<span class="icon">category</span>
<div>
<span>{translations.chatPage.task.taskBoardLabel}</span>
<select
bind:value={taskViewModel.boardId}
children:append={[
taskViewModel.chatViewModel.taskBoardSuggestions,
BoardOptionConverter,
]}
></select>
<span class="icon">arrow_drop_down</span>
</div>
</label>
<label class="tile flex-no">
<span class="icon">description</span>
<div>
<span>{translations.chatPage.task.taskDescriptionLabel}</span>
<textarea
rows="10"
bind:value={taskViewModel.description}
></textarea>
</div>
</label>
<hr></hr>
<label class="tile flex-no">
<span class="icon">category</span>
<div>
<span>{translations.chatPage.task.taskCategoryLabel}</span>
<input
bind:value={taskViewModel.category}
list={categorySuggestionId}
></input>
</div>
</label>
<datalist
hidden
id={categorySuggestionId}
children:append={[
taskViewModel.coreViewModel.taskCategorySuggestions,
StringToOption,
]}
></datalist>
<label class="tile flex-no">
<span class="icon">clock_loader_40</span>
<div>
<span>{translations.chatPage.task.taskStatusLabel}</span>
<input
bind:value={taskViewModel.status}
list={statusSuggestionId}
></input>
</div>
</label>
<datalist
hidden
id={statusSuggestionId}
children:append={[
taskViewModel.coreViewModel.taskStatusSuggestions,
StringToOption,
]}
></datalist>
<label class="tile flex-no">
<span class="icon">priority_high</span>
<div>
<span>{translations.chatPage.task.taskPriorityLabel}</span>
<input type="number" bind:value={taskViewModel.priority}></input>
</div>
</label>
<hr></hr>
<label class="tile flex-no">
<span class="icon">calendar_month</span>
<div>
<span>{translations.chatPage.task.taskDateLabel}</span>
<input type="date" bind:value={taskViewModel.date}></input>
</div>
</label>
<label class="tile flex-no">
<span class="icon">schedule</span>
<div>
<span>{translations.chatPage.task.taskTimeLabel}</span>
<input type="time" bind:value={taskViewModel.time}></input>
</div>
</label>
<hr></hr>
<div class="width-input">
{DangerousActionButton(
translations.chatPage.task.deleteTaskButton,
"delete_forever",
taskViewModel.deleteTask
)}
</div>
</main>
<div class="flex-row width-100">
<button class="flex" on:click={taskViewModel.closeAndDiscard}>
{translations.general.closeButton}
</button>
<button class="flex primary" on:click={taskViewModel.closeAndSave}>
{translations.general.saveButton}
<span class="icon">save</span>
</button>
</div>
</div>
</div>
);
);
}
+100 -100
View File
@@ -1,29 +1,29 @@
/* show/hide */
#chat-page-wrapper {
display: none;
display: none;
}
body[showing-chat] #home-page {
display: none;
display: none;
}
body[showing-chat] #chat-page-wrapper {
display: flex;
display: flex;
position: absolute;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
position: absolute;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
background-color: var(--background-glass);
background-color: var(--background-glass);
z-index: 2;
z-index: 2;
}
/* layout */
#chat-page {
--ribbon-size: var(--control-height);
--ribbon-size: var(--control-height);
}
#chat-page > div,
@@ -35,179 +35,179 @@ body[showing-chat] #chat-page-wrapper {
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar,
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar > span,
#chat-page > div > #main > div > .pane-wrapper > .pane > .content {
display: flex;
display: flex;
}
#chat-page > div {
padding: 0px;
overflow: hidden;
padding: 0px;
overflow: hidden;
}
#chat-page > div > #main {
width: calc(100% - var(--ribbon-size) - 1px);
width: calc(100% - var(--ribbon-size) - 1px);
}
#chat-page #main > div,
#chat-page > div > #main > div > .pane-wrapper,
#chat-page > div > #main > div > .pane-wrapper > .pane,
#chat-page > div > #main > div > .pane-wrapper > .pane > .content {
width: 100%;
height: 100%;
flex: 1;
width: 100%;
height: 100%;
flex: 1;
}
#chat-page > div > #main > div > .pane-wrapper,
#chat-page > div > #main > div > .pane-wrapper > .pane {
flex-direction: column;
scroll-snap-align: center;
flex-direction: column;
scroll-snap-align: center;
min-width: 100%;
overflow: hidden;
min-width: 100%;
overflow: hidden;
}
#chat-page > div > #main > div > .pane-wrapper > .pane:not(.side),
#chat-page > div > #main > div > .pane-wrapper > .pane:not(.side) .content {
animation: slide-up var(--animation-duration);
animation: slide-up var(--animation-duration);
}
@media (min-width: 1024px) {
#chat-page > div > #main > div > .pane-wrapper {
min-width: unset;
}
#chat-page > div > #main > div > .pane-wrapper {
min-width: unset;
}
#chat-page > div > #main > div > .pane-wrapper.side {
width: 18rem;
min-width: 18rem;
max-width: 18rem;
}
#chat-page > div > #main > div > .pane-wrapper.side {
width: 18rem;
min-width: 18rem;
max-width: 18rem;
}
}
#chat-page > div > #main > div > .pane-wrapper > .pane > .content {
flex-direction: column;
overflow: auto;
padding: var(--content-padding);
flex-direction: column;
overflow: auto;
padding: var(--content-padding);
}
#chat-page #ribbon,
#chat-page #main,
#chat-page #main .content {
flex-direction: column;
flex-direction: column;
}
/* ribbon, toolbar */
#chat-page > div > #ribbon,
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar {
align-items: center;
align-items: center;
}
#chat-page > div > #ribbon {
width: calc(var(--ribbon-size) + 1px);
border-right: 1px solid var(--border);
padding-bottom: var(--safe-area-bottom);
width: calc(var(--ribbon-size) + 1px);
border-right: 1px solid var(--border);
padding-bottom: var(--safe-area-bottom);
}
#chat-page > div > #ribbon > span,
#chat-page
> div
> #main
> div
> .pane-wrapper
> .pane
> .toolbar
> span.ribbon {
gap: var(--gap-between-tiles);
padding: var(--gap-between-tiles);
> div
> #main
> div
> .pane-wrapper
> .pane
> .toolbar
> span.ribbon {
gap: var(--gap-between-tiles);
padding: var(--gap-between-tiles);
}
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar {
justify-content: start;
justify-content: space-between;
justify-content: start;
justify-content: space-between;
}
#chat-page > div > #ribbon > span {
height: 100%;
height: 100%;
flex: 1;
overflow-y: auto;
flex: 1;
overflow-y: auto;
flex-direction: column;
justify-content: end;
flex-direction: column;
justify-content: end;
}
#chat-page > div > #ribbon button,
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar button {
padding: 0px;
padding: 0px;
width: var(--control-height);
max-width: var(--control-height);
height: var(--control-height);
min-height: var(--control-height);
width: var(--control-height);
max-width: var(--control-height);
height: var(--control-height);
min-height: var(--control-height);
align-items: center;
justify-content: center;
align-items: center;
justify-content: center;
}
#chat-page > div > #ribbon button.ribbon-button,
#chat-page
> div
> #main
> div
> .pane-wrapper
> .pane
> .toolbar
button.ribbon-button {
min-width: calc(var(--control-height) - 2 * var(--gap-between-tiles));
width: calc(var(--control-height) - 2 * var(--gap-between-tiles));
max-width: calc(var(--control-height) - 2 * var(--gap-between-tiles));
> div
> #main
> div
> .pane-wrapper
> .pane
> .toolbar
button.ribbon-button {
min-width: calc(var(--control-height) - 2 * var(--gap-between-tiles));
width: calc(var(--control-height) - 2 * var(--gap-between-tiles));
max-width: calc(var(--control-height) - 2 * var(--gap-between-tiles));
min-height: unset;
height: calc(var(--control-height) - 2 * var(--gap-between-tiles));
min-height: unset;
height: calc(var(--control-height) - 2 * var(--gap-between-tiles));
color: var(--primary);
color: var(--primary);
}
#chat-page > div > #ribbon button > .icon,
#chat-page
> div
> #main
> div
> .pane-wrapper
> .pane
> .toolbar
button
> .icon {
font-size: 1.1rem;
> div
> #main
> div
> .pane-wrapper
> .pane
> .toolbar
button
> .icon {
font-size: 1.1rem;
}
#chat-page > div > #ribbon > span {
width: 100%;
width: 100%;
}
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar {
height: var(--ribbon-size);
height: var(--ribbon-size);
overflow-x: auto;
overflow-x: auto;
padding: 0px;
padding: 0px;
}
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar > span {
display: flex;
align-items: center;
display: flex;
align-items: center;
}
#chat-page > div > #main > div > .pane-wrapper > .pane > .toolbar > span.title {
padding-left: var(--content-padding);
padding-left: var(--content-padding);
}
#chat-page
> div
> #main
> div
> .pane-wrapper
> .pane
> .toolbar
:is(input, select, button) {
height: 100%;
flex: 1;
> div
> #main
> div
> .pane-wrapper
> .pane
> .toolbar
:is(input, select, button) {
height: 100%;
flex: 1;
}
+66 -66
View File
@@ -12,73 +12,73 @@ import { TaskPage } from "./ChatPages/taskPage";
import { translations } from "./translations";
export function ChatPage(chatViewModel: ChatViewModel) {
const mainContent = React.createProxyState(
[chatViewModel.selectedPage],
() => {
chatViewModel.closeSubPages();
const mainContent = React.createProxyState(
[chatViewModel.selectedPage],
() => {
chatViewModel.closeSubPages();
switch (chatViewModel.selectedPage.value) {
case ChatPageType.Settings: {
return SettingsPage(chatViewModel.settingsPageViewModel);
}
case ChatPageType.Tasks: {
return TaskPage(chatViewModel.taskPageViewModel);
}
case ChatPageType.Calendar: {
return CalendarPage(chatViewModel.calendarViewModel)
}
default: {
return MessagePage(chatViewModel.messagePageViewModel);
}
}
}
);
switch (chatViewModel.selectedPage.value) {
case ChatPageType.Settings: {
return SettingsPage(chatViewModel.settingsPageViewModel);
}
case ChatPageType.Tasks: {
return TaskPage(chatViewModel.taskPageViewModel);
}
case ChatPageType.Calendar: {
return CalendarPage(chatViewModel.calendarViewModel);
}
default: {
return MessagePage(chatViewModel.messagePageViewModel);
}
}
},
);
return (
<article
id="chat-page"
set:color={chatViewModel.displayedColor}
class="subtle-background"
>
<div>
<div id="ribbon">
<button
class="ghost"
aria-label={translations.chatPage.closeChatAudioLabe}
on:click={chatViewModel.close}
>
<span class="icon">close</span>
</button>
return (
<article
id="chat-page"
set:color={chatViewModel.displayedColor}
class="subtle-background"
>
<div>
<div id="ribbon">
<button
class="ghost"
aria-label={translations.chatPage.closeChatAudioLabe}
on:click={chatViewModel.close}
>
<span class="icon">close</span>
</button>
<span>
{ChatViewToggleButton(
translations.chatPage.pages.calendar,
"calendar_month",
ChatPageType.Calendar,
chatViewModel
)}
{ChatViewToggleButton(
translations.chatPage.pages.tasks,
"task_alt",
ChatPageType.Tasks,
chatViewModel
)}
{ChatViewToggleButton(
translations.chatPage.pages.messages,
"forum",
ChatPageType.Messages,
chatViewModel
)}
{ChatViewToggleButton(
translations.chatPage.pages.settings,
"settings",
ChatPageType.Settings,
chatViewModel
)}
</span>
</div>
<div id="main" children:set={mainContent}></div>
</div>
</article>
);
<span>
{ChatViewToggleButton(
translations.chatPage.pages.calendar,
"calendar_month",
ChatPageType.Calendar,
chatViewModel,
)}
{ChatViewToggleButton(
translations.chatPage.pages.tasks,
"task_alt",
ChatPageType.Tasks,
chatViewModel,
)}
{ChatViewToggleButton(
translations.chatPage.pages.messages,
"forum",
ChatPageType.Messages,
chatViewModel,
)}
{ChatViewToggleButton(
translations.chatPage.pages.settings,
"settings",
ChatPageType.Settings,
chatViewModel,
)}
</span>
</div>
<div id="main" children:set={mainContent}></div>
</div>
</article>
);
}
+11 -16
View File
@@ -6,21 +6,16 @@ import ChatListViewModel from "../ViewModel/Chat/chatListViewModel";
import { ChatPage } from "./chatPage";
export function ChatPageWrapper(chatListViewModel: ChatListViewModel) {
const chatPageContent = React.createProxyState(
[chatListViewModel.selectedChat],
() => {
if (chatListViewModel.selectedChat.value == undefined) {
return <div></div>;
} else {
return ChatPage(chatListViewModel.selectedChat.value);
}
}
);
const chatPageContent = React.createProxyState(
[chatListViewModel.selectedChat],
() => {
if (chatListViewModel.selectedChat.value == undefined) {
return <div></div>;
} else {
return ChatPage(chatListViewModel.selectedChat.value);
}
},
);
return (
<div
id="chat-page-wrapper"
children:set={chatPageContent}
></div>
);
return <div id="chat-page-wrapper" children:set={chatPageContent}></div>;
}
+52 -52
View File
@@ -1,88 +1,88 @@
#home-page > div {
display: flex;
flex-direction: row;
display: flex;
flex-direction: row;
gap: var(--large-gap);
gap: var(--large-gap);
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
overflow: hidden;
overflow-x: scroll;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
overflow: hidden;
overflow-x: scroll;
padding: 0px;
padding: 0px;
}
#home-page > div > div {
display: flex;
flex-direction: column;
justify-content: start;
display: flex;
flex-direction: column;
justify-content: start;
gap: var(--gap-between-tiles);
padding: var(--content-padding);
padding-top: calc(var(--header-height) + var(--content-margin));
gap: var(--gap-between-tiles);
padding: var(--content-padding);
padding-top: calc(var(--header-height) + var(--content-margin));
width: 100%;
min-width: 100%;
height: 100%;
overflow-y: auto;
width: 100%;
min-width: 100%;
height: 100%;
overflow-y: auto;
scroll-snap-align: center;
scroll-snap-align: center;
}
#home-page #overview-section,
#home-page #chat-section {
padding-bottom: calc(
var(--content-padding) + var(--header-height) + var(--safe-area-bottom)
);
padding-bottom: calc(
var(--content-padding) + var(--header-height) + var(--safe-area-bottom)
);
}
#home-page #chat-grid {
margin-top: var(--content-padding);
margin-top: var(--content-padding);
display: grid;
gap: var(--gap-between-tiles);
display: grid;
gap: var(--gap-between-tiles);
grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
}
.chat-entry[unread] {
border: var(--outline-width) solid black;
border: var(--outline-width) solid black;
}
.chat-entry[unread]:not(:hover):not(:active) {
background: rgb(255, 217, 0) !important;
color: black;
background: rgb(255, 217, 0) !important;
color: black;
}
@media (min-width: 640px) {
#home-page > div {
overflow-x: hidden;
}
#home-page > div {
overflow-x: hidden;
}
#home-page #overview-section {
width: 20rem;
min-width: 20rem;
}
#home-page #overview-section {
width: 20rem;
min-width: 20rem;
}
#home-page #chat-section {
flex: 1;
min-width: unset;
}
#home-page #chat-section {
flex: 1;
min-width: unset;
}
}
@media (min-width: 1200px) {
#home-page #overview-section,
#home-page #chat-section {
padding-left: 3rem;
padding-right: 3rem;
}
#home-page #overview-section,
#home-page #chat-section {
padding-left: 3rem;
padding-right: 3rem;
}
#home-page #chat-grid {
margin-top: 3rem;
}
#home-page #chat-grid {
margin-top: 3rem;
}
#home-page #overview-section {
width: var(--max-input-width);
min-width: var(--max-input-width);
}
#home-page #overview-section {
width: var(--max-input-width);
min-width: var(--max-input-width);
}
}
+183 -171
View File
@@ -13,184 +13,196 @@ import StorageViewModel from "../ViewModel/Global/storageViewModel";
import { translations } from "./translations";
export function HomePage(
storageViewModel: StorageViewModel,
settingsViewModel: SettingsViewModel,
connectionViewModel: ConnectionViewModel,
fileTransferViewModel: FileTransferViewModel,
chatListViewModel: ChatListViewModel
storageViewModel: StorageViewModel,
settingsViewModel: SettingsViewModel,
connectionViewModel: ConnectionViewModel,
fileTransferViewModel: FileTransferViewModel,
chatListViewModel: ChatListViewModel,
) {
// sections
const overviewSection = (
<div id="overview-section">
<h2>{translations.homePage.overviewHeadline}</h2>
// sections
const overviewSection = (
<div id="overview-section">
<h2>{translations.homePage.overviewHeadline}</h2>
<label class="tile flex-no">
<span class="icon">cell_tower</span>
<div>
<span>{translations.homePage.serverAddress}</span>
<input
list="previous-connection-list"
placeholder={translations.homePage.serverAddressPlaceholder}
bind:value={connectionViewModel.serverAddressInput}
on:enter={connectionViewModel.connect}
></input>
<datalist
hidden
id="previous-connection-list"
children:append={[
connectionViewModel.previousAddresses,
StringToOption,
]}
></datalist>
<label class="tile flex-no">
<span class="icon">cell_tower</span>
<div>
<span>{translations.homePage.serverAddress}</span>
<input
list="previous-connection-list"
placeholder={
translations.homePage.serverAddressPlaceholder
}
bind:value={connectionViewModel.serverAddressInput}
on:enter={connectionViewModel.connect}
></input>
<datalist
hidden
id="previous-connection-list"
children:append={[
connectionViewModel.previousAddresses,
StringToOption,
]}
></datalist>
</div>
</label>
<div class="flex-row">
<button
class="danger flex justify-center"
aria-label={translations.homePage.disconnectAudioLabel}
on:click={connectionViewModel.disconnect}
toggle:disabled={connectionViewModel.cannotDisonnect}
>
<span class="icon">link_off</span>
</button>
<button
class="flex justify-center"
aria-label={
translations.homePage.manageConnectionsAudioLabel
}
on:click={connectionViewModel.showConnectionModal}
toggle:disabled={
connectionViewModel.hasNoPreviousConnections
}
>
<span class="icon">history</span>
</button>
<button
class="primary flex justify-center"
aria-label={translations.homePage.connectAudioLabel}
on:click={connectionViewModel.connect}
toggle:disabled={connectionViewModel.cannotConnect}
>
<span class="icon">link</span>
</button>
</div>
<hr></hr>
<label class="tile flex-no">
<span class="icon">account_circle</span>
<div>
<span>{translations.homePage.yourNameLabel}</span>
<input
placeholder={translations.homePage.yourNamePlaceholder}
bind:value={settingsViewModel.usernameInput}
on:enter={settingsViewModel.setName}
></input>
</div>
</label>
<div class="flex-row justify-end">
<button
class="width-50"
on:click={settingsViewModel.setName}
toggle:disabled={settingsViewModel.cannotSetName}
aria-label={translations.homePage.setNameButtonAudioLabel}
>
{translations.general.setButton}
<span class="icon">check</span>
</button>
</div>
<hr></hr>
<label class="tile flex-no">
<span class="icon">calendar_month</span>
<div>
<span>{translations.homePage.firstDayOfWeekLabel}</span>
<select bind:value={settingsViewModel.firstDayOfWeekInput}>
{...translations.regional.weekdays.full.map(
(weekdayName, i) =>
Option(
weekdayName,
i.toString(),
i.toString() ==
settingsViewModel.firstDayOfWeekInput
.value,
),
)}
</select>
<span class="icon">arrow_drop_down</span>
</div>
</label>
<hr></hr>
<button
class="tile flex-no"
on:click={fileTransferViewModel.showDirectionSelectionModal}
>
<span class="icon">sync_alt</span>
<div>
<span>{translations.homePage.transferDataButton}</span>
</div>
</button>
<button
class="tile flex-no"
on:click={storageViewModel.showStorageModal}
>
<span class="icon">hard_drive_2</span>
<div>
<span>{translations.homePage.manageStorageButton}</span>
</div>
</button>
<div class="mobile-only">
<hr></hr>
<div class="flex-row justify-end">
<button class="ghost width-50" on:click={scrollToChat}>
{translations.homePage.scrollToChatButton}
<span class="icon">arrow_forward</span>
</button>
</div>
</div>
</div>
</label>
);
<div class="flex-row">
<button
class="danger flex justify-center"
aria-label={translations.homePage.disconnectAudioLabel}
on:click={connectionViewModel.disconnect}
toggle:disabled={connectionViewModel.cannotDisonnect}
>
<span class="icon">link_off</span>
</button>
<button
class="flex justify-center"
aria-label={translations.homePage.manageConnectionsAudioLabel}
on:click={connectionViewModel.showConnectionModal}
toggle:disabled={connectionViewModel.hasNoPreviousConnections}
>
<span class="icon">history</span>
</button>
<button
class="primary flex justify-center"
aria-label={translations.homePage.connectAudioLabel}
on:click={connectionViewModel.connect}
toggle:disabled={connectionViewModel.cannotConnect}
>
<span class="icon">link</span>
</button>
</div>
const chatSection = (
<div id="chat-section">
<h2>{translations.homePage.chatsHeadline}</h2>
<hr></hr>
<div class="flex-row width-input">
<input
placeholder={translations.homePage.addChatPlaceholder}
aria-label={translations.homePage.addChatAudioLabel}
bind:value={chatListViewModel.newChatPrimaryChannel}
on:enter={chatListViewModel.createChat}
></input>
<button
class="primary"
aria-label={translations.homePage.addChatButton}
on:click={chatListViewModel.createChat}
toggle:disabled={chatListViewModel.cannotCreateChat}
>
<span class="icon">add</span>
</button>
</div>
<label class="tile flex-no">
<span class="icon">account_circle</span>
<div>
<span>{translations.homePage.yourNameLabel}</span>
<input
placeholder={translations.homePage.yourNamePlaceholder}
bind:value={settingsViewModel.usernameInput}
on:enter={settingsViewModel.setName}
></input>
<div
id="chat-grid"
children:append={[
chatListViewModel.chatViewModels,
ChatViewModelToChatEntry,
]}
></div>
</div>
</label>
<div class="flex-row justify-end">
<button
class="width-50"
on:click={settingsViewModel.setName}
toggle:disabled={settingsViewModel.cannotSetName}
aria-label={translations.homePage.setNameButtonAudioLabel}
>
{translations.general.setButton}
<span class="icon">check</span>
</button>
</div>
);
<hr></hr>
// methods
function scrollToChat() {
chatSection.scrollIntoView();
}
<label class="tile flex-no">
<span class="icon">calendar_month</span>
<div>
<span>{translations.homePage.firstDayOfWeekLabel}</span>
<select bind:value={settingsViewModel.firstDayOfWeekInput}>
{...translations.regional.weekdays.full.map((weekdayName, i) =>
Option(
weekdayName,
i.toString(),
i.toString() == settingsViewModel.firstDayOfWeekInput.value
)
)}
</select>
<span class="icon">arrow_drop_down</span>
</div>
</label>
<hr></hr>
<button
class="tile flex-no"
on:click={fileTransferViewModel.showDirectionSelectionModal}
>
<span class="icon">sync_alt</span>
<div>
<span>{translations.homePage.transferDataButton}</span>
</div>
</button>
<button class="tile flex-no" on:click={storageViewModel.showStorageModal}>
<span class="icon">hard_drive_2</span>
<div>
<span>{translations.homePage.manageStorageButton}</span>
</div>
</button>
<div class="mobile-only">
<hr></hr>
<div class="flex-row justify-end">
<button class="ghost width-50" on:click={scrollToChat}>
{translations.homePage.scrollToChatButton}
<span class="icon">arrow_forward</span>
</button>
</div>
</div>
</div>
);
const chatSection = (
<div id="chat-section">
<h2>{translations.homePage.chatsHeadline}</h2>
<div class="flex-row width-input">
<input
placeholder={translations.homePage.addChatPlaceholder}
aria-label={translations.homePage.addChatAudioLabel}
bind:value={chatListViewModel.newChatPrimaryChannel}
on:enter={chatListViewModel.createChat}
></input>
<button
class="primary"
aria-label={translations.homePage.addChatButton}
on:click={chatListViewModel.createChat}
toggle:disabled={chatListViewModel.cannotCreateChat}
>
<span class="icon">add</span>
</button>
</div>
<div
id="chat-grid"
children:append={[
chatListViewModel.chatViewModels,
ChatViewModelToChatEntry,
]}
></div>
</div>
);
// methods
function scrollToChat() {
chatSection.scrollIntoView();
}
// final
return (
<article id="home-page">
<div>
{overviewSection}
{chatSection}
</div>
</article>
);
// final
return (
<article id="home-page">
<div>
{overviewSection}
{chatSection}
</div>
</article>
);
}
+8 -8
View File
@@ -3,12 +3,12 @@ import * as React from "bloatless-react";
import { translations } from "./translations";
export function MainPage() {
return (
<article id="home-page">
<header>
<span>{translations.homePage.appName}</span>
</header>
<div></div>
</article>
);
return (
<article id="home-page">
<header>
<span>{translations.homePage.appName}</span>
</header>
<div></div>
</article>
);
}
+722 -708
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -1,11 +1,11 @@
export function allowDrop(event: DragEvent) {
event.preventDefault();
event.preventDefault();
}
export function allowDrag(event: DragEvent) {
event.dataTransfer?.setData("text", "")
event.dataTransfer?.setData("text", "");
}
export function reload() {
window.location.reload();
}
window.location.reload();
}
+88 -87
View File
@@ -9,98 +9,99 @@ import SettingsViewModel from "../Global/settingsViewModel";
import StorageModel from "../../Model/Global/storageModel";
export default class ChatListViewModel {
storageModel: StorageModel;
chatListModel: ChatListModel;
settingsViewModel: SettingsViewModel;
storageModel: StorageModel;
chatListModel: ChatListModel;
settingsViewModel: SettingsViewModel;
// data
chatIndexManager: IndexManager<ChatViewModel> = new IndexManager(
(chatViewModel: ChatViewModel) =>
chatViewModel.settingsPageViewModel.primaryChannel.value
);
// state
newChatPrimaryChannel: React.State<string> = new React.State("");
chatViewModels: React.ListState<ChatViewModel> = new React.ListState();
selectedChat: React.State<ChatViewModel | undefined> = new React.State<any>(
undefined
);
// guards
cannotCreateChat = React.createProxyState(
[this.newChatPrimaryChannel],
() => this.newChatPrimaryChannel.value == ""
);
// methods
createChat = (): void => {
const chatModel: ChatModel = this.chatListModel.createChat(
this.newChatPrimaryChannel.value
// data
chatIndexManager: IndexManager<ChatViewModel> = new IndexManager(
(chatViewModel: ChatViewModel) =>
chatViewModel.settingsPageViewModel.primaryChannel.value,
);
this.newChatPrimaryChannel.value = "";
const chatViewModel: ChatViewModel = this.createChatViewModel(chatModel);
this.trackChat(chatViewModel);
this.updateIndices();
};
// state
newChatPrimaryChannel: React.State<string> = new React.State("");
chatViewModels: React.ListState<ChatViewModel> = new React.ListState();
trackChat = (chatViewModel: ChatViewModel): void => {
this.chatViewModels.add(chatViewModel);
};
untrackChat = (chatViewModel: ChatViewModel): void => {
this.chatListModel.untrackChat(chatViewModel.chatModel);
this.chatViewModels.remove(chatViewModel);
};
createChatViewModel = (chatModel: ChatModel): ChatViewModel => {
return new ChatViewModel(
this.coreViewModel,
this.storageModel,
chatModel,
this.settingsViewModel,
this
selectedChat: React.State<ChatViewModel | undefined> = new React.State<any>(
undefined,
);
};
updateIndices = (): void => {
this.chatIndexManager.update([...this.chatViewModels.value.values()]);
for (const chatViewModel of this.chatViewModels.value) {
chatViewModel.updateIndex();
// guards
cannotCreateChat = React.createProxyState(
[this.newChatPrimaryChannel],
() => this.newChatPrimaryChannel.value == "",
);
// methods
createChat = (): void => {
const chatModel: ChatModel = this.chatListModel.createChat(
this.newChatPrimaryChannel.value,
);
this.newChatPrimaryChannel.value = "";
const chatViewModel: ChatViewModel =
this.createChatViewModel(chatModel);
this.trackChat(chatViewModel);
this.updateIndices();
};
trackChat = (chatViewModel: ChatViewModel): void => {
this.chatViewModels.add(chatViewModel);
};
untrackChat = (chatViewModel: ChatViewModel): void => {
this.chatListModel.untrackChat(chatViewModel.chatModel);
this.chatViewModels.remove(chatViewModel);
};
createChatViewModel = (chatModel: ChatModel): ChatViewModel => {
return new ChatViewModel(
this.coreViewModel,
this.storageModel,
chatModel,
this.settingsViewModel,
this,
);
};
updateIndices = (): void => {
this.chatIndexManager.update([...this.chatViewModels.value.values()]);
for (const chatViewModel of this.chatViewModels.value) {
chatViewModel.updateIndex();
}
};
// view
openChat = (chatViewModel: ChatViewModel): void => {
this.selectedChat.value = chatViewModel;
};
closeChat = (): void => {
this.selectedChat.value = undefined;
};
// load
loadChats = (): void => {
this.chatViewModels.clear();
for (const chatModel of this.chatListModel.chatModels.values()) {
const chatViewModel = this.createChatViewModel(chatModel);
this.trackChat(chatViewModel);
}
this.updateIndices();
};
// init
constructor(
public coreViewModel: CoreViewModel,
storageModel: StorageModel,
chatListModel: ChatListModel,
settingsViewModel: SettingsViewModel,
) {
this.storageModel = storageModel;
this.chatListModel = chatListModel;
this.settingsViewModel = settingsViewModel;
this.loadChats();
}
};
// view
openChat = (chatViewModel: ChatViewModel): void => {
this.selectedChat.value = chatViewModel;
};
closeChat = (): void => {
this.selectedChat.value = undefined;
};
// load
loadChats = (): void => {
this.chatViewModels.clear();
for (const chatModel of this.chatListModel.chatModels.values()) {
const chatViewModel = this.createChatViewModel(chatModel);
this.trackChat(chatViewModel);
}
this.updateIndices();
};
// init
constructor(
public coreViewModel: CoreViewModel,
storageModel: StorageModel,
chatListModel: ChatListModel,
settingsViewModel: SettingsViewModel
) {
this.storageModel = storageModel;
this.chatListModel = chatListModel;
this.settingsViewModel = settingsViewModel;
this.loadChats();
}
}
+50 -50
View File
@@ -6,62 +6,62 @@ import CoreViewModel from "../Global/coreViewModel";
import MessagePageViewModel from "../Pages/messagePageViewModel";
export default class ChatMessageViewModel {
messagePageViewModel: MessagePageViewModel;
messagePageViewModel: MessagePageViewModel;
// data
chatMessage: ChatMessage;
channel: string;
sender: string;
dateSent: string;
body: React.State<string> = new React.State("");
status: React.State<ChatMessageStatus | any> = new React.State<any>(
undefined
);
sentByUser: boolean;
// data
chatMessage: ChatMessage;
channel: string;
sender: string;
dateSent: string;
body: React.State<string> = new React.State("");
status: React.State<ChatMessageStatus | any> = new React.State<any>(
undefined,
);
sentByUser: boolean;
// state
isPresentingInfoModal: React.State<boolean> = new React.State(false);
// state
isPresentingInfoModal: React.State<boolean> = new React.State(false);
// methods
copyMessage = (): void => {
navigator.clipboard.writeText(this.body.value);
};
resendMessage = (): void => {
this.messagePageViewModel.sendMessageFromBody(this.body.value);
};
decryptMessage = (): void => {
this.messagePageViewModel.decryptMessage(this);
};
// methods
copyMessage = (): void => {
navigator.clipboard.writeText(this.body.value);
};
resendMessage = (): void => {
this.messagePageViewModel.sendMessageFromBody(this.body.value);
};
decryptMessage = (): void => {
this.messagePageViewModel.decryptMessage(this);
};
// view
showInfoModal = (): void => {
this.isPresentingInfoModal.value = true;
};
// view
showInfoModal = (): void => {
this.isPresentingInfoModal.value = true;
};
hideInfoModal = (): void => {
this.isPresentingInfoModal.value = false;
};
hideInfoModal = (): void => {
this.isPresentingInfoModal.value = false;
};
// load
loadData = (): void => {
this.channel = this.chatMessage.channel;
this.sender = this.chatMessage.sender;
this.dateSent = new Date(this.chatMessage.dateSent).toLocaleString();
this.body.value = this.chatMessage.body;
this.status.value = this.chatMessage.status;
};
// load
loadData = (): void => {
this.channel = this.chatMessage.channel;
this.sender = this.chatMessage.sender;
this.dateSent = new Date(this.chatMessage.dateSent).toLocaleString();
this.body.value = this.chatMessage.body;
this.status.value = this.chatMessage.status;
};
// init
constructor(
public coreViewModel: CoreViewModel,
messagePageViewModel: MessagePageViewModel,
chatMessage: ChatMessage,
sentByUser: boolean
) {
this.messagePageViewModel = messagePageViewModel;
// init
constructor(
public coreViewModel: CoreViewModel,
messagePageViewModel: MessagePageViewModel,
chatMessage: ChatMessage,
sentByUser: boolean,
) {
this.messagePageViewModel = messagePageViewModel;
this.chatMessage = chatMessage;
this.sentByUser = sentByUser;
this.loadData();
}
this.chatMessage = chatMessage;
this.sentByUser = sentByUser;
this.loadData();
}
}
+133 -133
View File
@@ -1,12 +1,12 @@
import * as React from "bloatless-react";
import CalendarPageViewModel, {
CALENDAR_EVENT_BOARD_ID,
CALENDAR_EVENT_BOARD_ID,
} from "../Pages/calendarPageViewModel";
import ChatModel, { ChatMessage } from "../../Model/Chat/chatModel";
import StorageModel, {
StorageModelSubPath,
filePaths,
StorageModelSubPath,
filePaths,
} from "../../Model/Global/storageModel";
import ChatListViewModel from "./chatListViewModel";
@@ -20,145 +20,145 @@ import TaskPageViewModel from "../Pages/taskPageViewModel";
import { translations } from "../../View/translations";
export default class ChatViewModel {
chatModel: ChatModel;
storageModel: StorageModel;
settingsViewModel: SettingsViewModel;
chatListViewModel: ChatListViewModel;
chatModel: ChatModel;
storageModel: StorageModel;
settingsViewModel: SettingsViewModel;
chatListViewModel: ChatListViewModel;
calendarViewModel: CalendarPageViewModel;
taskPageViewModel: TaskPageViewModel;
messagePageViewModel: MessagePageViewModel;
settingsPageViewModel: SettingsPageViewModel;
calendarViewModel: CalendarPageViewModel;
taskPageViewModel: TaskPageViewModel;
messagePageViewModel: MessagePageViewModel;
settingsPageViewModel: SettingsPageViewModel;
// state
displayedColor: React.State<Color> = new React.State<any>(Color.Standard);
selectedPage: React.State<ChatPageType> = new React.State<any>(
ChatPageType.Messages
);
index: React.State<number> = new React.State(0);
hasUnreadMessages: React.State<boolean> = new React.State(false);
taskBoardSuggestions: React.MapState<Entry> = new React.MapState();
// view
open = (): void => {
this.chatListViewModel.openChat(this);
this.markRead();
};
close = (): void => {
this.chatListViewModel.closeChat();
};
closeSubPages = (): void => {};
setColor = (color: Color): void => {
this.setDisplayedColor(color);
this.chatModel.setColor(color);
};
setDisplayedColor = (color: Color): void => {
this.displayedColor.value = color;
};
resetColor = (): void => {
this.displayedColor.value = this.settingsPageViewModel.color.value;
};
updateIndex = (): void => {
const index: number =
this.chatListViewModel.chatIndexManager.getIndex(this);
this.index.value = index;
};
markRead = (): void => {
this.hasUnreadMessages.value = false;
this.chatModel.markRead();
};
// load
loadPageSelection = (): void => {
const path: string[] = StorageModel.getPath(
StorageModelSubPath.Chat,
filePaths.chat.lastUsedPage(this.chatModel.id)
);
const lastUsedPage: string | null = this.storageModel.read(path);
if (lastUsedPage != null) {
this.selectedPage.value = lastUsedPage as any;
}
this.selectedPage.subscribeSilent((newPage) => {
this.storageModel.write(path, newPage);
this.resetColor();
});
};
loadInfo = (): void => {
this.hasUnreadMessages.value = this.chatModel.info.hasUnreadMessages;
this.taskBoardSuggestions.set(CALENDAR_EVENT_BOARD_ID, [
CALENDAR_EVENT_BOARD_ID,
translations.chatPage.calendar.eventsBoard,
]);
};
// init
constructor(
public coreViewModel: CoreViewModel,
storageModel: StorageModel,
chatModel: ChatModel,
settingsViewModel: SettingsViewModel,
chatListViewModel: ChatListViewModel
) {
// models
this.storageModel = storageModel;
this.chatModel = chatModel;
this.settingsViewModel = settingsViewModel;
this.chatListViewModel = chatListViewModel;
// page viewModels
this.calendarViewModel = new CalendarPageViewModel(
coreViewModel,
this,
this.storageModel,
this.chatModel.fileModel.boardsAndTasksModel.calendarModel,
this.chatModel.fileModel.boardsAndTasksModel
);
this.taskPageViewModel = new TaskPageViewModel(
this.coreViewModel,
this,
this.storageModel,
this.chatModel.fileModel.boardsAndTasksModel
);
this.messagePageViewModel = new MessagePageViewModel(
this.coreViewModel,
this
);
this.settingsPageViewModel = new SettingsPageViewModel(
this.coreViewModel,
this
// state
displayedColor: React.State<Color> = new React.State<any>(Color.Standard);
selectedPage: React.State<ChatPageType> = new React.State<any>(
ChatPageType.Messages,
);
// handlers
chatModel.chatMessageHandlerManager.addHandler(
(chatMessage: ChatMessage) => {
this.messagePageViewModel.showChatMessage(chatMessage);
index: React.State<number> = new React.State(0);
hasUnreadMessages: React.State<boolean> = new React.State(false);
taskBoardSuggestions: React.MapState<Entry> = new React.MapState();
// view
open = (): void => {
this.chatListViewModel.openChat(this);
this.markRead();
}
);
};
close = (): void => {
this.chatListViewModel.closeChat();
};
closeSubPages = (): void => {};
setColor = (color: Color): void => {
this.setDisplayedColor(color);
this.chatModel.setColor(color);
};
setDisplayedColor = (color: Color): void => {
this.displayedColor.value = color;
};
resetColor = (): void => {
this.displayedColor.value = this.settingsPageViewModel.color.value;
};
updateIndex = (): void => {
const index: number =
this.chatListViewModel.chatIndexManager.getIndex(this);
this.index.value = index;
};
markRead = (): void => {
this.hasUnreadMessages.value = false;
this.chatModel.markRead();
};
// load
this.loadPageSelection();
this.resetColor();
this.loadInfo();
}
loadPageSelection = (): void => {
const path: string[] = StorageModel.getPath(
StorageModelSubPath.Chat,
filePaths.chat.lastUsedPage(this.chatModel.id),
);
const lastUsedPage: string | null = this.storageModel.read(path);
if (lastUsedPage != null) {
this.selectedPage.value = lastUsedPage as any;
}
this.selectedPage.subscribeSilent((newPage) => {
this.storageModel.write(path, newPage);
this.resetColor();
});
};
loadInfo = (): void => {
this.hasUnreadMessages.value = this.chatModel.info.hasUnreadMessages;
this.taskBoardSuggestions.set(CALENDAR_EVENT_BOARD_ID, [
CALENDAR_EVENT_BOARD_ID,
translations.chatPage.calendar.eventsBoard,
]);
};
// init
constructor(
public coreViewModel: CoreViewModel,
storageModel: StorageModel,
chatModel: ChatModel,
settingsViewModel: SettingsViewModel,
chatListViewModel: ChatListViewModel,
) {
// models
this.storageModel = storageModel;
this.chatModel = chatModel;
this.settingsViewModel = settingsViewModel;
this.chatListViewModel = chatListViewModel;
// page viewModels
this.calendarViewModel = new CalendarPageViewModel(
coreViewModel,
this,
this.storageModel,
this.chatModel.fileModel.boardsAndTasksModel.calendarModel,
this.chatModel.fileModel.boardsAndTasksModel,
);
this.taskPageViewModel = new TaskPageViewModel(
this.coreViewModel,
this,
this.storageModel,
this.chatModel.fileModel.boardsAndTasksModel,
);
this.messagePageViewModel = new MessagePageViewModel(
this.coreViewModel,
this,
);
this.settingsPageViewModel = new SettingsPageViewModel(
this.coreViewModel,
this,
);
// handlers
chatModel.chatMessageHandlerManager.addHandler(
(chatMessage: ChatMessage) => {
this.messagePageViewModel.showChatMessage(chatMessage);
this.markRead();
},
);
// load
this.loadPageSelection();
this.resetColor();
this.loadInfo();
}
}
// types
export enum ChatPageType {
Settings = "settings",
Messages = "messages",
Tasks = "tasks",
Calendar = "calendar",
Settings = "settings",
Messages = "messages",
Tasks = "tasks",
Calendar = "calendar",
}
+81 -80
View File
@@ -4,89 +4,90 @@ import ConnectionModel from "../../Model/Global/connectionModel";
import CoreViewModel from "./coreViewModel";
export default class ConnectionViewModel {
connectionModel: ConnectionModel;
connectionModel: ConnectionModel;
// state
serverAddressInput: React.State<string> = new React.State("");
isConnected: React.State<boolean> = new React.State(false);
// state
serverAddressInput: React.State<string> = new React.State("");
isConnected: React.State<boolean> = new React.State(false);
isShowingConnectionModal: React.State<boolean> = new React.State(false);
isShowingConnectionModal: React.State<boolean> = new React.State(false);
previousAddresses: React.ListState<string> = new React.ListState();
previousAddresses: React.ListState<string> = new React.ListState();
// guards
cannotConnect: React.State<boolean> = React.createProxyState(
[this.serverAddressInput, this.isConnected],
() =>
(this.isConnected.value == true &&
this.serverAddressInput.value == this.connectionModel.address) ||
this.serverAddressInput.value == ""
);
cannotDisonnect: React.State<boolean> = React.createProxyState(
[this.isConnected],
() => this.isConnected.value == false
);
hasNoPreviousConnections: React.State<boolean> = React.createProxyState(
[this.previousAddresses],
() => this.previousAddresses.value.size == 0
);
// handlers
connectionChangeHandler = (): void => {
this.isConnected.value = this.connectionModel.isConnected;
if (this.connectionModel.isConnected == false) return;
if (this.connectionModel.address == undefined) return;
this.serverAddressInput.value = this.connectionModel.address;
if (!this.previousAddresses.value.has(this.connectionModel.address)) {
this.previousAddresses.add(this.connectionModel.address);
}
};
// methods
connect = (): void => {
this.connectToAddress(this.serverAddressInput.value);
};
connectToAddress = (address: string): void => {
this.connectionModel.connect(address);
};
disconnect = (): void => {
this.connectionModel.disconnect();
};
removePreviousAddress = (address: string): void => {
this.connectionModel.removeAddress(address);
this.updatePreviousAddresses();
};
// view
showConnectionModal = (): void => {
this.isShowingConnectionModal.value = true;
};
hideConnectionModal = (): void => {
this.isShowingConnectionModal.value = false;
};
updatePreviousAddresses = (): void => {
this.previousAddresses.clear();
this.previousAddresses.add(...this.connectionModel.addresses);
};
// init
constructor(
public coreViewModel: CoreViewModel,
connectionModel: ConnectionModel
) {
this.connectionModel = connectionModel;
this.updatePreviousAddresses();
connectionModel.connectionChangeHandlerManager.addHandler(
this.connectionChangeHandler
// guards
cannotConnect: React.State<boolean> = React.createProxyState(
[this.serverAddressInput, this.isConnected],
() =>
(this.isConnected.value == true &&
this.serverAddressInput.value ==
this.connectionModel.address) ||
this.serverAddressInput.value == "",
);
}
cannotDisonnect: React.State<boolean> = React.createProxyState(
[this.isConnected],
() => this.isConnected.value == false,
);
hasNoPreviousConnections: React.State<boolean> = React.createProxyState(
[this.previousAddresses],
() => this.previousAddresses.value.size == 0,
);
// handlers
connectionChangeHandler = (): void => {
this.isConnected.value = this.connectionModel.isConnected;
if (this.connectionModel.isConnected == false) return;
if (this.connectionModel.address == undefined) return;
this.serverAddressInput.value = this.connectionModel.address;
if (!this.previousAddresses.value.has(this.connectionModel.address)) {
this.previousAddresses.add(this.connectionModel.address);
}
};
// methods
connect = (): void => {
this.connectToAddress(this.serverAddressInput.value);
};
connectToAddress = (address: string): void => {
this.connectionModel.connect(address);
};
disconnect = (): void => {
this.connectionModel.disconnect();
};
removePreviousAddress = (address: string): void => {
this.connectionModel.removeAddress(address);
this.updatePreviousAddresses();
};
// view
showConnectionModal = (): void => {
this.isShowingConnectionModal.value = true;
};
hideConnectionModal = (): void => {
this.isShowingConnectionModal.value = false;
};
updatePreviousAddresses = (): void => {
this.previousAddresses.clear();
this.previousAddresses.add(...this.connectionModel.addresses);
};
// init
constructor(
public coreViewModel: CoreViewModel,
connectionModel: ConnectionModel,
) {
this.connectionModel = connectionModel;
this.updatePreviousAddresses();
connectionModel.connectionChangeHandlerManager.addHandler(
this.connectionChangeHandler,
);
}
}
+8 -8
View File
@@ -1,13 +1,13 @@
import * as React from "bloatless-react";
export default class CoreViewModel {
// DRAG & DROP
draggedObject: React.State<any> = new React.State<any>(undefined);
// DRAG & DROP
draggedObject: React.State<any> = new React.State<any>(undefined);
// SUGGESTIONS
// boards & tasks
boardSearchSuggestions: React.ListState<string> = new React.ListState();
taskCategorySuggestions: React.ListState<string> = new React.ListState();
taskStatusSuggestions: React.ListState<string> = new React.ListState();
// SUGGESTIONS
// boards & tasks
boardSearchSuggestions: React.ListState<string> = new React.ListState();
taskCategorySuggestions: React.ListState<string> = new React.ListState();
taskStatusSuggestions: React.ListState<string> = new React.ListState();
}
+163 -157
View File
@@ -1,11 +1,11 @@
import * as React from "bloatless-react";
import FileTransferModel, {
TransferData,
TransferData,
} from "../../Model/Global/fileTransferModel";
import StorageModel, {
StorageModelSubPath,
filePaths,
StorageModelSubPath,
filePaths,
} from "../../Model/Global/storageModel";
import ChatListModel from "../../Model/Chat/chatListModel";
@@ -13,180 +13,186 @@ import ChatModel from "../../Model/Chat/chatModel";
import { translations } from "../../View/translations";
export default class FileTransferViewModel {
fileTransferModel: FileTransferModel;
chatListModel: ChatListModel;
fileTransferModel: FileTransferModel;
chatListModel: ChatListModel;
// state
presentedModal: React.State<FileTransferModal | undefined> =
new React.State<any>(undefined);
// state
presentedModal: React.State<FileTransferModal | undefined> =
new React.State<any>(undefined);
generalFileOptions: React.ListState<FileTransferOption> =
new React.ListState();
chatFileOptions: React.ListState<FileTransferOption> = new React.ListState();
selectedPaths: React.ListState<string[]> = new React.ListState();
generalFileOptions: React.ListState<FileTransferOption> =
new React.ListState();
chatFileOptions: React.ListState<FileTransferOption> =
new React.ListState();
selectedPaths: React.ListState<string[]> = new React.ListState();
transferChannel: React.State<string> = new React.State("");
transferKey: React.State<string> = new React.State("");
transferChannel: React.State<string> = new React.State("");
transferKey: React.State<string> = new React.State("");
receivingTransferChannel: React.State<string> = new React.State("");
receivingTransferKey: React.State<string> = new React.State("");
receivingTransferChannel: React.State<string> = new React.State("");
receivingTransferKey: React.State<string> = new React.State("");
filePathsSent: React.ListState<string> = new React.ListState();
filesSentCount: React.State<number> = React.createProxyState(
[this.filePathsSent],
() => this.filePathsSent.value.size
);
filesSentText: React.State<string> = React.createProxyState(
[this.filesSentCount],
() =>
translations.dataTransferModal.filesSentCount(this.filesSentCount.value)
);
filePathsReceived: React.ListState<string> = new React.ListState();
filesReceivedCount: React.State<number> = React.createProxyState(
[this.filePathsReceived],
() => this.filePathsReceived.value.size
);
filesReceivedText: React.State<string> = React.createProxyState(
[this.filesReceivedCount],
() =>
translations.dataTransferModal.filesReceivedCount(
this.filesReceivedCount.value
)
);
// guards
hasNoPathsSelected: React.State<boolean> = React.createProxyState(
[this.selectedPaths],
() => this.selectedPaths.value.size == 0
);
didNotFinishSending: React.State<boolean> = new React.State(true);
cannotPrepareToReceive: React.State<boolean> = React.createProxyState(
[this.receivingTransferChannel, this.receivingTransferKey],
() =>
this.receivingTransferChannel.value == "" ||
this.receivingTransferKey.value == ""
);
// handlers
handleReceivedFile = (path: string): void => {
this.filePathsReceived.add(path);
};
// methods
getOptions = (): void => {
this.generalFileOptions.clear();
this.chatFileOptions.clear();
this.selectedPaths.clear();
this.generalFileOptions.add(
{
label: translations.dataTransferModal.connectionData,
path: StorageModel.getPath(
StorageModelSubPath.ConnectionModel,
filePaths.connectionModel.previousAddresses
),
},
{
label: translations.dataTransferModal.settingsData,
path: StorageModel.getPath(StorageModelSubPath.SettingsModel, []),
}
filePathsSent: React.ListState<string> = new React.ListState();
filesSentCount: React.State<number> = React.createProxyState(
[this.filePathsSent],
() => this.filePathsSent.value.size,
);
filesSentText: React.State<string> = React.createProxyState(
[this.filesSentCount],
() =>
translations.dataTransferModal.filesSentCount(
this.filesSentCount.value,
),
);
const chatModels: Set<ChatModel> = this.chatListModel.chatModels;
for (const chatModel of chatModels) {
this.chatFileOptions.add({
label: chatModel.info.primaryChannel,
path: chatModel.getBasePath(),
});
}
};
getTransferData = (): void => {
const transferData: TransferData =
this.fileTransferModel.generateTransferData();
this.transferChannel.value = transferData.channel;
this.transferKey.value = transferData.key;
};
// view
showDirectionSelectionModal = (): void => {
this.presentedModal.value = FileTransferModal.DirectionSelection;
this.getOptions();
};
showFileSelectionModal = (): void => {
this.presentedModal.value = FileTransferModal.FileSelection;
};
showTransferDataModal = (): void => {
this.presentedModal.value = FileTransferModal.TransferDataDisplay;
this.getTransferData();
};
initiateTransfer = (): void => {
this.presentedModal.value = FileTransferModal.TransferDisplay;
this.didNotFinishSending.value = true;
this.filePathsSent.clear();
this.fileTransferModel.sendFiles(
this.selectedPaths.value.values(),
(path: string) => {
console.log(path);
this.filePathsSent.add(path);
}
filePathsReceived: React.ListState<string> = new React.ListState();
filesReceivedCount: React.State<number> = React.createProxyState(
[this.filePathsReceived],
() => this.filePathsReceived.value.size,
);
filesReceivedText: React.State<string> = React.createProxyState(
[this.filesReceivedCount],
() =>
translations.dataTransferModal.filesReceivedCount(
this.filesReceivedCount.value,
),
);
this.didNotFinishSending.value = false;
};
// guards
hasNoPathsSelected: React.State<boolean> = React.createProxyState(
[this.selectedPaths],
() => this.selectedPaths.value.size == 0,
);
didNotFinishSending: React.State<boolean> = new React.State(true);
cannotPrepareToReceive: React.State<boolean> = React.createProxyState(
[this.receivingTransferChannel, this.receivingTransferKey],
() =>
this.receivingTransferChannel.value == "" ||
this.receivingTransferKey.value == "",
);
showTransferDataInputModal = (): void => {
this.presentedModal.value = FileTransferModal.TransferDataInput;
};
prepareReceivingData = (): void => {
this.presentedModal.value = FileTransferModal.ReceptionDisplay;
this.filePathsReceived.clear();
const transferData: TransferData = {
channel: this.receivingTransferChannel.value,
key: this.receivingTransferKey.value,
// handlers
handleReceivedFile = (path: string): void => {
this.filePathsReceived.add(path);
};
this.fileTransferModel.prepareToReceive(transferData);
};
hideModal = (): void => {
this.presentedModal.value = undefined;
};
// methods
getOptions = (): void => {
this.generalFileOptions.clear();
this.chatFileOptions.clear();
this.selectedPaths.clear();
// init
constructor(
fileTransferModel: FileTransferModel,
chatListModel: ChatListModel
) {
this.fileTransferModel = fileTransferModel;
this.chatListModel = chatListModel;
this.generalFileOptions.add(
{
label: translations.dataTransferModal.connectionData,
path: StorageModel.getPath(
StorageModelSubPath.ConnectionModel,
filePaths.connectionModel.previousAddresses,
),
},
{
label: translations.dataTransferModal.settingsData,
path: StorageModel.getPath(
StorageModelSubPath.SettingsModel,
[],
),
},
);
this.fileTransferModel.fileHandlerManager.addHandler(
this.handleReceivedFile
);
}
const chatModels: Set<ChatModel> = this.chatListModel.chatModels;
for (const chatModel of chatModels) {
this.chatFileOptions.add({
label: chatModel.info.primaryChannel,
path: chatModel.getBasePath(),
});
}
};
getTransferData = (): void => {
const transferData: TransferData =
this.fileTransferModel.generateTransferData();
this.transferChannel.value = transferData.channel;
this.transferKey.value = transferData.key;
};
// view
showDirectionSelectionModal = (): void => {
this.presentedModal.value = FileTransferModal.DirectionSelection;
this.getOptions();
};
showFileSelectionModal = (): void => {
this.presentedModal.value = FileTransferModal.FileSelection;
};
showTransferDataModal = (): void => {
this.presentedModal.value = FileTransferModal.TransferDataDisplay;
this.getTransferData();
};
initiateTransfer = (): void => {
this.presentedModal.value = FileTransferModal.TransferDisplay;
this.didNotFinishSending.value = true;
this.filePathsSent.clear();
this.fileTransferModel.sendFiles(
this.selectedPaths.value.values(),
(path: string) => {
console.log(path);
this.filePathsSent.add(path);
},
);
this.didNotFinishSending.value = false;
};
showTransferDataInputModal = (): void => {
this.presentedModal.value = FileTransferModal.TransferDataInput;
};
prepareReceivingData = (): void => {
this.presentedModal.value = FileTransferModal.ReceptionDisplay;
this.filePathsReceived.clear();
const transferData: TransferData = {
channel: this.receivingTransferChannel.value,
key: this.receivingTransferKey.value,
};
this.fileTransferModel.prepareToReceive(transferData);
};
hideModal = (): void => {
this.presentedModal.value = undefined;
};
// init
constructor(
fileTransferModel: FileTransferModel,
chatListModel: ChatListModel,
) {
this.fileTransferModel = fileTransferModel;
this.chatListModel = chatListModel;
this.fileTransferModel.fileHandlerManager.addHandler(
this.handleReceivedFile,
);
}
}
export interface FileTransferOption {
label: string;
path: string[];
label: string;
path: string[];
}
export enum FileTransferModal {
DirectionSelection,
DirectionSelection,
// sending
FileSelection,
TransferDataDisplay,
TransferDisplay,
// sending
FileSelection,
TransferDataDisplay,
TransferDisplay,
// receiving
TransferDataInput,
ReceptionDisplay,
// receiving
TransferDataInput,
ReceptionDisplay,
}
+33 -33
View File
@@ -4,44 +4,44 @@ import CoreViewModel from "./coreViewModel";
import SettingsModel from "../../Model/Global/settingsModel";
export default class SettingsViewModel {
settingsModel: SettingsModel;
settingsModel: SettingsModel;
// state
username: React.State<string> = new React.State("");
usernameInput: React.State<string> = new React.State("");
firstDayOfWeekInput: React.State<string> = new React.State("0");
// state
username: React.State<string> = new React.State("");
usernameInput: React.State<string> = new React.State("");
firstDayOfWeekInput: React.State<string> = new React.State("0");
// guards
cannotSetName: React.State<boolean> = React.createProxyState(
[this.usernameInput],
() =>
this.usernameInput.value == "" ||
this.usernameInput.value == this.settingsModel.username
);
// guards
cannotSetName: React.State<boolean> = React.createProxyState(
[this.usernameInput],
() =>
this.usernameInput.value == "" ||
this.usernameInput.value == this.settingsModel.username,
);
// methods
setName = (): void => {
this.settingsModel.setName(this.usernameInput.value);
this.username.value = this.settingsModel.username;
this.usernameInput.callSubscriptions();
};
// methods
setName = (): void => {
this.settingsModel.setName(this.usernameInput.value);
this.username.value = this.settingsModel.username;
this.usernameInput.callSubscriptions();
};
setFirstDayofWeek = (): void => {
this.settingsModel.setFirstDayOfWeek(this.firstDayOfWeekInput.value);
};
setFirstDayofWeek = (): void => {
this.settingsModel.setFirstDayOfWeek(this.firstDayOfWeekInput.value);
};
// init
constructor(
public coreViewModel: CoreViewModel,
settingsModel: SettingsModel
) {
this.settingsModel = settingsModel;
// init
constructor(
public coreViewModel: CoreViewModel,
settingsModel: SettingsModel,
) {
this.settingsModel = settingsModel;
this.username.value = settingsModel.username;
this.usernameInput.value = settingsModel.username;
this.firstDayOfWeekInput.value = settingsModel.firstDayOfWeek;
this.username.value = settingsModel.username;
this.usernameInput.value = settingsModel.username;
this.firstDayOfWeekInput.value = settingsModel.firstDayOfWeek;
// subscriptions
this.firstDayOfWeekInput.subscribe(this.setFirstDayofWeek);
}
// subscriptions
this.firstDayOfWeekInput.subscribe(this.setFirstDayofWeek);
}
}
+63 -52
View File
@@ -1,70 +1,81 @@
import * as React from "bloatless-react";
import StorageModel, {
PATH_COMPONENT_SEPARATOR,
PATH_COMPONENT_SEPARATOR,
} from "../../Model/Global/storageModel";
import CoreViewModel from "./coreViewModel";
import { translations } from "../../View/translations";
export default class StorageViewModel {
storageModel: StorageModel;
storageModel: StorageModel;
// state
isShowingStorageModal: React.State<boolean> = new React.State(false);
selectedPath: React.State<string> = new React.State(PATH_COMPONENT_SEPARATOR);
didMakeChanges: React.State<boolean> = new React.State(false);
selectedFileName: React.State<string>;
selectedFileContent: React.State<string>;
lastDeletedItemPath: React.State<string> = new React.State("");
// methods
getSelectedItemContent = (): string => {
const path = StorageModel.stringToPathComponents(this.selectedPath.value);
const content = this.storageModel.read(path);
return (
(content ?? translations.storage.notAFile) ||
translations.storage.contentEmpty
// state
isShowingStorageModal: React.State<boolean> = new React.State(false);
selectedPath: React.State<string> = new React.State(
PATH_COMPONENT_SEPARATOR,
);
};
didMakeChanges: React.State<boolean> = new React.State(false);
deleteSelectedItem = (): void => {
const path = StorageModel.stringToPathComponents(this.selectedPath.value);
this.lastDeletedItemPath.value = this.selectedPath.value;
selectedFileName: React.State<string>;
selectedFileContent: React.State<string>;
this.storageModel.removeRecursively(path);
this.didMakeChanges.value = true;
};
lastDeletedItemPath: React.State<string> = new React.State("");
removeJunk = (): void => {
this.storageModel.removeJunk();
this.selectedPath.value = PATH_COMPONENT_SEPARATOR;
};
// methods
getSelectedItemContent = (): string => {
const path = StorageModel.stringToPathComponents(
this.selectedPath.value,
);
const content = this.storageModel.read(path);
return (
(content ?? translations.storage.notAFile) ||
translations.storage.contentEmpty
);
};
// view
showStorageModal = (): void => {
this.isShowingStorageModal.value = true;
};
deleteSelectedItem = (): void => {
const path = StorageModel.stringToPathComponents(
this.selectedPath.value,
);
this.lastDeletedItemPath.value = this.selectedPath.value;
hideStorageModal = (): void => {
if (this.didMakeChanges.value == true) {
window.location.reload();
return;
this.storageModel.removeRecursively(path);
this.didMakeChanges.value = true;
};
removeJunk = (): void => {
this.storageModel.removeJunk();
this.selectedPath.value = PATH_COMPONENT_SEPARATOR;
};
// view
showStorageModal = (): void => {
this.isShowingStorageModal.value = true;
};
hideStorageModal = (): void => {
if (this.didMakeChanges.value == true) {
window.location.reload();
return;
}
this.isShowingStorageModal.value = false;
};
// init
constructor(
public coreViewModel: CoreViewModel,
storageModel: StorageModel,
) {
this.storageModel = storageModel;
this.selectedFileName = React.createProxyState(
[this.selectedPath],
() => StorageModel.getFileNameFromString(this.selectedPath.value),
);
this.selectedFileContent = React.createProxyState(
[this.selectedPath],
() => this.getSelectedItemContent(),
);
}
this.isShowingStorageModal.value = false;
};
// init
constructor(public coreViewModel: CoreViewModel, storageModel: StorageModel) {
this.storageModel = storageModel;
this.selectedFileName = React.createProxyState([this.selectedPath], () =>
StorageModel.getFileNameFromString(this.selectedPath.value)
);
this.selectedFileContent = React.createProxyState([this.selectedPath], () =>
this.getSelectedItemContent()
);
}
}
+258 -252
View File
@@ -1,8 +1,8 @@
import * as React from "bloatless-react";
import BoardsAndTasksModel, {
BoardInfoFileContent,
TaskFileContent,
BoardInfoFileContent,
TaskFileContent,
} from "../../Model/Files/boardsAndTasksModel";
import ChatViewModel from "../Chat/chatViewModel";
@@ -15,293 +15,299 @@ import TaskPageViewModel from "./taskPageViewModel";
import TaskViewModel from "./taskViewModel";
export default class BoardViewModel extends TaskContainingPageViewModel {
storageModel: StorageModel;
boardsAndTasksModel: BoardsAndTasksModel;
storageModel: StorageModel;
boardsAndTasksModel: BoardsAndTasksModel;
taskPageViewModel: TaskPageViewModel;
taskPageViewModel: TaskPageViewModel;
// data
boardInfo: BoardInfoFileContent;
// data
boardInfo: BoardInfoFileContent;
// state
name: React.State<string> = new React.State("");
color: React.State<Color> = new React.State<any>(Color.Standard);
// state
name: React.State<string> = new React.State("");
color: React.State<Color> = new React.State<any>(Color.Standard);
index: React.State<number> = new React.State(0);
index: React.State<number> = new React.State(0);
selectedPage: React.State<BoardPageType> = new React.State<any>(
BoardPageType.List
);
isSelected: React.State<boolean>;
isPresentingSettingsModal: React.State<boolean> = new React.State(false);
isPresentingFilterModal: React.State<boolean> = new React.State(false);
searchViewModel: SearchViewModel<TaskViewModel>;
filteredTaskViewModels: React.ListState<TaskViewModel> =
new React.ListState();
// paths
getBasePath = (): string[] => {
return [...this.taskPageViewModel.getBoardViewPath(this.boardInfo.fileId)];
};
getLastUsedBoardPath = (): string[] => {
return [...this.getBasePath(), BoardViewModelSubPath.LastUsedView];
};
getPreviousSearchesPath = (): string[] => {
return [...this.getBasePath(), BoardViewModelSubPath.PreviousSearches];
};
getLastSearchPath = (): string[] => {
return [...this.getBasePath(), BoardViewModelSubPath.LastSearch];
};
// settings
saveSettings = (): void => {
const newBoardInfoFileContent: BoardInfoFileContent =
BoardsAndTasksModel.createBoardInfoFileContent(
this.boardInfo.fileId,
this.name.value,
this.color.value
);
this.taskPageViewModel.updateBoard(newBoardInfoFileContent);
};
applyColor = (): void => {
this.taskPageViewModel.chatViewModel.setDisplayedColor(this.color.value);
};
deleteBoard = (): void => {
this.taskPageViewModel.deleteBoard(this.boardInfo);
this.chatViewModel.taskBoardSuggestions.remove(this.boardInfo.fileId);
this.close();
};
// methods
createTask = (): void => {
this.createTaskFromBoardId(this.boardInfo.fileId);
};
handleDropWithinBoard = (category?: string, status?: string): void => {
const draggedObject: any = this.coreViewModel.draggedObject.value;
if (draggedObject instanceof TaskViewModel == false) return;
draggedObject.setCategoryAndStatus(category, status);
};
handleDropBetweenBoards = (): void => {
const draggedObject: any = this.coreViewModel.draggedObject.value;
if (draggedObject instanceof TaskViewModel == false) return;
draggedObject.setBoardId(this.boardInfo.fileId);
};
// storage
storeLastUsedView = (): void => {
const path: string[] = this.getLastUsedBoardPath();
const lastUsedView: string = this.selectedPage.value;
this.storageModel.write(path, lastUsedView);
};
restoreLastUsedView = (): void => {
const path: string[] = this.getLastUsedBoardPath();
const lastUsedView: string | null = this.storageModel.read(path);
if (lastUsedView == null) return;
this.selectedPage.value = lastUsedView as BoardPageType;
};
handleNewSearch = (searchTerm: string): void => {
const suggestionPath: string[] = [
...this.getPreviousSearchesPath(),
searchTerm,
];
this.storageModel.write(suggestionPath, "");
if (!this.coreViewModel.boardSearchSuggestions.value.has(searchTerm)) {
this.coreViewModel.boardSearchSuggestions.add(searchTerm);
}
const lastSearchPath: string[] = this.getLastSearchPath();
this.storageModel.write(lastSearchPath, searchTerm);
};
// view
showTask = (taskFileContent: TaskFileContent): void => {
if (taskFileContent.boardId != this.boardInfo.fileId) {
// remove task that was moved to different board
this.boardsAndTasksModel.deleteTaskReference(
this.boardInfo.fileId,
taskFileContent.fileId
);
this.removeTaskFromView(taskFileContent);
return;
}
const taskViewModel: TaskViewModel = new TaskViewModel(
this.coreViewModel,
this.chatViewModel,
this.boardsAndTasksModel,
this,
taskFileContent
selectedPage: React.State<BoardPageType> = new React.State<any>(
BoardPageType.List,
);
this.taskViewModels.set(taskFileContent.fileId, taskViewModel);
};
removeTaskFromView = (taskFileContent: TaskFileContent): void => {
this.taskViewModels.remove(taskFileContent.fileId);
this.updateIndex();
};
isSelected: React.State<boolean>;
isPresentingSettingsModal: React.State<boolean> = new React.State(false);
isPresentingFilterModal: React.State<boolean> = new React.State(false);
select = (): void => {
this.taskPageViewModel.selectBoard(this);
};
searchViewModel: SearchViewModel<TaskViewModel>;
filteredTaskViewModels: React.ListState<TaskViewModel> =
new React.ListState();
close = (): void => {
this.taskPageViewModel.closeBoard();
this.taskViewModels.clear();
};
// paths
getBasePath = (): string[] => {
return [
...this.taskPageViewModel.getBoardViewPath(this.boardInfo.fileId),
];
};
showSettings = (): void => {
this.isPresentingSettingsModal.value = true;
};
getLastUsedBoardPath = (): string[] => {
return [...this.getBasePath(), BoardViewModelSubPath.LastUsedView];
};
hideSettings = (): void => {
this.saveSettings();
this.isPresentingSettingsModal.value = false;
};
getPreviousSearchesPath = (): string[] => {
return [...this.getBasePath(), BoardViewModelSubPath.PreviousSearches];
};
showFilterModal = (): void => {
this.isPresentingFilterModal.value = true;
};
getLastSearchPath = (): string[] => {
return [...this.getBasePath(), BoardViewModelSubPath.LastSearch];
};
hideFilterModal = (): void => {
this.isPresentingFilterModal.value = false;
};
// settings
saveSettings = (): void => {
const newBoardInfoFileContent: BoardInfoFileContent =
BoardsAndTasksModel.createBoardInfoFileContent(
this.boardInfo.fileId,
this.name.value,
this.color.value,
);
this.taskPageViewModel.updateBoard(newBoardInfoFileContent);
};
updateIndex = (): void => {
const index: number =
this.taskPageViewModel.boardIndexManager.getIndex(this);
this.index.value = index;
};
applyColor = (): void => {
this.taskPageViewModel.chatViewModel.setDisplayedColor(
this.color.value,
);
};
// load
loadListRelevantData = (): void => {
this.name.value = this.boardInfo.name;
this.color.value = this.boardInfo.color;
};
deleteBoard = (): void => {
this.taskPageViewModel.deleteBoard(this.boardInfo);
this.chatViewModel.taskBoardSuggestions.remove(this.boardInfo.fileId);
this.close();
};
loadTasks = (): void => {
const taskIds: string[] = this.boardsAndTasksModel.listTaskIds(
this.boardInfo.fileId
);
for (const taskId of taskIds) {
if (this.taskViewModels.value.has(taskId)) return;
// methods
createTask = (): void => {
this.createTaskFromBoardId(this.boardInfo.fileId);
};
const taskFileContent: TaskFileContent | null =
this.boardsAndTasksModel.getLatestTaskFileContent(taskId);
if (taskFileContent == null) continue;
handleDropWithinBoard = (category?: string, status?: string): void => {
const draggedObject: any = this.coreViewModel.draggedObject.value;
if (draggedObject instanceof TaskViewModel == false) return;
draggedObject.setCategoryAndStatus(category, status);
};
const taskViewModel: TaskViewModel = new TaskViewModel(
this.coreViewModel,
this.chatViewModel,
this.boardsAndTasksModel,
this,
taskFileContent
);
this.taskViewModels.set(taskFileContent.fileId, taskViewModel);
}
handleDropBetweenBoards = (): void => {
const draggedObject: any = this.coreViewModel.draggedObject.value;
if (draggedObject instanceof TaskViewModel == false) return;
draggedObject.setBoardId(this.boardInfo.fileId);
};
this.updateTaskIndices();
};
// storage
storeLastUsedView = (): void => {
const path: string[] = this.getLastUsedBoardPath();
const lastUsedView: string = this.selectedPage.value;
this.storageModel.write(path, lastUsedView);
};
loadSearchSuggestions = (): void => {
const dirPath: string[] = this.getPreviousSearchesPath();
const searches: string[] = this.storageModel.list(dirPath);
this.coreViewModel.boardSearchSuggestions.add(...searches);
};
restoreLastUsedView = (): void => {
const path: string[] = this.getLastUsedBoardPath();
const lastUsedView: string | null = this.storageModel.read(path);
if (lastUsedView == null) return;
restoreSearch = (): void => {
const lastSearchPath: string[] = this.getLastSearchPath();
const lastSearch: string | null = this.storageModel.read(lastSearchPath);
if (lastSearch != null) {
this.searchViewModel.search(lastSearch);
}
};
this.selectedPage.value = lastUsedView as BoardPageType;
};
loadData = (): void => {
this.restoreLastUsedView();
this.loadTasks();
this.loadSearchSuggestions();
};
handleNewSearch = (searchTerm: string): void => {
const suggestionPath: string[] = [
...this.getPreviousSearchesPath(),
searchTerm,
];
this.storageModel.write(suggestionPath, "");
if (!this.coreViewModel.boardSearchSuggestions.value.has(searchTerm)) {
this.coreViewModel.boardSearchSuggestions.add(searchTerm);
}
// init
constructor(
public coreViewModel: CoreViewModel,
public chatViewModel: ChatViewModel,
storageModel: StorageModel,
boardsAndTasksModel: BoardsAndTasksModel,
taskPageViewModel: TaskPageViewModel,
boardInfo: BoardInfoFileContent
) {
super(coreViewModel, chatViewModel, boardsAndTasksModel);
const lastSearchPath: string[] = this.getLastSearchPath();
this.storageModel.write(lastSearchPath, searchTerm);
};
// set
this.storageModel = storageModel;
this.boardsAndTasksModel = boardsAndTasksModel;
this.taskPageViewModel = taskPageViewModel;
this.boardInfo = boardInfo;
// view
showTask = (taskFileContent: TaskFileContent): void => {
if (taskFileContent.boardId != this.boardInfo.fileId) {
// remove task that was moved to different board
this.boardsAndTasksModel.deleteTaskReference(
this.boardInfo.fileId,
taskFileContent.fileId,
);
this.removeTaskFromView(taskFileContent);
return;
}
const taskViewModel: TaskViewModel = new TaskViewModel(
this.coreViewModel,
this.chatViewModel,
this.boardsAndTasksModel,
this,
taskFileContent,
);
this.taskViewModels.set(taskFileContent.fileId, taskViewModel);
};
removeTaskFromView = (taskFileContent: TaskFileContent): void => {
this.taskViewModels.remove(taskFileContent.fileId);
this.updateIndex();
};
select = (): void => {
this.taskPageViewModel.selectBoard(this);
};
close = (): void => {
this.taskPageViewModel.closeBoard();
this.taskViewModels.clear();
};
showSettings = (): void => {
this.isPresentingSettingsModal.value = true;
};
hideSettings = (): void => {
this.saveSettings();
this.isPresentingSettingsModal.value = false;
};
showFilterModal = (): void => {
this.isPresentingFilterModal.value = true;
};
hideFilterModal = (): void => {
this.isPresentingFilterModal.value = false;
};
updateIndex = (): void => {
const index: number =
this.taskPageViewModel.boardIndexManager.getIndex(this);
this.index.value = index;
};
// load
this.loadListRelevantData();
loadListRelevantData = (): void => {
this.name.value = this.boardInfo.name;
this.color.value = this.boardInfo.color;
};
// subscriptions
this.isSelected = React.createProxyState(
[this.taskPageViewModel.selectedBoardId],
() =>
this.taskPageViewModel.selectedBoardId.value == this.boardInfo.fileId
);
loadTasks = (): void => {
const taskIds: string[] = this.boardsAndTasksModel.listTaskIds(
this.boardInfo.fileId,
);
for (const taskId of taskIds) {
if (this.taskViewModels.value.has(taskId)) return;
this.color.subscribe(() => {
if (this.isSelected.value == false) return;
this.applyColor();
});
const taskFileContent: TaskFileContent | null =
this.boardsAndTasksModel.getLatestTaskFileContent(taskId);
if (taskFileContent == null) continue;
this.selectedPage.subscribeSilent(() => {
this.storeLastUsedView();
});
const taskViewModel: TaskViewModel = new TaskViewModel(
this.coreViewModel,
this.chatViewModel,
this.boardsAndTasksModel,
this,
taskFileContent,
);
this.taskViewModels.set(taskFileContent.fileId, taskViewModel);
}
// handlers
boardsAndTasksModel.taskHandlerManager.addHandler(
(taskFileContent: TaskFileContent) => {
if (taskFileContent.boardId != this.boardInfo.fileId) return;
this.showTask(taskFileContent);
this.updateTaskIndices();
}
);
};
// search
this.searchViewModel = new SearchViewModel(
this.taskViewModels,
this.filteredTaskViewModels,
TaskViewModel.getStringsForFilter,
this.coreViewModel.boardSearchSuggestions
);
this.searchViewModel.appliedQuery.subscribeSilent((newQuery) => {
this.handleNewSearch(newQuery);
});
this.restoreSearch();
}
loadSearchSuggestions = (): void => {
const dirPath: string[] = this.getPreviousSearchesPath();
const searches: string[] = this.storageModel.list(dirPath);
this.coreViewModel.boardSearchSuggestions.add(...searches);
};
restoreSearch = (): void => {
const lastSearchPath: string[] = this.getLastSearchPath();
const lastSearch: string | null =
this.storageModel.read(lastSearchPath);
if (lastSearch != null) {
this.searchViewModel.search(lastSearch);
}
};
loadData = (): void => {
this.restoreLastUsedView();
this.loadTasks();
this.loadSearchSuggestions();
};
// init
constructor(
public coreViewModel: CoreViewModel,
public chatViewModel: ChatViewModel,
storageModel: StorageModel,
boardsAndTasksModel: BoardsAndTasksModel,
taskPageViewModel: TaskPageViewModel,
boardInfo: BoardInfoFileContent,
) {
super(coreViewModel, chatViewModel, boardsAndTasksModel);
// set
this.storageModel = storageModel;
this.boardsAndTasksModel = boardsAndTasksModel;
this.taskPageViewModel = taskPageViewModel;
this.boardInfo = boardInfo;
// load
this.loadListRelevantData();
// subscriptions
this.isSelected = React.createProxyState(
[this.taskPageViewModel.selectedBoardId],
() =>
this.taskPageViewModel.selectedBoardId.value ==
this.boardInfo.fileId,
);
this.color.subscribe(() => {
if (this.isSelected.value == false) return;
this.applyColor();
});
this.selectedPage.subscribeSilent(() => {
this.storeLastUsedView();
});
// handlers
boardsAndTasksModel.taskHandlerManager.addHandler(
(taskFileContent: TaskFileContent) => {
if (taskFileContent.boardId != this.boardInfo.fileId) return;
this.showTask(taskFileContent);
this.updateTaskIndices();
},
);
// search
this.searchViewModel = new SearchViewModel(
this.taskViewModels,
this.filteredTaskViewModels,
TaskViewModel.getStringsForFilter,
this.coreViewModel.boardSearchSuggestions,
);
this.searchViewModel.appliedQuery.subscribeSilent((newQuery) => {
this.handleNewSearch(newQuery);
});
this.restoreSearch();
}
}
export enum BoardViewModelSubPath {
LastUsedView = "last-used-view",
PreviousSearches = "previous-searches",
LastSearch = "last-search",
LastUsedView = "last-used-view",
PreviousSearches = "previous-searches",
LastSearch = "last-search",
}
// types
export enum BoardPageType {
List = "list",
Kanban = "kanban",
StatusGrid = "status-grid",
List = "list",
Kanban = "kanban",
StatusGrid = "status-grid",
}
+195 -188
View File
@@ -1,7 +1,7 @@
import * as React from "bloatless-react";
import BoardsAndTasksModel, {
TaskFileContent,
TaskFileContent,
} from "../../Model/Files/boardsAndTasksModel";
import CalendarModel, { MonthGrid } from "../../Model/Files/calendarModel";
@@ -14,201 +14,208 @@ import TaskViewModel from "./taskViewModel";
export const CALENDAR_EVENT_BOARD_ID = "events";
export default class CalendarPageViewModel extends TaskContainingPageViewModel {
storageModel: StorageModel;
calendarModel: CalendarModel;
boardsAndTasksModel: BoardsAndTasksModel;
storageModel: StorageModel;
calendarModel: CalendarModel;
boardsAndTasksModel: BoardsAndTasksModel;
// data
get monthString(): string {
return CalendarModel.getMonthString(
this.selectedYear.value.toString(),
this.selectedMonth.value.toString()
);
}
// paths
getBasePath = (): string[] => {
return [...this.calendarModel.getViewPath()];
};
// state
selectedYear: React.State<number> = new React.State(0);
selectedMonth: React.State<number> = new React.State(0);
selectedDate: React.State<number> = new React.State(0);
monthGrid: React.State<MonthGrid<React.MapState<TaskViewModel>> | undefined> =
new React.State<any>(undefined);
// methods
createEvent = (): void => {
const taskFileContent: TaskFileContent =
this.boardsAndTasksModel.createTask(CALENDAR_EVENT_BOARD_ID);
taskFileContent.date = CalendarModel.getISODateString(
this.selectedYear.value.toString(),
this.selectedMonth.value.toString(),
this.selectedDate.value.toString()
);
const taskViewModel: TaskViewModel = new TaskViewModel(
this.coreViewModel,
this.chatViewModel,
this.boardsAndTasksModel,
this,
taskFileContent
);
this.selectTask(taskViewModel);
this.updateTaskIndices();
};
getEventsForDate = (): React.MapState<TaskViewModel> | undefined => {
const paddedDate: string = CalendarModel.padDateOrMonth(
this.selectedDate.toString()
);
if (this.monthGrid.value == undefined) {
return undefined;
// data
get monthString(): string {
return CalendarModel.getMonthString(
this.selectedYear.value.toString(),
this.selectedMonth.value.toString(),
);
}
return this.monthGrid.value.days[paddedDate];
};
// paths
getBasePath = (): string[] => {
return [...this.calendarModel.getViewPath()];
};
// view
getTaskMapState = (
taskFileContent: TaskFileContent
): React.MapState<TaskViewModel> | null => {
if (this.monthGrid.value == null) return null;
// state
selectedYear: React.State<number> = new React.State(0);
selectedMonth: React.State<number> = new React.State(0);
selectedDate: React.State<number> = new React.State(0);
const date: string = CalendarModel.isoToDateString(
taskFileContent.date ?? ""
);
return this.monthGrid.value.days[date];
};
monthGrid: React.State<
MonthGrid<React.MapState<TaskViewModel>> | undefined
> = new React.State<any>(undefined);
showTask = (taskFileContent: TaskFileContent): void => {
const monthString: string | undefined = CalendarModel.isoToMonthString(
taskFileContent.date ?? ""
);
if (monthString == undefined || monthString != this.monthString) {
this.removeTaskFromView(taskFileContent);
this.calendarModel.deleteTaskReference(
this.monthString,
taskFileContent.fileId
);
return;
// methods
createEvent = (): void => {
const taskFileContent: TaskFileContent =
this.boardsAndTasksModel.createTask(CALENDAR_EVENT_BOARD_ID);
taskFileContent.date = CalendarModel.getISODateString(
this.selectedYear.value.toString(),
this.selectedMonth.value.toString(),
this.selectedDate.value.toString(),
);
const taskViewModel: TaskViewModel = new TaskViewModel(
this.coreViewModel,
this.chatViewModel,
this.boardsAndTasksModel,
this,
taskFileContent,
);
this.selectTask(taskViewModel);
this.updateTaskIndices();
};
getEventsForDate = (): React.MapState<TaskViewModel> | undefined => {
const paddedDate: string = CalendarModel.padDateOrMonth(
this.selectedDate.toString(),
);
if (this.monthGrid.value == undefined) {
return undefined;
}
return this.monthGrid.value.days[paddedDate];
};
// view
getTaskMapState = (
taskFileContent: TaskFileContent,
): React.MapState<TaskViewModel> | null => {
if (this.monthGrid.value == null) return null;
const date: string = CalendarModel.isoToDateString(
taskFileContent.date ?? "",
);
return this.monthGrid.value.days[date];
};
showTask = (taskFileContent: TaskFileContent): void => {
const monthString: string | undefined = CalendarModel.isoToMonthString(
taskFileContent.date ?? "",
);
if (monthString == undefined || monthString != this.monthString) {
this.removeTaskFromView(taskFileContent);
this.calendarModel.deleteTaskReference(
this.monthString,
taskFileContent.fileId,
);
return;
}
const taskViewModel: TaskViewModel = new TaskViewModel(
this.coreViewModel,
this.chatViewModel,
this.boardsAndTasksModel,
this,
taskFileContent,
);
const mapState: React.MapState<TaskViewModel> | null =
this.getTaskMapState(taskFileContent);
this.taskViewModels.handleRemoval(taskViewModel, () => {
mapState?.remove(taskFileContent.fileId);
});
this.taskViewModels.remove(taskFileContent.fileId);
this.taskViewModels.set(taskFileContent.fileId, taskViewModel);
mapState?.set(taskFileContent.fileId, taskViewModel);
};
removeTaskFromView = (taskFileContent: TaskFileContent): void => {
this.taskViewModels.remove(taskFileContent.fileId);
};
showToday = (): void => {
const today: Date = new Date();
this.selectedYear.value = today.getFullYear();
this.selectedMonth.value = today.getMonth() + 1;
this.selectedDate.value = today.getDate();
};
showPreviousMonth = (): void => {
this.selectedMonth.value -= 1;
if (this.selectedMonth.value <= 0) {
this.selectedYear.value -= 1;
this.selectedMonth.value = 12;
}
};
showNextMonth = (): void => {
this.selectedMonth.value += 1;
if (this.selectedMonth.value >= 13) {
this.selectedYear.value += 1;
this.selectedMonth.value = 1;
}
};
handleDrop = (year: string, month: string, date: string): void => {
const ISOString: string = CalendarModel.getISODateString(
year,
month,
date,
);
const draggedObject: any = this.coreViewModel.draggedObject.value;
if (draggedObject instanceof TaskViewModel == false) return;
draggedObject.setDate(ISOString);
};
// load
loadMonthTasks = (): void => {
this.monthGrid.value = this.calendarModel.generateMonthGrid(
this.selectedYear.value,
this.selectedMonth.value,
() => new React.MapState(),
);
const taskIds: string[] = this.calendarModel.listTaskIds(
this.monthString,
);
for (const taskId of taskIds) {
const taskFileContent: TaskFileContent | null =
this.boardsAndTasksModel.getLatestTaskFileContent(taskId);
if (taskFileContent == null) continue;
this.showTask(taskFileContent);
}
this.updateTaskIndices();
};
loadData = (): void => {
this.loadMonthTasks();
this.showToday();
};
// init
constructor(
public coreViewModel: CoreViewModel,
public chatViewModel: ChatViewModel,
storageModel: StorageModel,
calendarModel: CalendarModel,
boardsAndTasksModel: BoardsAndTasksModel,
) {
super(coreViewModel, chatViewModel, boardsAndTasksModel);
this.storageModel = storageModel;
this.calendarModel = calendarModel;
this.boardsAndTasksModel = boardsAndTasksModel;
this.chatViewModel = chatViewModel;
React.bulkSubscribe([this.selectedYear, this.selectedMonth], () => {
this.loadMonthTasks();
});
// handlers
boardsAndTasksModel.taskHandlerManager.addHandler(
(taskFileContent: TaskFileContent) => {
this.showTask(taskFileContent);
},
);
}
const taskViewModel: TaskViewModel = new TaskViewModel(
this.coreViewModel,
this.chatViewModel,
this.boardsAndTasksModel,
this,
taskFileContent
);
const mapState: React.MapState<TaskViewModel> | null =
this.getTaskMapState(taskFileContent);
this.taskViewModels.handleRemoval(taskViewModel, () => {
mapState?.remove(taskFileContent.fileId);
});
this.taskViewModels.remove(taskFileContent.fileId);
this.taskViewModels.set(taskFileContent.fileId, taskViewModel);
mapState?.set(taskFileContent.fileId, taskViewModel);
};
removeTaskFromView = (taskFileContent: TaskFileContent): void => {
this.taskViewModels.remove(taskFileContent.fileId);
};
showToday = (): void => {
const today: Date = new Date();
this.selectedYear.value = today.getFullYear();
this.selectedMonth.value = today.getMonth() + 1;
this.selectedDate.value = today.getDate();
};
showPreviousMonth = (): void => {
this.selectedMonth.value -= 1;
if (this.selectedMonth.value <= 0) {
this.selectedYear.value -= 1;
this.selectedMonth.value = 12;
}
};
showNextMonth = (): void => {
this.selectedMonth.value += 1;
if (this.selectedMonth.value >= 13) {
this.selectedYear.value += 1;
this.selectedMonth.value = 1;
}
};
handleDrop = (year: string, month: string, date: string): void => {
const ISOString: string = CalendarModel.getISODateString(year, month, date);
const draggedObject: any = this.coreViewModel.draggedObject.value;
if (draggedObject instanceof TaskViewModel == false) return;
draggedObject.setDate(ISOString);
};
// load
loadMonthTasks = (): void => {
this.monthGrid.value = this.calendarModel.generateMonthGrid(
this.selectedYear.value,
this.selectedMonth.value,
() => new React.MapState()
);
const taskIds: string[] = this.calendarModel.listTaskIds(this.monthString);
for (const taskId of taskIds) {
const taskFileContent: TaskFileContent | null =
this.boardsAndTasksModel.getLatestTaskFileContent(taskId);
if (taskFileContent == null) continue;
this.showTask(taskFileContent);
}
this.updateTaskIndices();
};
loadData = (): void => {
this.loadMonthTasks();
this.showToday();
};
// init
constructor(
public coreViewModel: CoreViewModel,
public chatViewModel: ChatViewModel,
storageModel: StorageModel,
calendarModel: CalendarModel,
boardsAndTasksModel: BoardsAndTasksModel
) {
super(coreViewModel, chatViewModel, boardsAndTasksModel);
this.storageModel = storageModel;
this.calendarModel = calendarModel;
this.boardsAndTasksModel = boardsAndTasksModel;
this.chatViewModel = chatViewModel;
React.bulkSubscribe([this.selectedYear, this.selectedMonth], () => {
this.loadMonthTasks();
});
// handlers
boardsAndTasksModel.taskHandlerManager.addHandler(
(taskFileContent: TaskFileContent) => {
this.showTask(taskFileContent);
}
);
}
}
export enum TaskPageViewModelSubPath {
LastUsedBoard = "last-used-board",
LastUsedBoard = "last-used-board",
}
+70 -63
View File
@@ -6,76 +6,83 @@ import ChatViewModel from "../Chat/chatViewModel";
import CoreViewModel from "../Global/coreViewModel";
export default class MessagePageViewModel {
chatViewModel: ChatViewModel;
chatViewModel: ChatViewModel;
// state
chatMessageViewModels: React.MapState<ChatMessageViewModel> =
new React.MapState();
composingMessage: React.State<string> = new React.State("");
// state
chatMessageViewModels: React.MapState<ChatMessageViewModel> =
new React.MapState();
composingMessage: React.State<string> = new React.State("");
// guards
cannotSendMessage: React.State<boolean>;
// guards
cannotSendMessage: React.State<boolean>;
// methods
sendMessage = (): void => {
if (this.cannotSendMessage.value == true) return;
// methods
sendMessage = (): void => {
if (this.cannotSendMessage.value == true) return;
this.sendMessageFromBody(this.composingMessage.value);
this.composingMessage.value = "";
};
this.sendMessageFromBody(this.composingMessage.value);
this.composingMessage.value = "";
};
sendMessageFromBody = (body: string): void => {
this.chatViewModel.chatModel.sendMessage(body);
};
sendMessageFromBody = (body: string): void => {
this.chatViewModel.chatModel.sendMessage(body);
};
decryptMessage = async (
messageViewModel: ChatMessageViewModel
): Promise<void> => {
const chatMessage: ChatMessage = messageViewModel.chatMessage;
await this.chatViewModel.chatModel.decryptMessage(chatMessage);
this.chatViewModel.chatModel.addMessage(chatMessage);
messageViewModel.loadData();
};
decryptMessage = async (
messageViewModel: ChatMessageViewModel,
): Promise<void> => {
const chatMessage: ChatMessage = messageViewModel.chatMessage;
await this.chatViewModel.chatModel.decryptMessage(chatMessage);
this.chatViewModel.chatModel.addMessage(chatMessage);
messageViewModel.loadData();
};
// view
showChatMessage = (chatMessage: ChatMessage): void => {
const chatMessageViewModel = new ChatMessageViewModel(
this.coreViewModel,
this,
chatMessage,
chatMessage.sender == this.chatViewModel.settingsViewModel.username.value
);
// view
showChatMessage = (chatMessage: ChatMessage): void => {
const chatMessageViewModel = new ChatMessageViewModel(
this.coreViewModel,
this,
chatMessage,
chatMessage.sender ==
this.chatViewModel.settingsViewModel.username.value,
);
const existingChatMessageViewModel: ChatMessageViewModel | undefined =
this.chatMessageViewModels.value.get(chatMessage.id);
if (existingChatMessageViewModel != undefined) {
existingChatMessageViewModel.body.value = chatMessage.body;
existingChatMessageViewModel.status.value = chatMessage.status;
} else {
this.chatMessageViewModels.set(chatMessage.id, chatMessageViewModel);
const existingChatMessageViewModel: ChatMessageViewModel | undefined =
this.chatMessageViewModels.value.get(chatMessage.id);
if (existingChatMessageViewModel != undefined) {
existingChatMessageViewModel.body.value = chatMessage.body;
existingChatMessageViewModel.status.value = chatMessage.status;
} else {
this.chatMessageViewModels.set(
chatMessage.id,
chatMessageViewModel,
);
}
};
// load
loadData = (): void => {
this.chatMessageViewModels.clear();
for (const chatMessage of this.chatViewModel.chatModel.messages) {
this.showChatMessage(chatMessage);
}
};
// init
constructor(
public coreViewModel: CoreViewModel,
chatViewModel: ChatViewModel,
) {
this.chatViewModel = chatViewModel;
this.cannotSendMessage = React.createProxyState(
[
this.chatViewModel.settingsViewModel.username,
this.composingMessage,
],
() =>
this.chatViewModel.settingsViewModel.username.value == "" ||
this.composingMessage.value == "",
);
}
};
// load
loadData = (): void => {
this.chatMessageViewModels.clear();
for (const chatMessage of this.chatViewModel.chatModel.messages) {
this.showChatMessage(chatMessage);
}
};
// init
constructor(
public coreViewModel: CoreViewModel,
chatViewModel: ChatViewModel
) {
this.chatViewModel = chatViewModel;
this.cannotSendMessage = React.createProxyState(
[this.chatViewModel.settingsViewModel.username, this.composingMessage],
() =>
this.chatViewModel.settingsViewModel.username.value == "" ||
this.composingMessage.value == ""
);
}
}
+104 -104
View File
@@ -5,128 +5,128 @@ import { Color } from "../../colors";
import CoreViewModel from "../Global/coreViewModel";
export default class SettingsPageViewModel {
chatViewModel: ChatViewModel;
chatViewModel: ChatViewModel;
// state
primaryChannel: React.State<string> = new React.State("");
primaryChannelInput: React.State<string> = new React.State("");
// state
primaryChannel: React.State<string> = new React.State("");
primaryChannelInput: React.State<string> = new React.State("");
secondaryChannels: React.ListState<string> = new React.ListState();
newSecondaryChannelInput: React.State<string> = new React.State("");
secondaryChannels: React.ListState<string> = new React.ListState();
newSecondaryChannelInput: React.State<string> = new React.State("");
encryptionKeyInput: React.State<string> = new React.State("");
shouldShowEncryptionKey: React.State<boolean> = new React.State(false);
encryptionKeyInputType: React.State<"text" | "password"> =
React.createProxyState([this.shouldShowEncryptionKey], () =>
this.shouldShowEncryptionKey.value == true ? "text" : "password"
encryptionKeyInput: React.State<string> = new React.State("");
shouldShowEncryptionKey: React.State<boolean> = new React.State(false);
encryptionKeyInputType: React.State<"text" | "password"> =
React.createProxyState([this.shouldShowEncryptionKey], () =>
this.shouldShowEncryptionKey.value == true ? "text" : "password",
);
color: React.State<Color> = new React.State<any>(Color.Standard);
// guards
cannotSetPrimaryChannel: React.State<boolean> = React.createProxyState(
[this.primaryChannel, this.primaryChannelInput],
() =>
this.primaryChannelInput.value == "" ||
this.primaryChannelInput.value == this.primaryChannel.value,
);
color: React.State<Color> = new React.State<any>(Color.Standard);
// guards
cannotSetPrimaryChannel: React.State<boolean> = React.createProxyState(
[this.primaryChannel, this.primaryChannelInput],
() =>
this.primaryChannelInput.value == "" ||
this.primaryChannelInput.value == this.primaryChannel.value
);
cannotAddSecondaryChannel: React.State<boolean> = React.createProxyState(
[this.newSecondaryChannelInput],
() => this.newSecondaryChannelInput.value == ""
);
cannotSetEncryptionKey: React.State<boolean>;
// methods
setPrimaryChannel = (): void => {
this.chatViewModel.chatModel.setPrimaryChannel(
this.primaryChannelInput.value
cannotAddSecondaryChannel: React.State<boolean> = React.createProxyState(
[this.newSecondaryChannelInput],
() => this.newSecondaryChannelInput.value == "",
);
this.primaryChannel.value =
this.chatViewModel.chatModel.info.primaryChannel;
cannotSetEncryptionKey: React.State<boolean>;
this.chatViewModel.chatListViewModel.updateIndices();
};
// methods
setPrimaryChannel = (): void => {
this.chatViewModel.chatModel.setPrimaryChannel(
this.primaryChannelInput.value,
);
this.primaryChannel.value =
this.chatViewModel.chatModel.info.primaryChannel;
addSecondaryChannel = (): void => {
this.secondaryChannels.add(this.newSecondaryChannelInput.value);
this.newSecondaryChannelInput.value = "";
this.storeSecondaryChannels();
this.loadSecondaryChannels();
};
this.chatViewModel.chatListViewModel.updateIndices();
};
removeSecondaryChannel = (secondaryChannel: string): void => {
this.secondaryChannels.remove(secondaryChannel);
this.storeSecondaryChannels();
};
addSecondaryChannel = (): void => {
this.secondaryChannels.add(this.newSecondaryChannelInput.value);
this.newSecondaryChannelInput.value = "";
this.storeSecondaryChannels();
this.loadSecondaryChannels();
};
storeSecondaryChannels = (): void => {
this.chatViewModel.chatModel.setSecondaryChannels([
...this.secondaryChannels.value.values(),
]);
};
removeSecondaryChannel = (secondaryChannel: string): void => {
this.secondaryChannels.remove(secondaryChannel);
this.storeSecondaryChannels();
};
setEncryptionKey = (): void => {
this.chatViewModel.chatModel.setEncryptionKey(
this.encryptionKeyInput.value
);
storeSecondaryChannels = (): void => {
this.chatViewModel.chatModel.setSecondaryChannels([
...this.secondaryChannels.value.values(),
]);
};
// disable button
this.encryptionKeyInput.callSubscriptions();
};
setEncryptionKey = (): void => {
this.chatViewModel.chatModel.setEncryptionKey(
this.encryptionKeyInput.value,
);
applyColor = (newColor: Color): void => {
this.chatViewModel.setColor(newColor);
};
// disable button
this.encryptionKeyInput.callSubscriptions();
};
remove = (): void => {
this.chatViewModel.close();
this.chatViewModel.chatModel.delete();
this.chatViewModel.chatListViewModel.untrackChat(this.chatViewModel);
};
applyColor = (newColor: Color): void => {
this.chatViewModel.setColor(newColor);
};
// load
loadListRelevantData = (): void => {
this.primaryChannel.value =
this.chatViewModel.chatModel.info.primaryChannel;
remove = (): void => {
this.chatViewModel.close();
this.chatViewModel.chatModel.delete();
this.chatViewModel.chatListViewModel.untrackChat(this.chatViewModel);
};
this.color.value = this.chatViewModel.chatModel.color;
};
// load
loadListRelevantData = (): void => {
this.primaryChannel.value =
this.chatViewModel.chatModel.info.primaryChannel;
loadData = (): void => {
this.primaryChannelInput.value =
this.chatViewModel.chatModel.info.primaryChannel;
this.color.value = this.chatViewModel.chatModel.color;
};
this.loadSecondaryChannels();
loadData = (): void => {
this.primaryChannelInput.value =
this.chatViewModel.chatModel.info.primaryChannel;
this.encryptionKeyInput.value =
this.chatViewModel.chatModel.info.encryptionKey;
};
this.loadSecondaryChannels();
loadSecondaryChannels = (): void => {
this.secondaryChannels.clear();
for (const secondaryChannel of this.chatViewModel.chatModel
.secondaryChannels) {
this.secondaryChannels.add(secondaryChannel);
this.encryptionKeyInput.value =
this.chatViewModel.chatModel.info.encryptionKey;
};
loadSecondaryChannels = (): void => {
this.secondaryChannels.clear();
for (const secondaryChannel of this.chatViewModel.chatModel
.secondaryChannels) {
this.secondaryChannels.add(secondaryChannel);
}
};
// init
constructor(
public coreViewModel: CoreViewModel,
chatViewModel: ChatViewModel,
) {
this.chatViewModel = chatViewModel;
this.loadListRelevantData();
this.cannotSetEncryptionKey = React.createProxyState(
[this.encryptionKeyInput],
() =>
this.encryptionKeyInput.value ==
this.chatViewModel.chatModel.info.encryptionKey,
);
this.color.subscribe((newColor) => {
this.applyColor(newColor);
});
}
};
// init
constructor(
public coreViewModel: CoreViewModel,
chatViewModel: ChatViewModel
) {
this.chatViewModel = chatViewModel;
this.loadListRelevantData();
this.cannotSetEncryptionKey = React.createProxyState(
[this.encryptionKeyInput],
() =>
this.encryptionKeyInput.value ==
this.chatViewModel.chatModel.info.encryptionKey
);
this.color.subscribe((newColor) => {
this.applyColor(newColor);
});
}
}
@@ -1,7 +1,7 @@
import * as React from "bloatless-react";
import BoardsAndTasksModel, {
TaskFileContent,
TaskFileContent,
} from "../../Model/Files/boardsAndTasksModel";
import ChatViewModel from "../Chat/chatViewModel";
@@ -10,58 +10,58 @@ import { IndexManager } from "../../Model/Utility/utility";
import TaskViewModel from "./taskViewModel";
export default class TaskContainingPageViewModel {
boardsAndTasksModel: BoardsAndTasksModel;
boardsAndTasksModel: BoardsAndTasksModel;
// state
taskIndexManager: IndexManager<TaskViewModel> = new IndexManager(
(taskViewModel: TaskViewModel) => taskViewModel.sortingString
);
selectedTaskViewModel: React.State<TaskViewModel | undefined> =
new React.State<any>(undefined);
taskViewModels: React.MapState<TaskViewModel> = new React.MapState();
// methods
createTaskFromBoardId = (boardId: string): void => {
const taskFileContent: TaskFileContent =
this.boardsAndTasksModel.createTask(boardId);
const taskViewModel: TaskViewModel = new TaskViewModel(
this.coreViewModel,
this.chatViewModel,
this.boardsAndTasksModel,
this,
taskFileContent
// state
taskIndexManager: IndexManager<TaskViewModel> = new IndexManager(
(taskViewModel: TaskViewModel) => taskViewModel.sortingString,
);
this.selectTask(taskViewModel);
this.updateTaskIndices();
};
// view
showTask = (taskFileContent: TaskFileContent): void => {};
removeTaskFromView = (taskFileContent: TaskFileContent): void => {};
selectedTaskViewModel: React.State<TaskViewModel | undefined> =
new React.State<any>(undefined);
selectTask = (selectedTask: TaskViewModel): void => {
this.selectedTaskViewModel.value = selectedTask;
};
taskViewModels: React.MapState<TaskViewModel> = new React.MapState();
closeTask = () => {
this.selectedTaskViewModel.value = undefined;
};
// methods
createTaskFromBoardId = (boardId: string): void => {
const taskFileContent: TaskFileContent =
this.boardsAndTasksModel.createTask(boardId);
const taskViewModel: TaskViewModel = new TaskViewModel(
this.coreViewModel,
this.chatViewModel,
this.boardsAndTasksModel,
this,
taskFileContent,
);
this.selectTask(taskViewModel);
this.updateTaskIndices();
};
updateTaskIndices = (): void => {
this.taskIndexManager.update([...this.taskViewModels.value.values()]);
for (const boardViewModel of this.taskViewModels.value.values()) {
boardViewModel.updateIndex();
// view
showTask = (taskFileContent: TaskFileContent): void => {};
removeTaskFromView = (taskFileContent: TaskFileContent): void => {};
selectTask = (selectedTask: TaskViewModel): void => {
this.selectedTaskViewModel.value = selectedTask;
};
closeTask = () => {
this.selectedTaskViewModel.value = undefined;
};
updateTaskIndices = (): void => {
this.taskIndexManager.update([...this.taskViewModels.value.values()]);
for (const boardViewModel of this.taskViewModels.value.values()) {
boardViewModel.updateIndex();
}
};
// init
constructor(
public coreViewModel: CoreViewModel,
public chatViewModel: ChatViewModel,
boardsAndTasksModel: BoardsAndTasksModel,
) {
this.boardsAndTasksModel = boardsAndTasksModel;
}
};
// init
constructor(
public coreViewModel: CoreViewModel,
public chatViewModel: ChatViewModel,
boardsAndTasksModel: BoardsAndTasksModel
) {
this.boardsAndTasksModel = boardsAndTasksModel;
}
}
+148 -148
View File
@@ -1,7 +1,7 @@
import * as React from "bloatless-react";
import BoardsAndTasksModel, {
BoardInfoFileContent,
BoardInfoFileContent,
} from "../../Model/Files/boardsAndTasksModel";
import BoardViewModel from "./boardViewModel";
@@ -11,169 +11,169 @@ import { IndexManager } from "../../Model/Utility/utility";
import StorageModel from "../../Model/Global/storageModel";
export default class TaskPageViewModel {
storageModel: StorageModel;
boardsAndTasksModel: BoardsAndTasksModel;
storageModel: StorageModel;
boardsAndTasksModel: BoardsAndTasksModel;
// data
boardIndexManager: IndexManager<BoardViewModel> = new IndexManager(
(boardViewModel: BoardViewModel) => boardViewModel.name.value
);
// paths
getBasePath = (): string[] => {
return [...this.boardsAndTasksModel.getViewPath()];
};
getBoardViewPath = (boardId): string[] => {
return [...this.getBasePath(), boardId];
};
getLastUsedBoardPath = (): string[] => {
return [...this.getBasePath(), TaskPageViewModelSubPath.LastUsedBoard];
};
// state
newBoardNameInput: React.State<string> = new React.State("");
boardViewModels: React.MapState<BoardViewModel> = new React.MapState();
isShowingBoadList: React.State<boolean> = new React.State(true);
selectedBoardId: React.State<string | undefined> = new React.State<any>(
undefined
);
// guards
cannotCreateBoard: React.State<boolean> = React.createProxyState(
[this.newBoardNameInput],
() => this.newBoardNameInput.value == ""
);
// methods
createBoard = (): void => {
if (this.cannotCreateBoard.value == true) return;
const boardInfoFileContent: BoardInfoFileContent =
this.boardsAndTasksModel.createBoard(this.newBoardNameInput.value);
this.newBoardNameInput.value = "";
this.showBoardInList(boardInfoFileContent);
this.boardsAndTasksModel.updateBoardAndSend(boardInfoFileContent);
this.updateBoardIndices();
};
updateBoard = (boardInfoFileContent: BoardInfoFileContent): void => {
this.boardsAndTasksModel.updateBoardAndSend(boardInfoFileContent);
this.updateBoardIndices();
};
deleteBoard = (boardInfoFileContent: BoardInfoFileContent): void => {
this.boardsAndTasksModel.deleteBoard(boardInfoFileContent.fileId);
this.boardViewModels.remove(boardInfoFileContent.fileId);
this.updateBoardIndices();
};
// view
toggleBoardList = (): void => {
this.isShowingBoadList.value = !this.isShowingBoadList.value;
};
showBoardInList = (boardInfo: BoardInfoFileContent): void => {
const boardViewModel: BoardViewModel = new BoardViewModel(
this.coreViewModel,
this.chatViewModel,
this.storageModel,
this.boardsAndTasksModel,
this,
boardInfo
// data
boardIndexManager: IndexManager<BoardViewModel> = new IndexManager(
(boardViewModel: BoardViewModel) => boardViewModel.name.value,
);
this.boardViewModels.set(boardInfo.fileId, boardViewModel);
this.chatViewModel.taskBoardSuggestions.set(boardInfo.fileId, [
boardInfo.fileId,
this.boardsAndTasksModel.getBoardName(boardInfo.fileId),
]);
};
// paths
getBasePath = (): string[] => {
return [...this.boardsAndTasksModel.getViewPath()];
};
selectBoard = (boardViewModel: BoardViewModel): void => {
this.selectedBoardId.value = boardViewModel.boardInfo.fileId;
this.chatViewModel.displayedColor.value = boardViewModel.color.value;
getBoardViewPath = (boardId): string[] => {
return [...this.getBasePath(), boardId];
};
this.storeLastUsedBoard();
};
getLastUsedBoardPath = (): string[] => {
return [...this.getBasePath(), TaskPageViewModelSubPath.LastUsedBoard];
};
closeBoard = (): void => {
this.selectedBoardId.value = undefined;
this.chatViewModel.resetColor();
// state
newBoardNameInput: React.State<string> = new React.State("");
this.storeLastUsedBoard();
};
boardViewModels: React.MapState<BoardViewModel> = new React.MapState();
updateBoardIndices = (): void => {
this.boardIndexManager.update([...this.boardViewModels.value.values()]);
for (const boardViewModel of this.boardViewModels.value.values()) {
boardViewModel.updateIndex();
}
};
isShowingBoadList: React.State<boolean> = new React.State(true);
// storage
storeLastUsedBoard = (): void => {
const path: string[] = this.getLastUsedBoardPath();
const lastUsedBoardId: string = this.selectedBoardId.value ?? "";
this.storageModel.write(path, lastUsedBoardId);
};
selectedBoardId: React.State<string | undefined> = new React.State<any>(
undefined,
);
openLastUsedBoard = (): void => {
const path: string[] = this.getLastUsedBoardPath();
const lastUsedBoardId: string | null = this.storageModel.read(path);
if (lastUsedBoardId == null) return;
// guards
cannotCreateBoard: React.State<boolean> = React.createProxyState(
[this.newBoardNameInput],
() => this.newBoardNameInput.value == "",
);
const boardViewModel: BoardViewModel | undefined =
this.boardViewModels.value.get(lastUsedBoardId);
if (boardViewModel == undefined) return;
// methods
createBoard = (): void => {
if (this.cannotCreateBoard.value == true) return;
this.selectBoard(boardViewModel);
};
const boardInfoFileContent: BoardInfoFileContent =
this.boardsAndTasksModel.createBoard(this.newBoardNameInput.value);
this.newBoardNameInput.value = "";
// load
loadData = (): void => {
this.boardViewModels.clear();
const boardIds: string[] = this.boardsAndTasksModel.listBoardIds();
for (const boardId of boardIds) {
const boardInfo: BoardInfoFileContent | null =
this.boardsAndTasksModel.getBoardInfo(boardId);
if (boardInfo == null) continue;
this.showBoardInList(boardInfo);
}
this.updateBoardIndices();
this.openLastUsedBoard();
};
// init
constructor(
public coreViewModel: CoreViewModel,
public chatViewModel: ChatViewModel,
storageModel: StorageModel,
boardModel: BoardsAndTasksModel
) {
this.storageModel = storageModel;
this.boardsAndTasksModel = boardModel;
this.chatViewModel = chatViewModel;
// handlers
boardModel.boardHandlerManager.addHandler(
(boardInfoFileContent: BoardInfoFileContent) => {
this.showBoardInList(boardInfoFileContent);
this.boardsAndTasksModel.updateBoardAndSend(boardInfoFileContent);
this.updateBoardIndices();
}
);
}
};
updateBoard = (boardInfoFileContent: BoardInfoFileContent): void => {
this.boardsAndTasksModel.updateBoardAndSend(boardInfoFileContent);
this.updateBoardIndices();
};
deleteBoard = (boardInfoFileContent: BoardInfoFileContent): void => {
this.boardsAndTasksModel.deleteBoard(boardInfoFileContent.fileId);
this.boardViewModels.remove(boardInfoFileContent.fileId);
this.updateBoardIndices();
};
// view
toggleBoardList = (): void => {
this.isShowingBoadList.value = !this.isShowingBoadList.value;
};
showBoardInList = (boardInfo: BoardInfoFileContent): void => {
const boardViewModel: BoardViewModel = new BoardViewModel(
this.coreViewModel,
this.chatViewModel,
this.storageModel,
this.boardsAndTasksModel,
this,
boardInfo,
);
this.boardViewModels.set(boardInfo.fileId, boardViewModel);
this.chatViewModel.taskBoardSuggestions.set(boardInfo.fileId, [
boardInfo.fileId,
this.boardsAndTasksModel.getBoardName(boardInfo.fileId),
]);
};
selectBoard = (boardViewModel: BoardViewModel): void => {
this.selectedBoardId.value = boardViewModel.boardInfo.fileId;
this.chatViewModel.displayedColor.value = boardViewModel.color.value;
this.storeLastUsedBoard();
};
closeBoard = (): void => {
this.selectedBoardId.value = undefined;
this.chatViewModel.resetColor();
this.storeLastUsedBoard();
};
updateBoardIndices = (): void => {
this.boardIndexManager.update([...this.boardViewModels.value.values()]);
for (const boardViewModel of this.boardViewModels.value.values()) {
boardViewModel.updateIndex();
}
};
// storage
storeLastUsedBoard = (): void => {
const path: string[] = this.getLastUsedBoardPath();
const lastUsedBoardId: string = this.selectedBoardId.value ?? "";
this.storageModel.write(path, lastUsedBoardId);
};
openLastUsedBoard = (): void => {
const path: string[] = this.getLastUsedBoardPath();
const lastUsedBoardId: string | null = this.storageModel.read(path);
if (lastUsedBoardId == null) return;
const boardViewModel: BoardViewModel | undefined =
this.boardViewModels.value.get(lastUsedBoardId);
if (boardViewModel == undefined) return;
this.selectBoard(boardViewModel);
};
// load
loadData = (): void => {
this.boardViewModels.clear();
const boardIds: string[] = this.boardsAndTasksModel.listBoardIds();
for (const boardId of boardIds) {
const boardInfo: BoardInfoFileContent | null =
this.boardsAndTasksModel.getBoardInfo(boardId);
if (boardInfo == null) continue;
this.showBoardInList(boardInfo);
}
this.updateBoardIndices();
this.openLastUsedBoard();
};
// init
constructor(
public coreViewModel: CoreViewModel,
public chatViewModel: ChatViewModel,
storageModel: StorageModel,
boardModel: BoardsAndTasksModel,
) {
this.storageModel = storageModel;
this.boardsAndTasksModel = boardModel;
this.chatViewModel = chatViewModel;
// handlers
boardModel.boardHandlerManager.addHandler(
(boardInfoFileContent: BoardInfoFileContent) => {
this.showBoardInList(boardInfoFileContent);
this.updateBoardIndices();
},
);
}
}
export enum TaskPageViewModelSubPath {
LastUsedBoard = "last-used-board",
LastUsedBoard = "last-used-board",
}
+202 -192
View File
@@ -1,7 +1,7 @@
import * as React from "bloatless-react";
import BoardsAndTasksModel, {
TaskFileContent,
TaskFileContent,
} from "../../Model/Files/boardsAndTasksModel";
import { localeCompare, padZero } from "../../Model/Utility/utility";
@@ -11,225 +11,235 @@ import TaskContainingPageViewModel from "./taskContainingPageViewModel";
import { allowDrag } from "../../View/utility";
export default class TaskViewModel {
boardsAndTasksModel: BoardsAndTasksModel;
boardsAndTasksModel: BoardsAndTasksModel;
containingModel: TaskContainingPageViewModel;
containingModel: TaskContainingPageViewModel;
// data
task: TaskFileContent;
// data
task: TaskFileContent;
get sortingString(): string {
const splitDate: string[] = this.date.value.split("-");
const year = padZero(splitDate[0], 4);
const month = padZero(splitDate[1], 2);
const date = padZero(splitDate[2], 2);
get sortingString(): string {
const splitDate: string[] = this.date.value.split("-");
const year = padZero(splitDate[0], 4);
const month = padZero(splitDate[1], 2);
const date = padZero(splitDate[2], 2);
const splitTime: string[] = this.time.value.split(":");
const hour = padZero(splitTime[0], 2);
const minute = padZero(splitTime[1], 2);
const splitTime: string[] = this.time.value.split(":");
const hour = padZero(splitTime[0], 2);
const minute = padZero(splitTime[1], 2);
const priorityNumber = parseInt(this.priority.value);
const invertedPriority = 100 - priorityNumber;
const priorityNumber = parseInt(this.priority.value);
const invertedPriority = 100 - priorityNumber;
return (
year + month + date + hour + minute + invertedPriority + this.name.value
);
}
// paths
getFilePath = (): string[] => {
return this.boardsAndTasksModel.getTaskFilePath(this.task.fileId);
};
// state
index: React.State<number> = new React.State(0);
boardId: React.State<string> = new React.State("");
name: React.State<string> = new React.State("");
description: React.State<string> = new React.State("");
category: React.State<string> = new React.State("");
status: React.State<string> = new React.State("");
priority: React.State<string> = new React.State("");
date: React.State<string> = new React.State("");
time: React.State<string> = new React.State("");
selectedVersionId: React.State<string> = new React.State("");
versionIds: React.ListState<string> = new React.ListState();
// methods
dragStart = (event: DragEvent): void => {
allowDrag(event);
this.coreViewModel.draggedObject.value = this;
};
setCategoryAndStatus = (category?: string, status?: string): void => {
if (category != undefined) this.category.value = category;
if (status != undefined) this.status.value = status;
this.save();
};
setBoardId = (boardId: string): void => {
this.boardId.value = boardId;
this.save();
};
setDate = (dateISOString: string): void => {
this.date.value = dateISOString;
this.save();
};
// view
open = (): void => {
this.containingModel.selectTask(this);
};
close = (): void => {
this.containingModel.closeTask();
};
closeAndDiscard = (): void => {
this.close();
this.loadTaskData();
}
closeAndSave = (): void => {
this.close();
this.save();
};
updateIndex = (): void => {
const index: number = this.containingModel.taskIndexManager.getIndex(this);
this.index.value = index;
};
updateSuggestions = (): void => {
if (
this.coreViewModel.taskCategorySuggestions.value.has(
this.category.value
) == false
) {
this.coreViewModel.taskCategorySuggestions.add(this.category.value);
return (
year +
month +
date +
hour +
minute +
invertedPriority +
this.name.value
);
}
if (
this.coreViewModel.taskStatusSuggestions.value.has(this.status.value) ==
false
) {
this.coreViewModel.taskStatusSuggestions.add(this.status.value);
}
};
// paths
getFilePath = (): string[] => {
return this.boardsAndTasksModel.getTaskFilePath(this.task.fileId);
};
// settings
save = (): void => {
const newTaskFileContent: TaskFileContent =
BoardsAndTasksModel.createTaskFileContent(
this.task.fileId,
this.name.value,
this.task.boardId
);
// state
index: React.State<number> = new React.State(0);
newTaskFileContent.boardId = this.boardId.value;
newTaskFileContent.description = this.description.value;
newTaskFileContent.status = this.status.value;
newTaskFileContent.category = this.category.value;
newTaskFileContent.priority = this.priority.value;
newTaskFileContent.date = this.date.value;
newTaskFileContent.time = this.time.value;
boardId: React.State<string> = new React.State("");
this.boardsAndTasksModel.updateTaskAndSend(newTaskFileContent);
name: React.State<string> = new React.State("");
description: React.State<string> = new React.State("");
this.containingModel.showTask(newTaskFileContent);
this.containingModel.updateTaskIndices();
category: React.State<string> = new React.State("");
status: React.State<string> = new React.State("");
priority: React.State<string> = new React.State("");
this.updateSuggestions();
};
date: React.State<string> = new React.State("");
time: React.State<string> = new React.State("");
deleteTask = (): void => {
this.close();
this.boardsAndTasksModel.deleteTask(this.task.boardId, this.task.fileId);
this.containingModel.removeTaskFromView(this.task);
};
selectedVersionId: React.State<string> = new React.State("");
versionIds: React.ListState<string> = new React.ListState();
// load
loadVersionIds = (): void => {
const versionIds: string[] = this.boardsAndTasksModel.listTaskVersionIds(
this.task.fileId
);
const sortedVersionIds = versionIds.sort(localeCompare).reverse();
this.versionIds.clear();
this.versionIds.add(...sortedVersionIds);
};
// methods
dragStart = (event: DragEvent): void => {
allowDrag(event);
this.coreViewModel.draggedObject.value = this;
};
switchVersion = (versionId: string): void => {
const taskFileContent: TaskFileContent | null =
this.boardsAndTasksModel.getSpecificTaskFileContent(
this.task.fileId,
versionId
);
if (taskFileContent == null) return;
setCategoryAndStatus = (category?: string, status?: string): void => {
if (category != undefined) this.category.value = category;
if (status != undefined) this.status.value = status;
this.save();
};
this.task = taskFileContent;
this.loadTaskData();
};
setBoardId = (boardId: string): void => {
this.boardId.value = boardId;
this.save();
};
loadAllData = (): void => {
this.loadTaskData();
this.loadVersionIds();
};
setDate = (dateISOString: string): void => {
this.date.value = dateISOString;
this.save();
};
loadTaskData = (): void => {
this.boardId.value = this.task.boardId;
// view
open = (): void => {
this.containingModel.selectTask(this);
};
this.name.value = this.task.name;
this.description.value = this.task.description ?? "";
close = (): void => {
this.containingModel.closeTask();
};
this.category.value = this.task.category ?? "";
this.status.value = this.task.status ?? "";
this.priority.value = this.task.priority ?? "";
closeAndDiscard = (): void => {
this.close();
this.loadTaskData();
};
this.date.value = this.task.date ?? "";
this.time.value = this.task.time ?? "";
closeAndSave = (): void => {
this.close();
this.save();
};
this.selectedVersionId.value = this.task.fileContentId;
updateIndex = (): void => {
const index: number =
this.containingModel.taskIndexManager.getIndex(this);
this.index.value = index;
};
this.updateSuggestions();
};
updateSuggestions = (): void => {
if (
this.coreViewModel.taskCategorySuggestions.value.has(
this.category.value,
) == false
) {
this.coreViewModel.taskCategorySuggestions.add(this.category.value);
}
// init
constructor(
public coreViewModel: CoreViewModel,
public chatViewModel: ChatViewModel,
boardsAndTasksModel: BoardsAndTasksModel,
containingModel: TaskContainingPageViewModel,
taskFileContent: TaskFileContent
) {
this.boardsAndTasksModel = boardsAndTasksModel;
this.containingModel = containingModel;
if (
this.coreViewModel.taskStatusSuggestions.value.has(
this.status.value,
) == false
) {
this.coreViewModel.taskStatusSuggestions.add(this.status.value);
}
};
this.task = taskFileContent;
// settings
save = (): void => {
const newTaskFileContent: TaskFileContent =
BoardsAndTasksModel.createTaskFileContent(
this.task.fileId,
this.name.value,
this.task.boardId,
);
newTaskFileContent.boardId = this.boardId.value;
newTaskFileContent.description = this.description.value;
newTaskFileContent.status = this.status.value;
newTaskFileContent.category = this.category.value;
newTaskFileContent.priority = this.priority.value;
newTaskFileContent.date = this.date.value;
newTaskFileContent.time = this.time.value;
this.boardsAndTasksModel.updateTaskAndSend(newTaskFileContent);
this.containingModel.showTask(newTaskFileContent);
this.containingModel.updateTaskIndices();
this.updateSuggestions();
};
deleteTask = (): void => {
this.close();
this.boardsAndTasksModel.deleteTask(
this.task.boardId,
this.task.fileId,
);
this.containingModel.removeTaskFromView(this.task);
};
// load
this.loadAllData();
loadVersionIds = (): void => {
const versionIds: string[] =
this.boardsAndTasksModel.listTaskVersionIds(this.task.fileId);
const sortedVersionIds = versionIds.sort(localeCompare).reverse();
this.versionIds.clear();
this.versionIds.add(...sortedVersionIds);
};
// subscriptions
this.selectedVersionId.subscribeSilent((selectedVersionId) => {
this.switchVersion(selectedVersionId);
});
}
switchVersion = (versionId: string): void => {
const taskFileContent: TaskFileContent | null =
this.boardsAndTasksModel.getSpecificTaskFileContent(
this.task.fileId,
versionId,
);
if (taskFileContent == null) return;
// utility
static getStringsForFilter = (taskViewModel: TaskViewModel): string[] => {
return [
taskViewModel.task.name,
taskViewModel.task.description ?? "",
taskViewModel.task.category ?? "",
taskViewModel.task.status ?? "",
taskViewModel.task.priority ?? "",
taskViewModel.task.date ?? "",
taskViewModel.task.time ?? "",
];
};
this.task = taskFileContent;
this.loadTaskData();
};
loadAllData = (): void => {
this.loadTaskData();
this.loadVersionIds();
};
loadTaskData = (): void => {
this.boardId.value = this.task.boardId;
this.name.value = this.task.name;
this.description.value = this.task.description ?? "";
this.category.value = this.task.category ?? "";
this.status.value = this.task.status ?? "";
this.priority.value = this.task.priority ?? "";
this.date.value = this.task.date ?? "";
this.time.value = this.task.time ?? "";
this.selectedVersionId.value = this.task.fileContentId;
this.updateSuggestions();
};
// init
constructor(
public coreViewModel: CoreViewModel,
public chatViewModel: ChatViewModel,
boardsAndTasksModel: BoardsAndTasksModel,
containingModel: TaskContainingPageViewModel,
taskFileContent: TaskFileContent,
) {
this.boardsAndTasksModel = boardsAndTasksModel;
this.containingModel = containingModel;
this.task = taskFileContent;
// load
this.loadAllData();
// subscriptions
this.selectedVersionId.subscribeSilent((selectedVersionId) => {
this.switchVersion(selectedVersionId);
});
}
// utility
static getStringsForFilter = (taskViewModel: TaskViewModel): string[] => {
return [
taskViewModel.task.name,
taskViewModel.task.description ?? "",
taskViewModel.task.category ?? "",
taskViewModel.task.status ?? "",
taskViewModel.task.priority ?? "",
taskViewModel.task.date ?? "",
taskViewModel.task.time ?? "",
];
};
}
+66 -66
View File
@@ -3,74 +3,74 @@ import * as React from "bloatless-react";
import { checkDoesObjectMatchSearch } from "../../Model/Utility/utility";
export default class SearchViewModel<T> {
// data
allObjects: React.ListState<T> | React.MapState<T>;
getStringsOfObject: (object: T) => string[];
// data
allObjects: React.ListState<T> | React.MapState<T>;
getStringsOfObject: (object: T) => string[];
// state
appliedQuery: React.State<string> = new React.State("");
searchInput: React.State<string> = new React.State("");
matchingObjects: React.ListState<T>;
// state
appliedQuery: React.State<string> = new React.State("");
searchInput: React.State<string> = new React.State("");
matchingObjects: React.ListState<T>;
suggestions: React.ListState<string>;
suggestions: React.ListState<string>;
// guards
cannotApplySearch: React.State<boolean> = React.createProxyState(
[this.searchInput, this.appliedQuery],
() => this.searchInput.value == this.appliedQuery.value
);
// methods
search = (searchTerm: string): void => {
this.searchInput.value = searchTerm;
this.applySearch();
};
applySearch = (): void => {
this.appliedQuery.value = this.searchInput.value;
this.matchingObjects.clear();
for (const object of this.allObjects.value.values()) {
const doesMatch: boolean = this.checkDoesMatchSearch(object);
if (doesMatch == false) continue;
this.matchingObjects.add(object);
}
};
// init
constructor(
allObjects: React.ListState<T> | React.MapState<T>,
matchingObjects: React.ListState<T>,
getStringsOfObject: (object: T) => string[],
suggestions: React.ListState<string>
) {
this.allObjects = allObjects;
this.matchingObjects = matchingObjects;
this.getStringsOfObject = getStringsOfObject;
this.suggestions = suggestions;
// handle new objects
this.allObjects.handleAddition((newObject: T) => {
const doesMatch: boolean = this.checkDoesMatchSearch(newObject);
if (doesMatch == false) {
this.matchingObjects.remove(newObject);
} else {
if (this.matchingObjects.value.has(newObject)) return;
this.matchingObjects.add(newObject);
this.allObjects.handleRemoval(newObject, () => {
this.matchingObjects.remove(newObject);
});
}
});
}
// utility
checkDoesMatchSearch = (object: T): boolean => {
return checkDoesObjectMatchSearch(
this.appliedQuery.value,
this.getStringsOfObject,
object
// guards
cannotApplySearch: React.State<boolean> = React.createProxyState(
[this.searchInput, this.appliedQuery],
() => this.searchInput.value == this.appliedQuery.value,
);
};
// methods
search = (searchTerm: string): void => {
this.searchInput.value = searchTerm;
this.applySearch();
};
applySearch = (): void => {
this.appliedQuery.value = this.searchInput.value;
this.matchingObjects.clear();
for (const object of this.allObjects.value.values()) {
const doesMatch: boolean = this.checkDoesMatchSearch(object);
if (doesMatch == false) continue;
this.matchingObjects.add(object);
}
};
// init
constructor(
allObjects: React.ListState<T> | React.MapState<T>,
matchingObjects: React.ListState<T>,
getStringsOfObject: (object: T) => string[],
suggestions: React.ListState<string>,
) {
this.allObjects = allObjects;
this.matchingObjects = matchingObjects;
this.getStringsOfObject = getStringsOfObject;
this.suggestions = suggestions;
// handle new objects
this.allObjects.handleAddition((newObject: T) => {
const doesMatch: boolean = this.checkDoesMatchSearch(newObject);
if (doesMatch == false) {
this.matchingObjects.remove(newObject);
} else {
if (this.matchingObjects.value.has(newObject)) return;
this.matchingObjects.add(newObject);
this.allObjects.handleRemoval(newObject, () => {
this.matchingObjects.remove(newObject);
});
}
});
}
// utility
checkDoesMatchSearch = (object: T): boolean => {
return checkDoesObjectMatchSearch(
this.appliedQuery.value,
this.getStringsOfObject,
object,
);
};
}
@@ -3,70 +3,72 @@ import * as React from "bloatless-react";
import TaskViewModel from "../Pages/taskViewModel";
export default class TaskPropertyBulkChangeViewModel {
taskViewModels: React.ListState<TaskViewModel>;
taskViewModels: React.ListState<TaskViewModel>;
// state
inputValue: React.State<string> = new React.State("");
// state
inputValue: React.State<string> = new React.State("");
// guards
cannotSet: React.State<boolean>;
// guards
cannotSet: React.State<boolean>;
// methods
set = (): void => {
if (this.cannotSet.value == true) return;
this.taskViewModels.value.forEach((taskViewModel: TaskViewModel) => {
this.setValue(this.inputValue.value, taskViewModel);
});
};
// methods
set = (): void => {
if (this.cannotSet.value == true) return;
this.taskViewModels.value.forEach((taskViewModel: TaskViewModel) => {
this.setValue(this.inputValue.value, taskViewModel);
});
};
setValue: (newValue: string, taskViewModel: TaskViewModel) => void;
setValue: (newValue: string, taskViewModel: TaskViewModel) => void;
// init
constructor(
taskViewModels: React.ListState<TaskViewModel>,
valueSetter: (newValue: string, taskViewModel: TaskViewModel) => void,
initialValue: string
) {
this.taskViewModels = taskViewModels;
this.inputValue.value = initialValue;
// init
constructor(
taskViewModels: React.ListState<TaskViewModel>,
valueSetter: (newValue: string, taskViewModel: TaskViewModel) => void,
initialValue: string,
) {
this.taskViewModels = taskViewModels;
this.inputValue.value = initialValue;
this.setValue = valueSetter;
this.setValue = valueSetter;
this.cannotSet = React.createProxyState(
[this.inputValue],
() => this.inputValue.value == "" || this.inputValue.value == initialValue
);
}
this.cannotSet = React.createProxyState(
[this.inputValue],
() =>
this.inputValue.value == "" ||
this.inputValue.value == initialValue,
);
}
}
export class TaskCategoryBulkChangeViewModel extends TaskPropertyBulkChangeViewModel {
constructor(
taskViewModels: React.ListState<TaskViewModel>,
initialValue: string
) {
super(
taskViewModels,
(newCategory: string, taskViewModel: TaskViewModel) => {
taskViewModel.category.value = newCategory;
taskViewModel.save();
},
initialValue
);
}
constructor(
taskViewModels: React.ListState<TaskViewModel>,
initialValue: string,
) {
super(
taskViewModels,
(newCategory: string, taskViewModel: TaskViewModel) => {
taskViewModel.category.value = newCategory;
taskViewModel.save();
},
initialValue,
);
}
}
export class TaskStatusBulkChangeViewModel extends TaskPropertyBulkChangeViewModel {
constructor(
taskViewModels: React.ListState<TaskViewModel>,
initialValue: string
) {
super(
taskViewModels,
(newStatus: string, taskViewModel: TaskViewModel) => {
taskViewModel.status.value = newStatus;
taskViewModel.save();
},
initialValue
);
}
constructor(
taskViewModels: React.ListState<TaskViewModel>,
initialValue: string,
) {
super(
taskViewModels,
(newStatus: string, taskViewModel: TaskViewModel) => {
taskViewModel.status.value = newStatus;
taskViewModel.save();
},
initialValue,
);
}
}
+56 -56
View File
@@ -3,96 +3,96 @@
#home-page #chat-section,
#chat-page,
#chat-page #main > div {
animation: slide-up var(--animation-duration) ease;
animation: slide-up var(--animation-duration) ease;
}
/* Gradient */
#background-wrapper {
position: fixed;
position: fixed;
top: 0px;
left: var(--safe-area-left);
right: var(--safe-area-right);
bottom: 0px;
top: 0px;
left: var(--safe-area-left);
right: var(--safe-area-right);
bottom: 0px;
z-index: 1;
pointer-events: none;
z-index: 1;
pointer-events: none;
overflow: hidden;
overflow: hidden;
opacity: 0.3;
opacity: 0.3;
}
main {
z-index: 2;
z-index: 2;
}
#background-wrapper div {
content: "";
content: "";
position: absolute;
top: -80px;
left: -80px;
right: -80px;
bottom: -80px;
position: absolute;
top: -80px;
left: -80px;
right: -80px;
bottom: -80px;
z-index: -1;
pointer-events: none;
z-index: -1;
pointer-events: none;
filter: blur(40px);
filter: blur(40px);
}
#background-wrapper #sky {
left: -100%;
bottom: -50%;
left: -100%;
bottom: -50%;
--color-1: hsl(188, 100%, 83%);
--color-2: hsl(194, 72%, 56%);
background-image: radial-gradient(var(--color-1), var(--color-2));
--color-1: hsl(188, 100%, 83%);
--color-2: hsl(194, 72%, 56%);
background-image: radial-gradient(var(--color-1), var(--color-2));
}
#background-wrapper #grass-1 {
--color-1: hsl(150, 100%, 30%);
--color-2: hsl(150, 100%, 25%);
--color-1: hsl(150, 100%, 30%);
--color-2: hsl(150, 100%, 25%);
background-image: linear-gradient(
350deg,
var(--color-1),
var(--color-2) 30%,
transparent 40%
);
background-image: linear-gradient(
350deg,
var(--color-1),
var(--color-2) 30%,
transparent 40%
);
}
#background-wrapper #grass-2 {
--color-1: hsl(110, 100%, 40%);
--color-2: hsl(110, 100%, 35%);
--color-1: hsl(110, 100%, 40%);
--color-2: hsl(110, 100%, 35%);
background-image: linear-gradient(
9deg,
var(--color-1),
var(--color-2) 30%,
transparent 35%
);
background-image: linear-gradient(
9deg,
var(--color-1),
var(--color-2) 30%,
transparent 35%
);
}
@media (prefers-color-scheme: dark) {
#background-wrapper {
opacity: 0.2;
}
#background-wrapper {
opacity: 0.2;
}
#background-wrapper #sky {
--color-1: hsl(188, 100%, 30%);
--color-2: hsl(194, 72%, 24%);
}
#background-wrapper #sky {
--color-1: hsl(188, 100%, 30%);
--color-2: hsl(194, 72%, 24%);
}
#background-wrapper #grass-1 {
--color-1: hsl(120, 30%, 30%);
--color-2: hsl(120, 30%, 24%);
}
#background-wrapper #grass-1 {
--color-1: hsl(120, 30%, 30%);
--color-2: hsl(120, 30%, 24%);
}
#background-wrapper #grass-2 {
--color-1: hsl(110, 60%, 30%);
--color-2: hsl(110, 50%, 24%);
}
#background-wrapper #grass-2 {
--color-1: hsl(110, 60%, 30%);
--color-2: hsl(110, 50%, 24%);
}
}
+1 -1
View File
@@ -23,4 +23,4 @@
opacity: 0.1;
white-space: nowrap;
}
}
+114 -114
View File
@@ -1,202 +1,202 @@
/* basic */
[color]:not([color="standard"]) {
--primary: hsl(var(--hue), 100%, var(--accent-luma));
--primary-hover: hsl(var(--hue), 95%, var(--accent-luma));
--primary-active: hsl(var(--hue), 80%, var(--accent-luma));
--primary: hsl(var(--hue), 100%, var(--accent-luma));
--primary-hover: hsl(var(--hue), 95%, var(--accent-luma));
--primary-active: hsl(var(--hue), 80%, var(--accent-luma));
--text-on-color: hsl(0, 0%, var(--accent-text-luma));
--link: hsl(var(--hue), 100%, var(--accent-luma));
--text-on-color: hsl(0, 0%, var(--accent-text-luma));
--link: hsl(var(--hue), 100%, var(--accent-luma));
--surface: hsla(var(--hue), 80%, 30%, 5%);
--surface-hover: hsla(var(--hue), 100%, 5%, 10%);
--surface-active: hsla(var(--hue), 100%, 5%, 15%);
--surface: hsla(var(--hue), 80%, 30%, 5%);
--surface-hover: hsla(var(--hue), 100%, 5%, 10%);
--surface-active: hsla(var(--hue), 100%, 5%, 15%);
}
@media (prefers-color-scheme: dark) {
[color]:not([color="standard"]) {
--accent-luma: var(--accent-luma-dark);
--accent-text-luma: var(--accent-text-luma-dark);
[color]:not([color="standard"]) {
--accent-luma: var(--accent-luma-dark);
--accent-text-luma: var(--accent-text-luma-dark);
--surface: hsla(var(--hue), 100%, 95%, 5%);
--surface-hover: hsla(var(--hue), 100%, 95%, 10%);
--surface-active: hsla(var(--hue), 100%, 95%, 15%);
}
--surface: hsla(var(--hue), 100%, 95%, 5%);
--surface-hover: hsla(var(--hue), 100%, 95%, 10%);
--surface-active: hsla(var(--hue), 100%, 95%, 15%);
}
}
/* tile */
[color]:not([color="standard"]).tile {
--gradient-luma-1: 70%;
--gradient-luma-2: 90%;
--gradient-luma-1: 70%;
--gradient-luma-2: 90%;
--hover-luma: 75%;
--active-luma: 80%;
--hover-luma: 75%;
--active-luma: 80%;
--text-luma: 20%;
--text-luma: 20%;
--surface: linear-gradient(
120deg,
hsl(var(--hue), 100%, var(--gradient-luma-1)),
hsla(var(--hue), 100%, var(--gradient-luma-2), 90%)
);
--surface-hover: hsl(var(--hue), 100%, var(--hover-luma));
--surface-active: hsl(var(--hue), 100%, var(--active-luma));
--surface: linear-gradient(
120deg,
hsl(var(--hue), 100%, var(--gradient-luma-1)),
hsla(var(--hue), 100%, var(--gradient-luma-2), 90%)
);
--surface-hover: hsl(var(--hue), 100%, var(--hover-luma));
--surface-active: hsl(var(--hue), 100%, var(--active-luma));
--text-normal: hsl(var(--hue), 100%, var(--text-luma));
--text-normal: hsl(var(--hue), 100%, var(--text-luma));
}
@media (prefers-color-scheme: dark) {
[color]:not([color="standard"]).tile {
--gradient-luma-1: 20%;
--gradient-luma-2: 15%;
[color]:not([color="standard"]).tile {
--gradient-luma-1: 20%;
--gradient-luma-2: 15%;
--hover-luma: 25%;
--active-luma: 30%;
--hover-luma: 25%;
--active-luma: 30%;
--text-luma: 90%;
}
--text-luma: 90%;
}
}
/* fill */
[color]:not([color="standard"]).fill-color {
color: white;
background-color: var(--primary);
color: white;
background-color: var(--primary);
}
[color]:not([color="standard"])[selected].fill-color {
color: var(--primary);
color: var(--primary);
}
/* background */
@media (min-width: 1024px) {
[color]:not([color="standard"]).background {
--luma: var(--background-luma);
--sat: 80%;
background: hsl(var(--hue), var(--sat), var(--luma));
}
@media (prefers-color-scheme: dark) {
[color]:not([color="standard"]).background {
--luma: var(--background-luma-dark);
--sat: 40%;
--luma: var(--background-luma);
--sat: 80%;
background: hsl(var(--hue), var(--sat), var(--luma));
}
}
[color="standard"].background {
background: var(--surface);
}
@media (prefers-color-scheme: dark) {
[color]:not([color="standard"]).background {
--luma: var(--background-luma-dark);
--sat: 40%;
}
}
[color="standard"].background {
background: var(--surface);
}
}
/* subtle */
[color]:not([color="standard"]).subtle-background {
--luma: var(--subtle-luma);
background: hsl(var(--hue), 100%, var(--luma), 50%);
--luma: var(--subtle-luma);
background: hsl(var(--hue), 100%, var(--luma), 50%);
}
@media (prefers-color-scheme: dark) {
[color]:not([color="standard"]).subtle-background {
--luma: var(--subtle-luma-dark);
}
@media (display-mode: standalone) {
[color]:not([color="standard"]).subtle-background {
--luma: 0%;
--luma: var(--subtle-luma-dark);
}
@media (display-mode: standalone) {
[color]:not([color="standard"]).subtle-background {
--luma: 0%;
}
}
}
}
/* definitions */
[color="coral"] {
--hue: 20;
--hue: 20;
--accent-luma: 50%;
--accent-luma-dark: 70%;
--accent-luma: 50%;
--accent-luma-dark: 70%;
--accent-text-luma: 100%;
--accent-text-luma-dark: 100%;
--accent-text-luma: 100%;
--accent-text-luma-dark: 100%;
--background-luma: 90%;
--background-luma-dark: 11%;
--background-luma: 90%;
--background-luma-dark: 11%;
--subtle-luma: 95%;
--subtle-luma-dark: 5%;
--subtle-luma: 95%;
--subtle-luma-dark: 5%;
}
[color="yellow"] {
--hue: 50;
--hue: 50;
--accent-luma: 50%;
--accent-luma-dark: 60%;
--accent-luma: 50%;
--accent-luma-dark: 60%;
--accent-text-luma: 0%;
--accent-text-luma-dark: 0%;
--accent-text-luma: 0%;
--accent-text-luma-dark: 0%;
--background-luma: 85%;
--background-luma-dark: 11%;
--background-luma: 85%;
--background-luma-dark: 11%;
--subtle-luma: 95%;
--subtle-luma-dark: 5%;
--subtle-luma: 95%;
--subtle-luma-dark: 5%;
}
[color="mint"] {
--hue: 150;
--hue: 150;
--accent-luma: 40%;
--accent-luma-dark: 60%;
--accent-luma: 40%;
--accent-luma-dark: 60%;
--accent-text-luma: 100%;
--accent-text-luma-dark: 0%;
--accent-text-luma: 100%;
--accent-text-luma-dark: 0%;
--background-luma: 85%;
--background-luma-dark: 12%;
--background-luma: 85%;
--background-luma-dark: 12%;
--subtle-luma: 95%;
--subtle-luma-dark: 5%;
--subtle-luma: 95%;
--subtle-luma-dark: 5%;
}
[color="lightblue"] {
--hue: 180;
--hue: 180;
--accent-luma: 40%;
--accent-luma-dark: 60%;
--accent-luma: 40%;
--accent-luma-dark: 60%;
--accent-text-luma: 0%;
--accent-text-luma-dark: 0%;
--accent-text-luma: 0%;
--accent-text-luma-dark: 0%;
--background-luma: 85%;
--background-luma-dark: 12%;
--background-luma: 85%;
--background-luma-dark: 12%;
--subtle-luma: 95%;
--subtle-luma-dark: 5%;
--subtle-luma: 95%;
--subtle-luma-dark: 5%;
}
[color="blue"] {
--hue: 220;
--hue: 220;
--accent-luma: 50%;
--accent-luma-dark: 70%;
--accent-luma: 50%;
--accent-luma-dark: 70%;
--accent-text-luma: 100%;
--accent-text-luma-dark: 100%;
--accent-text-luma: 100%;
--accent-text-luma-dark: 100%;
--background-luma: 90%;
--background-luma-dark: 12%;
--background-luma: 90%;
--background-luma-dark: 12%;
--subtle-luma: 95%;
--subtle-luma-dark: 5%;
--subtle-luma: 95%;
--subtle-luma-dark: 5%;
}
[color="purple"] {
--hue: 280;
--hue: 280;
--accent-luma: 50%;
--accent-luma-dark: 70%;
--accent-luma: 50%;
--accent-luma-dark: 70%;
--accent-text-luma: 100%;
--accent-text-luma-dark: 100%;
--accent-text-luma: 100%;
--accent-text-luma-dark: 100%;
--background-luma: 90%;
--background-luma-dark: 11%;
--background-luma: 90%;
--background-luma-dark: 11%;
--subtle-luma: 95%;
--subtle-luma-dark: 5%;
--subtle-luma: 95%;
--subtle-luma-dark: 5%;
}
+7 -7
View File
@@ -1,9 +1,9 @@
export enum Color {
Standard = "standard",
Coral = "coral",
Yellow = "yellow",
Mint = "mint",
LightBlue = "lightblue",
Blue = "blue",
purple = "purple",
Standard = "standard",
Coral = "coral",
Yellow = "yellow",
Mint = "mint",
LightBlue = "lightblue",
Blue = "blue",
purple = "purple",
}
+34 -34
View File
@@ -27,9 +27,9 @@ const storageModel = new StorageModel();
const settingsModel = new SettingsModel(storageModel);
const connectionModel = new ConnectionModel(storageModel);
const chatListModel = new ChatListModel(
storageModel,
settingsModel,
connectionModel
storageModel,
settingsModel,
connectionModel,
);
const fileTransferModel = new FileTransferModel(storageModel, connectionModel);
@@ -42,47 +42,47 @@ const coreVieWModel = new CoreViewModel();
const storageViewModel = new StorageViewModel(coreVieWModel, storageModel);
const settingsViewModel = new SettingsViewModel(coreVieWModel, settingsModel);
const connectionViewModel = new ConnectionViewModel(
coreVieWModel,
connectionModel
coreVieWModel,
connectionModel,
);
const chatListViewModel = new ChatListViewModel(
coreVieWModel,
storageModel,
chatListModel,
settingsViewModel
coreVieWModel,
storageModel,
chatListModel,
settingsViewModel,
);
const fileTransferViewModel = new FileTransferViewModel(
fileTransferModel,
chatListModel
fileTransferModel,
chatListModel,
);
// view
chatListViewModel.selectedChat.subscribe(() => {
document.body.toggleAttribute(
"showing-chat",
chatListViewModel.selectedChat.value != undefined
);
document.body.toggleAttribute(
"showing-chat",
chatListViewModel.selectedChat.value != undefined,
);
});
document.body.append(
<div id="background-wrapper">
<div id="sky"></div>
<div id="grass-1"></div>
<div id="grass-2"></div>
</div>
<div id="background-wrapper">
<div id="sky"></div>
<div id="grass-1"></div>
<div id="grass-2"></div>
</div>,
);
document
.querySelector("main")!
.append(
HomePage(
storageViewModel,
settingsViewModel,
connectionViewModel,
fileTransferViewModel,
chatListViewModel
),
ChatPageWrapper(chatListViewModel),
ConnectionModal(connectionViewModel),
DataTransferModalWrapper(connectionViewModel, fileTransferViewModel),
StorageModal(storageViewModel)
);
.querySelector("main")!
.append(
HomePage(
storageViewModel,
settingsViewModel,
connectionViewModel,
fileTransferViewModel,
chatListViewModel,
),
ChatPageWrapper(chatListViewModel),
ConnectionModal(connectionViewModel),
DataTransferModalWrapper(connectionViewModel, fileTransferViewModel),
StorageModal(storageViewModel),
);