implement auto reconnect
This commit is contained in:
Vendored
+1
-1
@@ -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 {
|
||||
|
||||
Vendored
+389
-159
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Comms",
|
||||
"display": "standalone",
|
||||
"display": "fullscreen",
|
||||
"icons": [
|
||||
{
|
||||
"src": "icon.png",
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import ChatModel, { ChatMessage } from "./chatModel";
|
||||
import StorageModel, { StorageModelSubPath, filePaths } from "../Global/storageModel";
|
||||
import StorageModel, {
|
||||
StorageModelSubPath,
|
||||
filePaths,
|
||||
} from "../Global/storageModel";
|
||||
|
||||
import ConnectionModel from "../Global/connectionModel";
|
||||
import { Message } from "udn-frontend";
|
||||
@@ -7,104 +10,107 @@ import SettingsModel from "../Global/settingsModel";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
export default class ChatListModel {
|
||||
storageModel: StorageModel;
|
||||
settingsModel: SettingsModel;
|
||||
connectionModel: ConnectionModel;
|
||||
storageModel: StorageModel;
|
||||
settingsModel: SettingsModel;
|
||||
connectionModel: ConnectionModel;
|
||||
|
||||
// data
|
||||
chatModels = new Set<ChatModel>();
|
||||
// data
|
||||
chatModels = new Set<ChatModel>();
|
||||
|
||||
// handlers
|
||||
messageHandler = (data: Message): void => {
|
||||
const channel: string | undefined = data.messageChannel;
|
||||
const body: string | undefined = data.messageBody;
|
||||
if (channel == undefined) return;
|
||||
if (body == undefined) return;
|
||||
// handlers
|
||||
messageHandler = (data: Message): void => {
|
||||
const channel: string | undefined = data.messageChannel;
|
||||
const body: string | undefined = data.messageBody;
|
||||
if (channel == undefined) return;
|
||||
if (body == undefined) return;
|
||||
|
||||
this.routeMessageToCorrectChatModel(channel, (chatModel: ChatModel) =>
|
||||
chatModel.handleMessage(body)
|
||||
);
|
||||
};
|
||||
this.routeMessageToCorrectChatModel(channel, (chatModel: ChatModel) =>
|
||||
chatModel.handleMessage(body),
|
||||
);
|
||||
};
|
||||
|
||||
messageSentHandler = (chatMessage: ChatMessage): void => {
|
||||
const channel: string = chatMessage.channel;
|
||||
messageSentHandler = (chatMessage: ChatMessage): void => {
|
||||
const channel: string = chatMessage.channel;
|
||||
|
||||
this.routeMessageToCorrectChatModel(channel, (chatModel: ChatModel) =>
|
||||
chatModel.handleMessageSent(chatMessage)
|
||||
);
|
||||
};
|
||||
this.routeMessageToCorrectChatModel(channel, (chatModel: ChatModel) =>
|
||||
chatModel.handleMessageSent(chatMessage),
|
||||
);
|
||||
};
|
||||
|
||||
// methods
|
||||
routeMessageToCorrectChatModel = (
|
||||
channel: string,
|
||||
fn: (chatModel: ChatModel) => void
|
||||
): void => {
|
||||
const allChannels = channel.split("/");
|
||||
// methods
|
||||
routeMessageToCorrectChatModel = (
|
||||
channel: string,
|
||||
fn: (chatModel: ChatModel) => void,
|
||||
): void => {
|
||||
const allChannels = channel.split("/");
|
||||
|
||||
for (const chatModel of this.chatModels) {
|
||||
for (const channel of allChannels) {
|
||||
if (channel != chatModel.info.primaryChannel) continue;
|
||||
fn(chatModel);
|
||||
break;
|
||||
}
|
||||
for (const chatModel of this.chatModels) {
|
||||
for (const channel of allChannels) {
|
||||
if (channel != chatModel.info.primaryChannel) continue;
|
||||
fn(chatModel);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// storage
|
||||
addChatModel = (chatModel: ChatModel) => {
|
||||
this.chatModels.add(chatModel);
|
||||
};
|
||||
|
||||
createChat = (primaryChannel: string): ChatModel => {
|
||||
const id: string = v4();
|
||||
|
||||
const chatModel = new ChatModel(
|
||||
this.storageModel,
|
||||
this.connectionModel,
|
||||
this.settingsModel,
|
||||
this,
|
||||
id,
|
||||
);
|
||||
chatModel.setPrimaryChannel(primaryChannel);
|
||||
|
||||
this.addChatModel(chatModel);
|
||||
return chatModel;
|
||||
};
|
||||
|
||||
untrackChat = (chat: ChatModel): void => {
|
||||
this.chatModels.delete(chat);
|
||||
};
|
||||
|
||||
// load
|
||||
loadChats = (): void => {
|
||||
const chatDir = StorageModel.getPath(
|
||||
StorageModelSubPath.Chat,
|
||||
filePaths.chat.base,
|
||||
);
|
||||
const chatIds = this.storageModel.list(chatDir);
|
||||
for (const chatId of chatIds) {
|
||||
const chatModel = new ChatModel(
|
||||
this.storageModel,
|
||||
this.connectionModel,
|
||||
this.settingsModel,
|
||||
this,
|
||||
chatId,
|
||||
);
|
||||
this.addChatModel(chatModel);
|
||||
}
|
||||
};
|
||||
|
||||
// init
|
||||
constructor(
|
||||
storageModel: StorageModel,
|
||||
settingsModel: SettingsModel,
|
||||
connectionModel: ConnectionModel,
|
||||
) {
|
||||
this.storageModel = storageModel;
|
||||
this.settingsModel = settingsModel;
|
||||
this.connectionModel = connectionModel;
|
||||
this.loadChats();
|
||||
|
||||
connectionModel.messageHandlerManager.addHandler(this.messageHandler);
|
||||
connectionModel.messageSentHandlerManager.addHandler(
|
||||
this.messageSentHandler,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// storage
|
||||
addChatModel = (chatModel: ChatModel) => {
|
||||
this.chatModels.add(chatModel);
|
||||
};
|
||||
|
||||
createChat = (primaryChannel: string): ChatModel => {
|
||||
const id: string = v4();
|
||||
|
||||
const chatModel = new ChatModel(
|
||||
this.storageModel,
|
||||
this.connectionModel,
|
||||
this.settingsModel,
|
||||
this,
|
||||
id
|
||||
);
|
||||
chatModel.setPrimaryChannel(primaryChannel);
|
||||
|
||||
this.addChatModel(chatModel);
|
||||
return chatModel;
|
||||
};
|
||||
|
||||
untrackChat = (chat: ChatModel): void => {
|
||||
this.chatModels.delete(chat);
|
||||
};
|
||||
|
||||
// load
|
||||
loadChats = (): void => {
|
||||
const chatDir = StorageModel.getPath(StorageModelSubPath.Chat, filePaths.chat.base);
|
||||
const chatIds = this.storageModel.list(chatDir);
|
||||
for (const chatId of chatIds) {
|
||||
const chatModel = new ChatModel(
|
||||
this.storageModel,
|
||||
this.connectionModel,
|
||||
this.settingsModel,
|
||||
this,
|
||||
chatId
|
||||
);
|
||||
this.addChatModel(chatModel);
|
||||
}
|
||||
};
|
||||
|
||||
// init
|
||||
constructor(
|
||||
storageModel: StorageModel,
|
||||
settingsModel: SettingsModel,
|
||||
connectionModel: ConnectionModel
|
||||
) {
|
||||
this.storageModel = storageModel;
|
||||
this.settingsModel = settingsModel;
|
||||
this.connectionModel = connectionModel;
|
||||
this.loadChats();
|
||||
|
||||
connectionModel.messageHandlerManager.addHandler(this.messageHandler);
|
||||
connectionModel.messageSentHandlerManager.addHandler(
|
||||
this.messageSentHandler
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+321
-308
@@ -3,15 +3,15 @@
|
||||
import { DATA_VERSION, ValidObject } from "../Utility/typeSafety";
|
||||
import FileModel, { FileContent } from "../Files/fileModel";
|
||||
import {
|
||||
HandlerManager,
|
||||
createTimestamp,
|
||||
localeCompare,
|
||||
parseValidObject,
|
||||
stringify,
|
||||
HandlerManager,
|
||||
createTimestamp,
|
||||
localeCompare,
|
||||
parseValidObject,
|
||||
stringify,
|
||||
} from "../Utility/utility";
|
||||
import StorageModel, {
|
||||
StorageModelSubPath,
|
||||
filePaths,
|
||||
StorageModelSubPath,
|
||||
filePaths,
|
||||
} from "../Global/storageModel";
|
||||
import { decryptString, encryptString } from "../Utility/crypto";
|
||||
|
||||
@@ -22,345 +22,358 @@ import SettingsModel from "../Global/settingsModel";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
export default class ChatModel {
|
||||
connectionModel: ConnectionModel;
|
||||
storageModel: StorageModel;
|
||||
settingsModel: SettingsModel;
|
||||
chatListModel: ChatListModel;
|
||||
connectionModel: ConnectionModel;
|
||||
storageModel: StorageModel;
|
||||
settingsModel: SettingsModel;
|
||||
chatListModel: ChatListModel;
|
||||
|
||||
fileModel: FileModel;
|
||||
fileModel: FileModel;
|
||||
|
||||
// data
|
||||
id: string;
|
||||
info: ChatInfo;
|
||||
color: Color;
|
||||
// data
|
||||
id: string;
|
||||
info: ChatInfo;
|
||||
color: Color;
|
||||
|
||||
chatMessageHandlerManager: HandlerManager<ChatMessage> = new HandlerManager();
|
||||
chatMessageHandlerManager: HandlerManager<ChatMessage> =
|
||||
new HandlerManager();
|
||||
|
||||
get secondaryChannels(): string[] {
|
||||
return this.info.secondaryChannels.sort(localeCompare);
|
||||
}
|
||||
|
||||
// paths
|
||||
getBasePath = (): string[] => {
|
||||
return StorageModel.getPath(
|
||||
StorageModelSubPath.Chat,
|
||||
filePaths.chat.chatBase(this.id)
|
||||
);
|
||||
};
|
||||
|
||||
getInfoPath = (): string[] => {
|
||||
return StorageModel.getPath(
|
||||
StorageModelSubPath.Chat,
|
||||
filePaths.chat.info(this.id)
|
||||
);
|
||||
};
|
||||
|
||||
getColorPath = (): string[] => {
|
||||
return StorageModel.getPath(
|
||||
StorageModelSubPath.Chat,
|
||||
filePaths.chat.color(this.id)
|
||||
);
|
||||
};
|
||||
|
||||
getMessageDirPath = (): string[] => {
|
||||
return StorageModel.getPath(
|
||||
StorageModelSubPath.Chat,
|
||||
filePaths.chat.messages(this.id)
|
||||
);
|
||||
};
|
||||
|
||||
getMessagePath = (id: string): string[] => {
|
||||
return [...this.getMessageDirPath(), id];
|
||||
};
|
||||
|
||||
// handlers
|
||||
handleMessage = (body: string): void => {
|
||||
const chatMessage: ChatMessage | null = parseValidObject(
|
||||
body,
|
||||
ChatMessageReference
|
||||
);
|
||||
if (chatMessage == null) return;
|
||||
|
||||
chatMessage.status = ChatMessageStatus.Received;
|
||||
this.addMessage(chatMessage);
|
||||
|
||||
this.info.hasUnreadMessages = true;
|
||||
this.storeInfo();
|
||||
};
|
||||
|
||||
handleMessageSent = (chatMessage: ChatMessage): void => {
|
||||
chatMessage.status = ChatMessageStatus.Sent;
|
||||
this.addMessage(chatMessage);
|
||||
};
|
||||
|
||||
// settings
|
||||
setPrimaryChannel = (primaryChannel: string): void => {
|
||||
this.info.primaryChannel = primaryChannel;
|
||||
this.storeInfo();
|
||||
this.subscribe();
|
||||
};
|
||||
|
||||
setSecondaryChannels = (secondaryChannels: string[]): void => {
|
||||
this.info.secondaryChannels = secondaryChannels;
|
||||
this.storeInfo();
|
||||
};
|
||||
|
||||
setEncryptionKey = (key: string): void => {
|
||||
this.info.encryptionKey = key;
|
||||
this.storeInfo();
|
||||
};
|
||||
|
||||
setColor = (color: Color): void => {
|
||||
this.color = color;
|
||||
this.storeColor();
|
||||
};
|
||||
|
||||
// messaging
|
||||
addMessage = async (chatMessage: ChatMessage): Promise<void> => {
|
||||
await this.decryptMessage(chatMessage);
|
||||
|
||||
// message
|
||||
if (chatMessage.body != "") {
|
||||
const messagePath: string[] = this.getMessagePath(chatMessage.id);
|
||||
this.storageModel.writeStringifiable(messagePath, chatMessage);
|
||||
this.chatMessageHandlerManager.trigger(chatMessage);
|
||||
get secondaryChannels(): string[] {
|
||||
return this.info.secondaryChannels.sort(localeCompare);
|
||||
}
|
||||
|
||||
// file
|
||||
this.fileModel.handleStringifiedFileContent(chatMessage.stringifiedFile);
|
||||
};
|
||||
|
||||
sendMessage = async (
|
||||
body: string,
|
||||
fileContent?: FileContent<string>
|
||||
): Promise<boolean> => {
|
||||
const senderName = this.settingsModel.username;
|
||||
if (senderName == "") return false;
|
||||
|
||||
const allChannels = [this.info.primaryChannel];
|
||||
for (const secondaryChannel of this.info.secondaryChannels) {
|
||||
allChannels.push(secondaryChannel);
|
||||
}
|
||||
|
||||
const combinedChannel: string = allChannels.join("/");
|
||||
|
||||
const chatMessage: ChatMessage = await ChatModel.createChatMessage(
|
||||
combinedChannel,
|
||||
senderName,
|
||||
this.info.encryptionKey,
|
||||
body,
|
||||
fileContent
|
||||
);
|
||||
|
||||
this.addMessage(chatMessage);
|
||||
this.connectionModel.sendMessageOrStore(chatMessage);
|
||||
return true;
|
||||
};
|
||||
|
||||
decryptMessage = async (chatMessage: ChatMessage): Promise<void> => {
|
||||
const decryptedBody: string = await decryptString(
|
||||
chatMessage.body,
|
||||
this.info.encryptionKey
|
||||
);
|
||||
const decryptedFile: string = await decryptString(
|
||||
chatMessage.stringifiedFile ?? "",
|
||||
this.info.encryptionKey
|
||||
);
|
||||
chatMessage.body = decryptedBody;
|
||||
chatMessage.stringifiedFile = decryptedFile;
|
||||
};
|
||||
|
||||
subscribe = (): void => {
|
||||
this.connectionModel.addChannel(this.info.primaryChannel);
|
||||
};
|
||||
|
||||
markRead = (): void => {
|
||||
this.info.hasUnreadMessages = false;
|
||||
this.storeInfo();
|
||||
}
|
||||
|
||||
// storage
|
||||
storeInfo = (): void => {
|
||||
this.storageModel.writeStringifiable(this.getInfoPath(), this.info);
|
||||
};
|
||||
|
||||
storeColor = (): void => {
|
||||
this.storageModel.write(this.getColorPath(), this.color);
|
||||
};
|
||||
|
||||
delete = () => {
|
||||
// untrack
|
||||
this.chatListModel.untrackChat(this);
|
||||
|
||||
// delete
|
||||
const dirPath: string[] = this.getBasePath();
|
||||
this.storageModel.removeRecursively(dirPath);
|
||||
};
|
||||
|
||||
// load
|
||||
loadInfo = (): void => {
|
||||
const info: ChatInfo | null = this.storageModel.readStringifiable(
|
||||
this.getInfoPath(),
|
||||
ChatInfoReference
|
||||
);
|
||||
if (info != null) {
|
||||
this.info = info;
|
||||
} else {
|
||||
this.info = ChatModel.generateChatInfo("0");
|
||||
}
|
||||
};
|
||||
|
||||
loadColor = (): void => {
|
||||
const path: string[] = this.getColorPath();
|
||||
const color: string | null = this.storageModel.read(path);
|
||||
if (!color) {
|
||||
this.color = Color.Standard;
|
||||
} else {
|
||||
this.color = color as any;
|
||||
}
|
||||
};
|
||||
|
||||
get messages(): ChatMessage[] {
|
||||
const messageIds: string[] = this.storageModel.list(
|
||||
this.getMessageDirPath()
|
||||
);
|
||||
if (!Array.isArray(messageIds)) return [];
|
||||
|
||||
const chatMessages: ChatMessage[] = [];
|
||||
for (const messageId of messageIds) {
|
||||
const messagePath: string[] = this.getMessagePath(messageId);
|
||||
const chatMessage: ChatMessage | any =
|
||||
this.storageModel.readStringifiable(messagePath, ChatMessageReference);
|
||||
chatMessages.push(chatMessage);
|
||||
}
|
||||
|
||||
const sorted = chatMessages.sort((a, b) =>
|
||||
a.dateSent.localeCompare(b.dateSent)
|
||||
);
|
||||
return sorted;
|
||||
}
|
||||
|
||||
// init
|
||||
constructor(
|
||||
storageModel: StorageModel,
|
||||
connectionModel: ConnectionModel,
|
||||
settingsModel: SettingsModel,
|
||||
chatListModel: ChatListModel,
|
||||
chatId: string
|
||||
) {
|
||||
this.id = chatId;
|
||||
this.connectionModel = connectionModel;
|
||||
this.settingsModel = settingsModel;
|
||||
this.storageModel = storageModel;
|
||||
this.chatListModel = chatListModel;
|
||||
|
||||
this.loadInfo();
|
||||
this.loadColor();
|
||||
this.subscribe();
|
||||
|
||||
this.fileModel = new FileModel(this.storageModel, this.settingsModel, this);
|
||||
}
|
||||
|
||||
// utility
|
||||
static generateChatInfo = (primaryChannel: string): ChatInfo => {
|
||||
return {
|
||||
dataVersion: DATA_VERSION,
|
||||
|
||||
primaryChannel,
|
||||
secondaryChannels: [],
|
||||
encryptionKey: "",
|
||||
hasUnreadMessages: false,
|
||||
// paths
|
||||
getBasePath = (): string[] => {
|
||||
return StorageModel.getPath(
|
||||
StorageModelSubPath.Chat,
|
||||
filePaths.chat.chatBase(this.id),
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
static createChatMessage = async (
|
||||
channel: string,
|
||||
sender: string,
|
||||
encryptionKey: string,
|
||||
body: string,
|
||||
fileContent?: FileContent<string>
|
||||
): Promise<ChatMessage> => {
|
||||
const chatMessage: ChatMessage = {
|
||||
dataVersion: DATA_VERSION,
|
||||
|
||||
id: v4(),
|
||||
|
||||
channel,
|
||||
sender,
|
||||
body,
|
||||
dateSent: createTimestamp(),
|
||||
|
||||
status: ChatMessageStatus.Outbox,
|
||||
stringifiedFile: "",
|
||||
getInfoPath = (): string[] => {
|
||||
return StorageModel.getPath(
|
||||
StorageModelSubPath.Chat,
|
||||
filePaths.chat.info(this.id),
|
||||
);
|
||||
};
|
||||
if (fileContent != undefined) {
|
||||
const stringifiedFile = stringify(fileContent);
|
||||
chatMessage.stringifiedFile = stringifiedFile;
|
||||
|
||||
getColorPath = (): string[] => {
|
||||
return StorageModel.getPath(
|
||||
StorageModelSubPath.Chat,
|
||||
filePaths.chat.color(this.id),
|
||||
);
|
||||
};
|
||||
|
||||
getMessageDirPath = (): string[] => {
|
||||
return StorageModel.getPath(
|
||||
StorageModelSubPath.Chat,
|
||||
filePaths.chat.messages(this.id),
|
||||
);
|
||||
};
|
||||
|
||||
getMessagePath = (id: string): string[] => {
|
||||
return [...this.getMessageDirPath(), id];
|
||||
};
|
||||
|
||||
// handlers
|
||||
handleMessage = (body: string): void => {
|
||||
const chatMessage: ChatMessage | null = parseValidObject(
|
||||
body,
|
||||
ChatMessageReference,
|
||||
);
|
||||
if (chatMessage == null) return;
|
||||
|
||||
chatMessage.status = ChatMessageStatus.Received;
|
||||
this.addMessage(chatMessage);
|
||||
|
||||
this.info.hasUnreadMessages = true;
|
||||
this.storeInfo();
|
||||
};
|
||||
|
||||
handleMessageSent = (chatMessage: ChatMessage): void => {
|
||||
chatMessage.status = ChatMessageStatus.Sent;
|
||||
this.addMessage(chatMessage);
|
||||
};
|
||||
|
||||
// settings
|
||||
setPrimaryChannel = (primaryChannel: string): void => {
|
||||
this.info.primaryChannel = primaryChannel;
|
||||
this.storeInfo();
|
||||
this.subscribe();
|
||||
};
|
||||
|
||||
setSecondaryChannels = (secondaryChannels: string[]): void => {
|
||||
this.info.secondaryChannels = secondaryChannels;
|
||||
this.storeInfo();
|
||||
};
|
||||
|
||||
setEncryptionKey = (key: string): void => {
|
||||
this.info.encryptionKey = key;
|
||||
this.storeInfo();
|
||||
};
|
||||
|
||||
setColor = (color: Color): void => {
|
||||
this.color = color;
|
||||
this.storeColor();
|
||||
};
|
||||
|
||||
// messaging
|
||||
addMessage = async (chatMessage: ChatMessage): Promise<void> => {
|
||||
await this.decryptMessage(chatMessage);
|
||||
|
||||
// message
|
||||
if (chatMessage.body != "") {
|
||||
const messagePath: string[] = this.getMessagePath(chatMessage.id);
|
||||
this.storageModel.writeStringifiable(messagePath, chatMessage);
|
||||
this.chatMessageHandlerManager.trigger(chatMessage);
|
||||
}
|
||||
|
||||
// file
|
||||
this.fileModel.handleStringifiedFileContent(
|
||||
chatMessage.stringifiedFile,
|
||||
);
|
||||
};
|
||||
|
||||
sendMessage = async (
|
||||
body: string,
|
||||
fileContent?: FileContent<string>,
|
||||
): Promise<boolean> => {
|
||||
const senderName = this.settingsModel.username;
|
||||
if (senderName == "") return false;
|
||||
|
||||
const allChannels = [this.info.primaryChannel];
|
||||
for (const secondaryChannel of this.info.secondaryChannels) {
|
||||
allChannels.push(secondaryChannel);
|
||||
}
|
||||
|
||||
const combinedChannel: string = allChannels.join("/");
|
||||
|
||||
const chatMessage: ChatMessage = await ChatModel.createChatMessage(
|
||||
combinedChannel,
|
||||
senderName,
|
||||
this.info.encryptionKey,
|
||||
body,
|
||||
fileContent,
|
||||
);
|
||||
|
||||
this.addMessage(chatMessage);
|
||||
this.connectionModel.sendMessageOrStore(chatMessage);
|
||||
return true;
|
||||
};
|
||||
|
||||
decryptMessage = async (chatMessage: ChatMessage): Promise<void> => {
|
||||
const decryptedBody: string = await decryptString(
|
||||
chatMessage.body,
|
||||
this.info.encryptionKey,
|
||||
);
|
||||
const decryptedFile: string = await decryptString(
|
||||
chatMessage.stringifiedFile ?? "",
|
||||
this.info.encryptionKey,
|
||||
);
|
||||
chatMessage.body = decryptedBody;
|
||||
chatMessage.stringifiedFile = decryptedFile;
|
||||
};
|
||||
|
||||
subscribe = (): void => {
|
||||
this.connectionModel.addChannel(this.info.primaryChannel);
|
||||
};
|
||||
|
||||
markRead = (): void => {
|
||||
this.info.hasUnreadMessages = false;
|
||||
this.storeInfo();
|
||||
};
|
||||
|
||||
// storage
|
||||
storeInfo = (): void => {
|
||||
this.storageModel.writeStringifiable(this.getInfoPath(), this.info);
|
||||
};
|
||||
|
||||
storeColor = (): void => {
|
||||
this.storageModel.write(this.getColorPath(), this.color);
|
||||
};
|
||||
|
||||
delete = () => {
|
||||
// untrack
|
||||
this.chatListModel.untrackChat(this);
|
||||
|
||||
// delete
|
||||
const dirPath: string[] = this.getBasePath();
|
||||
this.storageModel.removeRecursively(dirPath);
|
||||
};
|
||||
|
||||
// load
|
||||
loadInfo = (): void => {
|
||||
const info: ChatInfo | null = this.storageModel.readStringifiable(
|
||||
this.getInfoPath(),
|
||||
ChatInfoReference,
|
||||
);
|
||||
if (info != null) {
|
||||
this.info = info;
|
||||
} else {
|
||||
this.info = ChatModel.generateChatInfo("0");
|
||||
}
|
||||
};
|
||||
|
||||
loadColor = (): void => {
|
||||
const path: string[] = this.getColorPath();
|
||||
const color: string | null = this.storageModel.read(path);
|
||||
if (!color) {
|
||||
this.color = Color.Standard;
|
||||
} else {
|
||||
this.color = color as any;
|
||||
}
|
||||
};
|
||||
|
||||
get messages(): ChatMessage[] {
|
||||
const messageIds: string[] = this.storageModel.list(
|
||||
this.getMessageDirPath(),
|
||||
);
|
||||
if (!Array.isArray(messageIds)) return [];
|
||||
|
||||
const chatMessages: ChatMessage[] = [];
|
||||
for (const messageId of messageIds) {
|
||||
const messagePath: string[] = this.getMessagePath(messageId);
|
||||
const chatMessage: ChatMessage | any =
|
||||
this.storageModel.readStringifiable(
|
||||
messagePath,
|
||||
ChatMessageReference,
|
||||
);
|
||||
chatMessages.push(chatMessage);
|
||||
}
|
||||
|
||||
const sorted = chatMessages.sort((a, b) =>
|
||||
a.dateSent.localeCompare(b.dateSent),
|
||||
);
|
||||
return sorted;
|
||||
}
|
||||
|
||||
if (encryptionKey != "") {
|
||||
chatMessage.body = await encryptString(chatMessage.body, encryptionKey);
|
||||
chatMessage.stringifiedFile = await encryptString(
|
||||
chatMessage.stringifiedFile,
|
||||
encryptionKey
|
||||
);
|
||||
// init
|
||||
constructor(
|
||||
storageModel: StorageModel,
|
||||
connectionModel: ConnectionModel,
|
||||
settingsModel: SettingsModel,
|
||||
chatListModel: ChatListModel,
|
||||
chatId: string,
|
||||
) {
|
||||
this.id = chatId;
|
||||
this.connectionModel = connectionModel;
|
||||
this.settingsModel = settingsModel;
|
||||
this.storageModel = storageModel;
|
||||
this.chatListModel = chatListModel;
|
||||
|
||||
this.loadInfo();
|
||||
this.loadColor();
|
||||
this.subscribe();
|
||||
|
||||
this.fileModel = new FileModel(
|
||||
this.storageModel,
|
||||
this.settingsModel,
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
return chatMessage;
|
||||
};
|
||||
// utility
|
||||
static generateChatInfo = (primaryChannel: string): ChatInfo => {
|
||||
return {
|
||||
dataVersion: DATA_VERSION,
|
||||
|
||||
primaryChannel,
|
||||
secondaryChannels: [],
|
||||
encryptionKey: "",
|
||||
hasUnreadMessages: false,
|
||||
};
|
||||
};
|
||||
|
||||
static createChatMessage = async (
|
||||
channel: string,
|
||||
sender: string,
|
||||
encryptionKey: string,
|
||||
body: string,
|
||||
fileContent?: FileContent<string>,
|
||||
): Promise<ChatMessage> => {
|
||||
const chatMessage: ChatMessage = {
|
||||
dataVersion: DATA_VERSION,
|
||||
|
||||
id: v4(),
|
||||
|
||||
channel,
|
||||
sender,
|
||||
body,
|
||||
dateSent: createTimestamp(),
|
||||
|
||||
status: ChatMessageStatus.Outbox,
|
||||
stringifiedFile: "",
|
||||
};
|
||||
if (fileContent != undefined) {
|
||||
const stringifiedFile = stringify(fileContent);
|
||||
chatMessage.stringifiedFile = stringifiedFile;
|
||||
}
|
||||
|
||||
if (encryptionKey != "") {
|
||||
chatMessage.body = await encryptString(
|
||||
chatMessage.body,
|
||||
encryptionKey,
|
||||
);
|
||||
chatMessage.stringifiedFile = await encryptString(
|
||||
chatMessage.stringifiedFile,
|
||||
encryptionKey,
|
||||
);
|
||||
}
|
||||
|
||||
return chatMessage;
|
||||
};
|
||||
}
|
||||
|
||||
// types
|
||||
export interface ChatInfo extends ValidObject {
|
||||
primaryChannel: string;
|
||||
secondaryChannels: string[];
|
||||
encryptionKey: string;
|
||||
primaryChannel: string;
|
||||
secondaryChannels: string[];
|
||||
encryptionKey: string;
|
||||
|
||||
hasUnreadMessages: boolean;
|
||||
hasUnreadMessages: boolean;
|
||||
}
|
||||
|
||||
export enum ChatMessageStatus {
|
||||
Outbox = "outbox",
|
||||
Sent = "sent",
|
||||
Received = "received",
|
||||
Other = "other",
|
||||
Outbox = "outbox",
|
||||
Sent = "sent",
|
||||
Received = "received",
|
||||
Other = "other",
|
||||
}
|
||||
|
||||
export interface ChatMessage extends ValidObject {
|
||||
id: string;
|
||||
id: string;
|
||||
|
||||
channel: string;
|
||||
sender: string;
|
||||
body: string;
|
||||
dateSent: string;
|
||||
channel: string;
|
||||
sender: string;
|
||||
body: string;
|
||||
dateSent: string;
|
||||
|
||||
status: ChatMessageStatus;
|
||||
status: ChatMessageStatus;
|
||||
|
||||
stringifiedFile: string;
|
||||
stringifiedFile: string;
|
||||
}
|
||||
|
||||
// references
|
||||
export const ChatInfoReference: ChatInfo = {
|
||||
dataVersion: DATA_VERSION,
|
||||
dataVersion: DATA_VERSION,
|
||||
|
||||
primaryChannel: "",
|
||||
secondaryChannels: [""],
|
||||
encryptionKey: "",
|
||||
primaryChannel: "",
|
||||
secondaryChannels: [""],
|
||||
encryptionKey: "",
|
||||
|
||||
hasUnreadMessages: true,
|
||||
hasUnreadMessages: true,
|
||||
};
|
||||
|
||||
export const ChatMessageReference: ChatMessage = {
|
||||
dataVersion: DATA_VERSION,
|
||||
dataVersion: DATA_VERSION,
|
||||
|
||||
id: "",
|
||||
id: "",
|
||||
|
||||
channel: "",
|
||||
sender: "",
|
||||
body: "",
|
||||
dateSent: "",
|
||||
channel: "",
|
||||
sender: "",
|
||||
body: "",
|
||||
dateSent: "",
|
||||
|
||||
status: "" as ChatMessageStatus,
|
||||
status: "" as ChatMessageStatus,
|
||||
|
||||
stringifiedFile: "",
|
||||
stringifiedFile: "",
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// this file is responsible for managing boards within chats and tasks within boards.
|
||||
|
||||
import {
|
||||
DATA_VERSION,
|
||||
checkMatchesObjectStructure,
|
||||
DATA_VERSION,
|
||||
checkMatchesObjectStructure,
|
||||
} from "../Utility/typeSafety";
|
||||
import FileModel, { FileContent, FileModelSubPath } from "./fileModel";
|
||||
|
||||
@@ -15,316 +15,324 @@ import StorageModel from "../Global/storageModel";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
export default class BoardsAndTasksModel {
|
||||
storageModel: StorageModel;
|
||||
settingsModel: SettingsModel;
|
||||
chatModel: ChatModel;
|
||||
fileModel: FileModel;
|
||||
storageModel: StorageModel;
|
||||
settingsModel: SettingsModel;
|
||||
chatModel: ChatModel;
|
||||
fileModel: FileModel;
|
||||
|
||||
calendarModel: CalendarModel;
|
||||
calendarModel: CalendarModel;
|
||||
|
||||
// data
|
||||
boardHandlerManager: HandlerManager<BoardInfoFileContent> =
|
||||
new HandlerManager();
|
||||
taskHandlerManager: HandlerManager<TaskFileContent> = new HandlerManager();
|
||||
// data
|
||||
boardHandlerManager: HandlerManager<BoardInfoFileContent> =
|
||||
new HandlerManager();
|
||||
taskHandlerManager: HandlerManager<TaskFileContent> = new HandlerManager();
|
||||
|
||||
// paths
|
||||
getBasePath = (): string[] => {
|
||||
return this.fileModel.getModelContainerPath(FileModelSubPath.ModelTask);
|
||||
};
|
||||
// paths
|
||||
getBasePath = (): string[] => {
|
||||
return this.fileModel.getModelContainerPath(FileModelSubPath.ModelTask);
|
||||
};
|
||||
|
||||
getViewPath = (): string[] => {
|
||||
return [...this.getBasePath(), FileModelSubPath.ModelView];
|
||||
};
|
||||
getViewPath = (): string[] => {
|
||||
return [...this.getBasePath(), FileModelSubPath.ModelView];
|
||||
};
|
||||
|
||||
getBoardFilePath = (boardId: string): string[] => {
|
||||
return [...this.fileModel.getFilePath(boardId)];
|
||||
};
|
||||
getBoardFilePath = (boardId: string): string[] => {
|
||||
return [...this.fileModel.getFilePath(boardId)];
|
||||
};
|
||||
|
||||
getTaskFilePath = (taskId: string): string[] => {
|
||||
return [...this.fileModel.getFilePath(taskId)];
|
||||
};
|
||||
getTaskFilePath = (taskId: string): string[] => {
|
||||
return [...this.fileModel.getFilePath(taskId)];
|
||||
};
|
||||
|
||||
getBoardContainerPath = (): string[] => {
|
||||
return [...this.getBasePath(), TaskModelSubPaths.Boards];
|
||||
};
|
||||
getBoardContainerPath = (): string[] => {
|
||||
return [...this.getBasePath(), TaskModelSubPaths.Boards];
|
||||
};
|
||||
|
||||
getBoardDirectoryPath = (boardId: string): string[] => {
|
||||
return [...this.getBoardContainerPath(), boardId];
|
||||
};
|
||||
getBoardDirectoryPath = (boardId: string): string[] => {
|
||||
return [...this.getBoardContainerPath(), boardId];
|
||||
};
|
||||
|
||||
getTaskContainerPath = (boardId: string): string[] => {
|
||||
return [
|
||||
...this.getBoardDirectoryPath(boardId),
|
||||
TaskModelSubPaths.BoardTasks,
|
||||
];
|
||||
};
|
||||
getTaskContainerPath = (boardId: string): string[] => {
|
||||
return [
|
||||
...this.getBoardDirectoryPath(boardId),
|
||||
TaskModelSubPaths.BoardTasks,
|
||||
];
|
||||
};
|
||||
|
||||
getTaskReferencePath = (boardId: string, fileId: string): string[] => {
|
||||
return [...this.getTaskContainerPath(boardId), fileId];
|
||||
};
|
||||
getTaskReferencePath = (boardId: string, fileId: string): string[] => {
|
||||
return [...this.getTaskContainerPath(boardId), fileId];
|
||||
};
|
||||
|
||||
// handlers
|
||||
handleFileContent = (fileContent: FileContent<string>): void => {
|
||||
if (
|
||||
checkMatchesObjectStructure(fileContent, BoardInfoFileContentReference) ==
|
||||
true
|
||||
// handlers
|
||||
handleFileContent = (fileContent: FileContent<string>): void => {
|
||||
if (
|
||||
checkMatchesObjectStructure(
|
||||
fileContent,
|
||||
BoardInfoFileContentReference,
|
||||
) == true
|
||||
) {
|
||||
this.handleBoard(fileContent as BoardInfoFileContent);
|
||||
} else if (
|
||||
checkMatchesObjectStructure(
|
||||
fileContent,
|
||||
TaskFileContentReference,
|
||||
) == true
|
||||
) {
|
||||
this.handleTask(fileContent as TaskFileContent);
|
||||
}
|
||||
};
|
||||
|
||||
handleBoard = (boardInfoFileContent: BoardInfoFileContent) => {
|
||||
this.updateBoard(boardInfoFileContent);
|
||||
};
|
||||
|
||||
handleTask = (taskFileContent: TaskFileContent) => {
|
||||
this.updateTask(taskFileContent);
|
||||
};
|
||||
|
||||
// boards
|
||||
createBoard = (name: string): BoardInfoFileContent => {
|
||||
const boardInfoFileContent: BoardInfoFileContent =
|
||||
BoardsAndTasksModel.createBoardInfoFileContent(
|
||||
v4(),
|
||||
name,
|
||||
Color.Standard,
|
||||
);
|
||||
return boardInfoFileContent;
|
||||
};
|
||||
|
||||
updateBoard = (boardInfoFileContent: BoardInfoFileContent): void => {
|
||||
this.storeBoard(boardInfoFileContent);
|
||||
this.boardHandlerManager.trigger(boardInfoFileContent);
|
||||
};
|
||||
|
||||
updateBoardAndSend = (boardInfoFileContent: BoardInfoFileContent): void => {
|
||||
this.updateBoard(boardInfoFileContent);
|
||||
this.chatModel.sendMessage("", boardInfoFileContent);
|
||||
};
|
||||
|
||||
storeBoard = (boardInfoFileContent: BoardInfoFileContent): void => {
|
||||
// store info
|
||||
this.fileModel.storeFileContent(boardInfoFileContent);
|
||||
|
||||
// add to list
|
||||
const boardDirectoryPath: string[] = this.getBoardDirectoryPath(
|
||||
boardInfoFileContent.fileId,
|
||||
);
|
||||
this.storageModel.write(boardDirectoryPath, "");
|
||||
};
|
||||
|
||||
deleteBoard = (boardId: string): void => {
|
||||
const boardFilePath: string[] = this.getBoardFilePath(boardId);
|
||||
const boardDirectoryPath: string[] =
|
||||
this.getBoardDirectoryPath(boardId);
|
||||
|
||||
this.storageModel.removeRecursively(boardFilePath);
|
||||
this.storageModel.removeRecursively(boardDirectoryPath);
|
||||
};
|
||||
|
||||
listBoardIds = (): string[] => {
|
||||
const boardContainerPath: string[] = this.getBoardContainerPath();
|
||||
const boardIds: string[] = this.storageModel.list(boardContainerPath);
|
||||
return boardIds;
|
||||
};
|
||||
|
||||
getBoardInfo = (fileId: string): BoardInfoFileContent | null => {
|
||||
const boardInfoFileContentOrNull: BoardInfoFileContent | null =
|
||||
this.fileModel.getLatestFileContent(
|
||||
fileId,
|
||||
BoardInfoFileContentReference,
|
||||
);
|
||||
return boardInfoFileContentOrNull;
|
||||
};
|
||||
|
||||
getBoardName = (boardId: string): string => {
|
||||
const boardInfo: BoardInfoFileContent | null =
|
||||
this.getBoardInfo(boardId);
|
||||
if (boardInfo == null) return "";
|
||||
return boardInfo.name;
|
||||
};
|
||||
|
||||
//tasks
|
||||
createTask = (boardId: string): TaskFileContent => {
|
||||
const taskFileContent: TaskFileContent =
|
||||
BoardsAndTasksModel.createTaskFileContent(v4(), "", boardId);
|
||||
return taskFileContent;
|
||||
};
|
||||
|
||||
updateTask = (taskFileContent: TaskFileContent): void => {
|
||||
this.storeTask(taskFileContent);
|
||||
this.taskHandlerManager.trigger(taskFileContent);
|
||||
};
|
||||
|
||||
updateTaskAndSend = (taskFileContent: TaskFileContent): void => {
|
||||
this.updateTask(taskFileContent);
|
||||
this.chatModel.sendMessage("", taskFileContent);
|
||||
};
|
||||
|
||||
storeTask = (taskFileContent: TaskFileContent): void => {
|
||||
// store info
|
||||
this.fileModel.storeFileContent(taskFileContent);
|
||||
|
||||
// add to board
|
||||
const taskReferencePath: string[] = this.getTaskReferencePath(
|
||||
taskFileContent.boardId,
|
||||
taskFileContent.fileId,
|
||||
);
|
||||
this.storageModel.write(taskReferencePath, "");
|
||||
|
||||
// add to calendar
|
||||
this.calendarModel.storeTaskReference(taskFileContent);
|
||||
};
|
||||
|
||||
listTaskIds = (boardId: string): string[] => {
|
||||
const taskContainerPath: string[] = this.getTaskContainerPath(boardId);
|
||||
const fileIds: string[] = this.storageModel.list(taskContainerPath);
|
||||
return fileIds;
|
||||
};
|
||||
|
||||
listTaskVersionIds = (taskId: string): string[] => {
|
||||
const versionIds: string[] = this.fileModel.listFileContentIds(taskId);
|
||||
return versionIds;
|
||||
};
|
||||
|
||||
getLatestTaskFileContent = (taskId: string): TaskFileContent | null => {
|
||||
const taskFileContentOrNull: TaskFileContent | null =
|
||||
this.fileModel.getLatestFileContent(
|
||||
taskId,
|
||||
TaskFileContentReference,
|
||||
);
|
||||
return taskFileContentOrNull;
|
||||
};
|
||||
|
||||
getSpecificTaskFileContent = (
|
||||
taskId: string,
|
||||
versionId: string,
|
||||
): TaskFileContent | null => {
|
||||
const taskFileContentOrNull: TaskFileContent | null =
|
||||
this.fileModel.getFileContent(
|
||||
taskId,
|
||||
versionId,
|
||||
TaskFileContentReference,
|
||||
);
|
||||
return taskFileContentOrNull;
|
||||
};
|
||||
|
||||
deleteTask = (boardId: string, taskId: string): void => {
|
||||
const taskFilePath: string[] = this.getTaskFilePath(taskId);
|
||||
this.storageModel.removeRecursively(taskFilePath);
|
||||
|
||||
this.deleteTaskReference(boardId, taskId);
|
||||
};
|
||||
|
||||
deleteTaskReference = (boardId: string, taskId: string): void => {
|
||||
const taskReferencePath: string[] = this.getTaskReferencePath(
|
||||
boardId,
|
||||
taskId,
|
||||
);
|
||||
this.storageModel.removeRecursively(taskReferencePath);
|
||||
};
|
||||
|
||||
// init
|
||||
constructor(
|
||||
storageModel: StorageModel,
|
||||
settingsModel: SettingsModel,
|
||||
chatModel: ChatModel,
|
||||
fileModel: FileModel,
|
||||
) {
|
||||
this.handleBoard(fileContent as BoardInfoFileContent);
|
||||
} else if (
|
||||
checkMatchesObjectStructure(fileContent, TaskFileContentReference) == true
|
||||
) {
|
||||
this.handleTask(fileContent as TaskFileContent);
|
||||
this.storageModel = storageModel;
|
||||
this.settingsModel = settingsModel;
|
||||
this.chatModel = chatModel;
|
||||
this.fileModel = fileModel;
|
||||
|
||||
this.calendarModel = new CalendarModel(
|
||||
this.storageModel,
|
||||
this.settingsModel,
|
||||
this.fileModel,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
handleBoard = (boardInfoFileContent: BoardInfoFileContent) => {
|
||||
this.updateBoard(boardInfoFileContent);
|
||||
};
|
||||
// utility
|
||||
static createBoardInfoFileContent = (
|
||||
fileId: string,
|
||||
name: string,
|
||||
color: Color,
|
||||
): BoardInfoFileContent => {
|
||||
const fileContent: FileContent<"board-info"> =
|
||||
FileModel.createFileContent(fileId, "board-info");
|
||||
return {
|
||||
...fileContent,
|
||||
|
||||
handleTask = (taskFileContent: TaskFileContent) => {
|
||||
this.updateTask(taskFileContent);
|
||||
};
|
||||
|
||||
// boards
|
||||
createBoard = (name: string): BoardInfoFileContent => {
|
||||
const boardInfoFileContent: BoardInfoFileContent =
|
||||
BoardsAndTasksModel.createBoardInfoFileContent(
|
||||
v4(),
|
||||
name,
|
||||
Color.Standard
|
||||
);
|
||||
return boardInfoFileContent;
|
||||
};
|
||||
|
||||
updateBoard = (boardInfoFileContent: BoardInfoFileContent): void => {
|
||||
this.storeBoard(boardInfoFileContent);
|
||||
this.boardHandlerManager.trigger(boardInfoFileContent);
|
||||
};
|
||||
|
||||
updateBoardAndSend = (boardInfoFileContent: BoardInfoFileContent): void => {
|
||||
this.updateBoard(boardInfoFileContent);
|
||||
this.chatModel.sendMessage("", boardInfoFileContent);
|
||||
};
|
||||
|
||||
storeBoard = (boardInfoFileContent: BoardInfoFileContent): void => {
|
||||
// store info
|
||||
this.fileModel.storeFileContent(boardInfoFileContent);
|
||||
|
||||
// add to list
|
||||
const boardDirectoryPath: string[] = this.getBoardDirectoryPath(
|
||||
boardInfoFileContent.fileId
|
||||
);
|
||||
this.storageModel.write(boardDirectoryPath, "");
|
||||
};
|
||||
|
||||
deleteBoard = (boardId: string): void => {
|
||||
const boardFilePath: string[] = this.getBoardFilePath(boardId);
|
||||
const boardDirectoryPath: string[] = this.getBoardDirectoryPath(boardId);
|
||||
|
||||
this.storageModel.removeRecursively(boardFilePath);
|
||||
this.storageModel.removeRecursively(boardDirectoryPath);
|
||||
};
|
||||
|
||||
listBoardIds = (): string[] => {
|
||||
const boardContainerPath: string[] = this.getBoardContainerPath();
|
||||
const boardIds: string[] = this.storageModel.list(boardContainerPath);
|
||||
return boardIds;
|
||||
};
|
||||
|
||||
getBoardInfo = (fileId: string): BoardInfoFileContent | null => {
|
||||
const boardInfoFileContentOrNull: BoardInfoFileContent | null =
|
||||
this.fileModel.getLatestFileContent(
|
||||
fileId,
|
||||
BoardInfoFileContentReference
|
||||
);
|
||||
return boardInfoFileContentOrNull;
|
||||
};
|
||||
|
||||
getBoardName = (boardId: string): string => {
|
||||
const boardInfo: BoardInfoFileContent|null = this.getBoardInfo(boardId);
|
||||
if (boardInfo == null) return "";
|
||||
return boardInfo.name;
|
||||
}
|
||||
|
||||
//tasks
|
||||
createTask = (boardId: string): TaskFileContent => {
|
||||
const taskFileContent: TaskFileContent =
|
||||
BoardsAndTasksModel.createTaskFileContent(v4(), "", boardId);
|
||||
return taskFileContent;
|
||||
};
|
||||
|
||||
updateTask = (taskFileContent: TaskFileContent): void => {
|
||||
this.storeTask(taskFileContent);
|
||||
this.taskHandlerManager.trigger(taskFileContent);
|
||||
};
|
||||
|
||||
updateTaskAndSend = (taskFileContent: TaskFileContent): void => {
|
||||
this.updateTask(taskFileContent);
|
||||
this.chatModel.sendMessage("", taskFileContent);
|
||||
};
|
||||
|
||||
storeTask = (taskFileContent: TaskFileContent): void => {
|
||||
// store info
|
||||
this.fileModel.storeFileContent(taskFileContent);
|
||||
|
||||
// add to board
|
||||
const taskReferencePath: string[] = this.getTaskReferencePath(
|
||||
taskFileContent.boardId,
|
||||
taskFileContent.fileId
|
||||
);
|
||||
this.storageModel.write(taskReferencePath, "");
|
||||
|
||||
// add to calendar
|
||||
this.calendarModel.storeTaskReference(taskFileContent);
|
||||
};
|
||||
|
||||
listTaskIds = (boardId: string): string[] => {
|
||||
const taskContainerPath: string[] = this.getTaskContainerPath(boardId);
|
||||
const fileIds: string[] = this.storageModel.list(taskContainerPath);
|
||||
return fileIds;
|
||||
};
|
||||
|
||||
listTaskVersionIds = (taskId: string): string[] => {
|
||||
const versionIds: string[] = this.fileModel.listFileContentIds(taskId);
|
||||
return versionIds;
|
||||
};
|
||||
|
||||
getLatestTaskFileContent = (taskId: string): TaskFileContent | null => {
|
||||
const taskFileContentOrNull: TaskFileContent | null =
|
||||
this.fileModel.getLatestFileContent(taskId, TaskFileContentReference);
|
||||
return taskFileContentOrNull;
|
||||
};
|
||||
|
||||
getSpecificTaskFileContent = (
|
||||
taskId: string,
|
||||
versionId: string
|
||||
): TaskFileContent | null => {
|
||||
const taskFileContentOrNull: TaskFileContent | null =
|
||||
this.fileModel.getFileContent(
|
||||
taskId,
|
||||
versionId,
|
||||
TaskFileContentReference
|
||||
);
|
||||
return taskFileContentOrNull;
|
||||
};
|
||||
|
||||
deleteTask = (boardId: string, taskId: string): void => {
|
||||
const taskFilePath: string[] = this.getTaskFilePath(taskId);
|
||||
this.storageModel.removeRecursively(taskFilePath);
|
||||
|
||||
this.deleteTaskReference(boardId, taskId);
|
||||
};
|
||||
|
||||
deleteTaskReference = (boardId: string, taskId: string): void => {
|
||||
const taskReferencePath: string[] = this.getTaskReferencePath(
|
||||
boardId,
|
||||
taskId
|
||||
);
|
||||
this.storageModel.removeRecursively(taskReferencePath);
|
||||
};
|
||||
|
||||
// init
|
||||
constructor(
|
||||
storageModel: StorageModel,
|
||||
settingsModel: SettingsModel,
|
||||
chatModel: ChatModel,
|
||||
fileModel: FileModel
|
||||
) {
|
||||
this.storageModel = storageModel;
|
||||
this.settingsModel = settingsModel;
|
||||
this.chatModel = chatModel;
|
||||
this.fileModel = fileModel;
|
||||
|
||||
this.calendarModel = new CalendarModel(
|
||||
this.storageModel,
|
||||
this.settingsModel,
|
||||
this.fileModel
|
||||
);
|
||||
}
|
||||
|
||||
// utility
|
||||
static createBoardInfoFileContent = (
|
||||
fileId: string,
|
||||
name: string,
|
||||
color: Color
|
||||
): BoardInfoFileContent => {
|
||||
const fileContent: FileContent<"board-info"> = FileModel.createFileContent(
|
||||
fileId,
|
||||
"board-info"
|
||||
);
|
||||
return {
|
||||
...fileContent,
|
||||
|
||||
name,
|
||||
color,
|
||||
name,
|
||||
color,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
static createTaskFileContent = (
|
||||
fileId: string,
|
||||
name: string,
|
||||
boardId: string
|
||||
): TaskFileContent => {
|
||||
const fileContent: FileContent<"task"> = FileModel.createFileContent(
|
||||
fileId,
|
||||
"task"
|
||||
);
|
||||
return {
|
||||
...fileContent,
|
||||
static createTaskFileContent = (
|
||||
fileId: string,
|
||||
name: string,
|
||||
boardId: string,
|
||||
): TaskFileContent => {
|
||||
const fileContent: FileContent<"task"> = FileModel.createFileContent(
|
||||
fileId,
|
||||
"task",
|
||||
);
|
||||
return {
|
||||
...fileContent,
|
||||
|
||||
name,
|
||||
boardId,
|
||||
name,
|
||||
boardId,
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export enum TaskModelSubPaths {
|
||||
Boards = "boards",
|
||||
BoardTasks = "tasks",
|
||||
Boards = "boards",
|
||||
BoardTasks = "tasks",
|
||||
}
|
||||
|
||||
// types
|
||||
export interface BoardInfoFileContent extends FileContent<"board-info"> {
|
||||
name: string;
|
||||
color: Color;
|
||||
name: string;
|
||||
color: Color;
|
||||
}
|
||||
|
||||
export interface TaskFileContent extends FileContent<"task"> {
|
||||
name: string;
|
||||
boardId: string;
|
||||
name: string;
|
||||
boardId: string;
|
||||
|
||||
description?: string;
|
||||
description?: string;
|
||||
|
||||
category?: string;
|
||||
status?: string;
|
||||
priority?: string;
|
||||
category?: string;
|
||||
status?: string;
|
||||
priority?: string;
|
||||
|
||||
date?: string;
|
||||
time?: string;
|
||||
date?: string;
|
||||
time?: string;
|
||||
}
|
||||
|
||||
// reference
|
||||
export const BoardInfoFileContentReference: BoardInfoFileContent = {
|
||||
dataVersion: DATA_VERSION,
|
||||
dataVersion: DATA_VERSION,
|
||||
|
||||
fileId: "string",
|
||||
fileContentId: "",
|
||||
creationDate: "",
|
||||
fileId: "string",
|
||||
fileContentId: "",
|
||||
creationDate: "",
|
||||
|
||||
type: "board-info",
|
||||
type: "board-info",
|
||||
|
||||
name: "",
|
||||
color: "" as Color,
|
||||
name: "",
|
||||
color: "" as Color,
|
||||
};
|
||||
|
||||
export const TaskFileContentReference: TaskFileContent = {
|
||||
dataVersion: DATA_VERSION,
|
||||
dataVersion: DATA_VERSION,
|
||||
|
||||
fileId: "string",
|
||||
fileContentId: "",
|
||||
creationDate: "",
|
||||
type: "task",
|
||||
fileId: "string",
|
||||
fileContentId: "",
|
||||
creationDate: "",
|
||||
type: "task",
|
||||
|
||||
name: "",
|
||||
boardId: "",
|
||||
name: "",
|
||||
boardId: "",
|
||||
};
|
||||
|
||||
+138
-136
@@ -7,158 +7,160 @@ import StorageModel from "../Global/storageModel";
|
||||
import { TaskFileContent } from "./boardsAndTasksModel";
|
||||
|
||||
export default class CalendarModel {
|
||||
storageModel: StorageModel;
|
||||
fileModel: FileModel;
|
||||
settingsModel: SettingsModel;
|
||||
storageModel: StorageModel;
|
||||
fileModel: FileModel;
|
||||
settingsModel: SettingsModel;
|
||||
|
||||
// paths
|
||||
getBasePath = (): string[] => {
|
||||
return this.fileModel.getModelContainerPath(FileModelSubPath.ModelCalendar);
|
||||
};
|
||||
|
||||
getViewPath = (): string[] => {
|
||||
return [...this.getBasePath(), FileModelSubPath.ModelView];
|
||||
};
|
||||
|
||||
getMonthContainerPath = (): string[] => {
|
||||
return [...this.getBasePath(), CalendarModelSubPaths.Months];
|
||||
};
|
||||
|
||||
getMonthPath = (monthString: string): string[] => {
|
||||
return [...this.getMonthContainerPath(), monthString];
|
||||
};
|
||||
|
||||
// main
|
||||
storeTaskReference = (taskFileContent: TaskFileContent): void => {
|
||||
if (taskFileContent.date == undefined) return;
|
||||
|
||||
const monthString: string = CalendarModel.isoToMonthString(
|
||||
taskFileContent.date
|
||||
);
|
||||
|
||||
const monthPath: string[] = this.getMonthPath(monthString);
|
||||
const referencePath: string[] = [...monthPath, taskFileContent.fileId];
|
||||
|
||||
this.storageModel.write(referencePath, "");
|
||||
};
|
||||
|
||||
deleteTaskReference = (monthString: string, taskId: string): void => {
|
||||
const monthPath: string[] = this.getMonthPath(monthString);
|
||||
const referencePath: string[] = [...monthPath, taskId];
|
||||
|
||||
this.storageModel.write(referencePath, "");
|
||||
};
|
||||
|
||||
listTaskIds = (monthString: string): string[] => {
|
||||
const monthPath: string[] = this.getMonthPath(monthString);
|
||||
return this.storageModel.list(monthPath);
|
||||
};
|
||||
|
||||
generateMonthGrid = <T>(
|
||||
year: number,
|
||||
month: number,
|
||||
defaultValueCreator: () => T
|
||||
): MonthGrid<T> => {
|
||||
// get today
|
||||
const date: Date = new Date();
|
||||
const isCurrentMonth: boolean =
|
||||
year == date.getFullYear() && month == date.getMonth() + 1;
|
||||
|
||||
// get data for offset
|
||||
date.setDate(1);
|
||||
date.setMonth(month - 1);
|
||||
date.setFullYear(year);
|
||||
|
||||
const firstWeekdayOfMonth = date.getDay();
|
||||
const firstDayOfWeekSetting: number = parseInt(
|
||||
this.settingsModel.firstDayOfWeek
|
||||
);
|
||||
|
||||
const offset: number =
|
||||
firstWeekdayOfMonth < firstDayOfWeekSetting
|
||||
? 7 - firstDayOfWeekSetting
|
||||
: firstWeekdayOfMonth - firstDayOfWeekSetting;
|
||||
|
||||
// get total day count
|
||||
date.setMonth(month);
|
||||
date.setDate(-1);
|
||||
const daysInMonth: number = date.getDate() + 1;
|
||||
|
||||
const grid: MonthGrid<T> = {
|
||||
offset,
|
||||
firstDayOfWeek: parseInt(this.settingsModel.firstDayOfWeek),
|
||||
isCurrentMonth,
|
||||
year,
|
||||
month,
|
||||
days: {},
|
||||
// paths
|
||||
getBasePath = (): string[] => {
|
||||
return this.fileModel.getModelContainerPath(
|
||||
FileModelSubPath.ModelCalendar,
|
||||
);
|
||||
};
|
||||
|
||||
for (let i = 0; i < daysInMonth; i++) {
|
||||
const paddedDate: string = CalendarModel.padDateOrMonth(
|
||||
(i + 1).toString()
|
||||
);
|
||||
grid.days[paddedDate] = defaultValueCreator();
|
||||
getViewPath = (): string[] => {
|
||||
return [...this.getBasePath(), FileModelSubPath.ModelView];
|
||||
};
|
||||
|
||||
getMonthContainerPath = (): string[] => {
|
||||
return [...this.getBasePath(), CalendarModelSubPaths.Months];
|
||||
};
|
||||
|
||||
getMonthPath = (monthString: string): string[] => {
|
||||
return [...this.getMonthContainerPath(), monthString];
|
||||
};
|
||||
|
||||
// main
|
||||
storeTaskReference = (taskFileContent: TaskFileContent): void => {
|
||||
if (taskFileContent.date == undefined) return;
|
||||
|
||||
const monthString: string = CalendarModel.isoToMonthString(
|
||||
taskFileContent.date,
|
||||
);
|
||||
|
||||
const monthPath: string[] = this.getMonthPath(monthString);
|
||||
const referencePath: string[] = [...monthPath, taskFileContent.fileId];
|
||||
|
||||
this.storageModel.write(referencePath, "");
|
||||
};
|
||||
|
||||
deleteTaskReference = (monthString: string, taskId: string): void => {
|
||||
const monthPath: string[] = this.getMonthPath(monthString);
|
||||
const referencePath: string[] = [...monthPath, taskId];
|
||||
|
||||
this.storageModel.write(referencePath, "");
|
||||
};
|
||||
|
||||
listTaskIds = (monthString: string): string[] => {
|
||||
const monthPath: string[] = this.getMonthPath(monthString);
|
||||
return this.storageModel.list(monthPath);
|
||||
};
|
||||
|
||||
generateMonthGrid = <T>(
|
||||
year: number,
|
||||
month: number,
|
||||
defaultValueCreator: () => T,
|
||||
): MonthGrid<T> => {
|
||||
// get today
|
||||
const date: Date = new Date();
|
||||
const isCurrentMonth: boolean =
|
||||
year == date.getFullYear() && month == date.getMonth() + 1;
|
||||
|
||||
// get data for offset
|
||||
date.setDate(1);
|
||||
date.setMonth(month - 1);
|
||||
date.setFullYear(year);
|
||||
|
||||
const firstWeekdayOfMonth = date.getDay();
|
||||
const firstDayOfWeekSetting: number = parseInt(
|
||||
this.settingsModel.firstDayOfWeek,
|
||||
);
|
||||
|
||||
const offset: number =
|
||||
firstWeekdayOfMonth < firstDayOfWeekSetting
|
||||
? 7 - firstDayOfWeekSetting
|
||||
: firstWeekdayOfMonth - firstDayOfWeekSetting;
|
||||
|
||||
// get total day count
|
||||
date.setMonth(month);
|
||||
date.setDate(-1);
|
||||
const daysInMonth: number = date.getDate() + 1;
|
||||
|
||||
const grid: MonthGrid<T> = {
|
||||
offset,
|
||||
firstDayOfWeek: parseInt(this.settingsModel.firstDayOfWeek),
|
||||
isCurrentMonth,
|
||||
year,
|
||||
month,
|
||||
days: {},
|
||||
};
|
||||
|
||||
for (let i = 0; i < daysInMonth; i++) {
|
||||
const paddedDate: string = CalendarModel.padDateOrMonth(
|
||||
(i + 1).toString(),
|
||||
);
|
||||
grid.days[paddedDate] = defaultValueCreator();
|
||||
}
|
||||
|
||||
return grid;
|
||||
};
|
||||
|
||||
// init
|
||||
constructor(
|
||||
storageModel: StorageModel,
|
||||
settingsModel: SettingsModel,
|
||||
fileModel: FileModel,
|
||||
) {
|
||||
this.storageModel = storageModel;
|
||||
this.settingsModel = settingsModel;
|
||||
this.fileModel = fileModel;
|
||||
}
|
||||
|
||||
return grid;
|
||||
};
|
||||
// utility
|
||||
static isoToMonthString = (dateISOString: string): string => {
|
||||
const [year, month, _] = dateISOString.split("-");
|
||||
return CalendarModel.getMonthString(year, month);
|
||||
};
|
||||
|
||||
// init
|
||||
constructor(
|
||||
storageModel: StorageModel,
|
||||
settingsModel: SettingsModel,
|
||||
fileModel: FileModel
|
||||
) {
|
||||
this.storageModel = storageModel;
|
||||
this.settingsModel = settingsModel;
|
||||
this.fileModel = fileModel;
|
||||
}
|
||||
static isoToDateString = (dateISOString: string): string => {
|
||||
const [year, month, date, _] = dateISOString.split("-");
|
||||
const paddedDate = CalendarModel.padDateOrMonth(date ?? "");
|
||||
return paddedDate;
|
||||
};
|
||||
|
||||
// utility
|
||||
static isoToMonthString = (dateISOString: string): string => {
|
||||
const [year, month, _] = dateISOString.split("-");
|
||||
return CalendarModel.getMonthString(year, month);
|
||||
};
|
||||
static getMonthString = (year: string = "", month: string = ""): string => {
|
||||
const paddedYear: string = year.padStart(4, "0");
|
||||
const paddedMonth: string = CalendarModel.padDateOrMonth(month);
|
||||
return `${paddedYear}-${paddedMonth}`;
|
||||
};
|
||||
|
||||
static isoToDateString = (dateISOString: string): string => {
|
||||
const [year, month, date, _] = dateISOString.split("-");
|
||||
const paddedDate = CalendarModel.padDateOrMonth(date ?? "");
|
||||
return paddedDate;
|
||||
};
|
||||
static getISODateString = (
|
||||
year: string,
|
||||
month: string,
|
||||
date: string,
|
||||
): string => {
|
||||
const monthString: string = CalendarModel.getMonthString(year, month);
|
||||
const paddedDate: string = CalendarModel.padDateOrMonth(date);
|
||||
return `${monthString}-${paddedDate}`;
|
||||
};
|
||||
|
||||
static getMonthString = (year: string = "", month: string = ""): string => {
|
||||
const paddedYear: string = year.padStart(4, "0");
|
||||
const paddedMonth: string = CalendarModel.padDateOrMonth(month);
|
||||
return `${paddedYear}-${paddedMonth}`;
|
||||
};
|
||||
|
||||
static getISODateString = (
|
||||
year: string,
|
||||
month: string,
|
||||
date: string
|
||||
): string => {
|
||||
const monthString: string = CalendarModel.getMonthString(year, month);
|
||||
const paddedDate: string = CalendarModel.padDateOrMonth(date);
|
||||
return `${monthString}-${paddedDate}`;
|
||||
};
|
||||
|
||||
static padDateOrMonth = (input: string): string => {
|
||||
return input.padStart(2, "0");
|
||||
};
|
||||
static padDateOrMonth = (input: string): string => {
|
||||
return input.padStart(2, "0");
|
||||
};
|
||||
}
|
||||
|
||||
export enum CalendarModelSubPaths {
|
||||
Months = "months",
|
||||
Months = "months",
|
||||
}
|
||||
|
||||
// types
|
||||
export interface MonthGrid<T> {
|
||||
offset: number;
|
||||
firstDayOfWeek: number;
|
||||
isCurrentMonth: boolean;
|
||||
offset: number;
|
||||
firstDayOfWeek: number;
|
||||
isCurrentMonth: boolean;
|
||||
|
||||
year: number;
|
||||
month: number;
|
||||
year: number;
|
||||
month: number;
|
||||
|
||||
days: { [date: string]: T };
|
||||
days: { [date: string]: T };
|
||||
}
|
||||
|
||||
+178
-175
@@ -2,14 +2,14 @@
|
||||
|
||||
import { DATA_VERSION, ValidObject } from "../Utility/typeSafety";
|
||||
import StorageModel, {
|
||||
StorageModelSubPath,
|
||||
filePaths,
|
||||
StorageModelSubPath,
|
||||
filePaths,
|
||||
} from "../Global/storageModel";
|
||||
import {
|
||||
StringEntryObject,
|
||||
createTimestamp,
|
||||
parseValidObject,
|
||||
stringify,
|
||||
StringEntryObject,
|
||||
createTimestamp,
|
||||
parseValidObject,
|
||||
stringify,
|
||||
} from "../Utility/utility";
|
||||
|
||||
import BoardsAndTasksModel from "./boardsAndTasksModel";
|
||||
@@ -18,191 +18,194 @@ import SettingsModel from "../Global/settingsModel";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
export default class FileModel {
|
||||
storageModel: StorageModel;
|
||||
settingsModel: SettingsModel;
|
||||
storageModel: StorageModel;
|
||||
settingsModel: SettingsModel;
|
||||
|
||||
chatModel: ChatModel;
|
||||
boardsAndTasksModel: BoardsAndTasksModel;
|
||||
chatModel: ChatModel;
|
||||
boardsAndTasksModel: BoardsAndTasksModel;
|
||||
|
||||
// paths
|
||||
getBasePath = (): string[] => {
|
||||
return StorageModel.getPath(
|
||||
StorageModelSubPath.Chat,
|
||||
filePaths.chat.files(this.chatModel.id)
|
||||
);
|
||||
};
|
||||
|
||||
getFileContainerPath = (): string[] => {
|
||||
return [...this.getBasePath(), FileModelSubPath.Data];
|
||||
};
|
||||
|
||||
getModelContainerPath = (modelName: FileModelSubPath): string[] => {
|
||||
return [...this.getBasePath(), FileModelSubPath.Model, modelName];
|
||||
};
|
||||
|
||||
getFilePath = (fileId: string): string[] => {
|
||||
return [...this.getFileContainerPath(), fileId];
|
||||
};
|
||||
|
||||
getFileContentPath = (fileId: string, fileContentId: string): string[] => {
|
||||
const filePath: string[] = this.getFilePath(fileId);
|
||||
return [...filePath, fileContentId];
|
||||
};
|
||||
|
||||
// handlers
|
||||
handleStringifiedFileContent = (stringifiedFileContent: string): void => {
|
||||
const fileContent: FileContent<string> | null = parseValidObject(
|
||||
stringifiedFileContent,
|
||||
FileContentReference
|
||||
);
|
||||
if (fileContent == null) return;
|
||||
|
||||
this.handleFileContent(fileContent);
|
||||
};
|
||||
|
||||
handleFileContent = (fileContent: FileContent<string>): void => {
|
||||
const didStore: boolean = this.storeFileContent(fileContent);
|
||||
if (didStore == false) return;
|
||||
|
||||
this.boardsAndTasksModel.handleFileContent(fileContent);
|
||||
};
|
||||
|
||||
// methods
|
||||
addFileContentAndSend = (fileContent: FileContent<string>): void => {
|
||||
this.handleFileContent(fileContent);
|
||||
this.chatModel.sendMessage("", fileContent);
|
||||
};
|
||||
|
||||
// storage
|
||||
storeFileContent = (fileContent: FileContent<string>): boolean => {
|
||||
const fileContentPath: string[] = this.getFileContentPath(
|
||||
fileContent.fileId,
|
||||
fileContent.fileContentId
|
||||
);
|
||||
|
||||
// check if fileContent already exists
|
||||
const existingFileContent: string | null =
|
||||
this.storageModel.read(fileContentPath);
|
||||
if (existingFileContent != null) return false;
|
||||
|
||||
const stringifiedContent: string = stringify(fileContent);
|
||||
this.storageModel.write(fileContentPath, stringifiedContent);
|
||||
return true;
|
||||
};
|
||||
|
||||
listFileIds = (): string[] => {
|
||||
return this.storageModel.list(this.getBasePath());
|
||||
};
|
||||
|
||||
listFileContentIds = (fileId: string): string[] => {
|
||||
const filePath: string[] = this.getFilePath(fileId);
|
||||
return this.storageModel.list(filePath);
|
||||
};
|
||||
|
||||
selectLatestFileContentId = (
|
||||
fileContentIds: string[]
|
||||
): string | undefined => {
|
||||
return fileContentIds[fileContentIds.length - 1];
|
||||
};
|
||||
|
||||
getFileContent = <T extends FileContent<string>>(
|
||||
fileId: string,
|
||||
fileContentName: string,
|
||||
reference: T
|
||||
): T | null => {
|
||||
const filePath: string[] = this.getFileContentPath(fileId, fileContentName);
|
||||
const fileContentOrNull: T | null = this.storageModel.readStringifiable(
|
||||
filePath,
|
||||
reference
|
||||
);
|
||||
return fileContentOrNull;
|
||||
};
|
||||
|
||||
getLatestFileContent = <T extends FileContent<string>>(
|
||||
fileId: string,
|
||||
reference: T
|
||||
): T | null => {
|
||||
const fileContentsIds: string[] = this.listFileContentIds(fileId);
|
||||
const latestFileContentId: string | undefined =
|
||||
this.selectLatestFileContentId(fileContentsIds);
|
||||
if (latestFileContentId == undefined) return null;
|
||||
|
||||
const fileContent: T | null = this.getFileContent(
|
||||
fileId,
|
||||
latestFileContentId,
|
||||
reference
|
||||
);
|
||||
return fileContent;
|
||||
};
|
||||
|
||||
// init
|
||||
constructor(
|
||||
storageModel: StorageModel,
|
||||
settingsModel: SettingsModel,
|
||||
chatModel: ChatModel
|
||||
) {
|
||||
this.chatModel = chatModel;
|
||||
this.settingsModel = settingsModel;
|
||||
this.storageModel = storageModel;
|
||||
|
||||
this.boardsAndTasksModel = new BoardsAndTasksModel(
|
||||
this.storageModel,
|
||||
this.settingsModel,
|
||||
chatModel,
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
// utility
|
||||
static generateFileContentId = (creationDate: string): string => {
|
||||
return creationDate + v4();
|
||||
};
|
||||
|
||||
static createFileContent = <T extends string>(
|
||||
fileId: string,
|
||||
type: T
|
||||
): FileContent<T> => {
|
||||
const creationDate: string = createTimestamp();
|
||||
const fileContentId: string = FileModel.generateFileContentId(creationDate);
|
||||
|
||||
return {
|
||||
dataVersion: DATA_VERSION,
|
||||
|
||||
fileId,
|
||||
fileContentId,
|
||||
creationDate,
|
||||
type,
|
||||
// paths
|
||||
getBasePath = (): string[] => {
|
||||
return StorageModel.getPath(
|
||||
StorageModelSubPath.Chat,
|
||||
filePaths.chat.files(this.chatModel.id),
|
||||
);
|
||||
};
|
||||
|
||||
getFileContainerPath = (): string[] => {
|
||||
return [...this.getBasePath(), FileModelSubPath.Data];
|
||||
};
|
||||
|
||||
getModelContainerPath = (modelName: FileModelSubPath): string[] => {
|
||||
return [...this.getBasePath(), FileModelSubPath.Model, modelName];
|
||||
};
|
||||
|
||||
getFilePath = (fileId: string): string[] => {
|
||||
return [...this.getFileContainerPath(), fileId];
|
||||
};
|
||||
|
||||
getFileContentPath = (fileId: string, fileContentId: string): string[] => {
|
||||
const filePath: string[] = this.getFilePath(fileId);
|
||||
return [...filePath, fileContentId];
|
||||
};
|
||||
|
||||
// handlers
|
||||
handleStringifiedFileContent = (stringifiedFileContent: string): void => {
|
||||
const fileContent: FileContent<string> | null = parseValidObject(
|
||||
stringifiedFileContent,
|
||||
FileContentReference,
|
||||
);
|
||||
if (fileContent == null) return;
|
||||
|
||||
this.handleFileContent(fileContent);
|
||||
};
|
||||
|
||||
handleFileContent = (fileContent: FileContent<string>): void => {
|
||||
const didStore: boolean = this.storeFileContent(fileContent);
|
||||
if (didStore == false) return;
|
||||
|
||||
this.boardsAndTasksModel.handleFileContent(fileContent);
|
||||
};
|
||||
|
||||
// methods
|
||||
addFileContentAndSend = (fileContent: FileContent<string>): void => {
|
||||
this.handleFileContent(fileContent);
|
||||
this.chatModel.sendMessage("", fileContent);
|
||||
};
|
||||
|
||||
// storage
|
||||
storeFileContent = (fileContent: FileContent<string>): boolean => {
|
||||
const fileContentPath: string[] = this.getFileContentPath(
|
||||
fileContent.fileId,
|
||||
fileContent.fileContentId,
|
||||
);
|
||||
|
||||
// check if fileContent already exists
|
||||
const existingFileContent: string | null =
|
||||
this.storageModel.read(fileContentPath);
|
||||
if (existingFileContent != null) return false;
|
||||
|
||||
const stringifiedContent: string = stringify(fileContent);
|
||||
this.storageModel.write(fileContentPath, stringifiedContent);
|
||||
return true;
|
||||
};
|
||||
|
||||
listFileIds = (): string[] => {
|
||||
return this.storageModel.list(this.getBasePath());
|
||||
};
|
||||
|
||||
listFileContentIds = (fileId: string): string[] => {
|
||||
const filePath: string[] = this.getFilePath(fileId);
|
||||
return this.storageModel.list(filePath);
|
||||
};
|
||||
|
||||
selectLatestFileContentId = (
|
||||
fileContentIds: string[],
|
||||
): string | undefined => {
|
||||
return fileContentIds[fileContentIds.length - 1];
|
||||
};
|
||||
|
||||
getFileContent = <T extends FileContent<string>>(
|
||||
fileId: string,
|
||||
fileContentName: string,
|
||||
reference: T,
|
||||
): T | null => {
|
||||
const filePath: string[] = this.getFileContentPath(
|
||||
fileId,
|
||||
fileContentName,
|
||||
);
|
||||
const fileContentOrNull: T | null = this.storageModel.readStringifiable(
|
||||
filePath,
|
||||
reference,
|
||||
);
|
||||
return fileContentOrNull;
|
||||
};
|
||||
|
||||
getLatestFileContent = <T extends FileContent<string>>(
|
||||
fileId: string,
|
||||
reference: T,
|
||||
): T | null => {
|
||||
const fileContentsIds: string[] = this.listFileContentIds(fileId);
|
||||
const latestFileContentId: string | undefined =
|
||||
this.selectLatestFileContentId(fileContentsIds);
|
||||
if (latestFileContentId == undefined) return null;
|
||||
|
||||
const fileContent: T | null = this.getFileContent(
|
||||
fileId,
|
||||
latestFileContentId,
|
||||
reference,
|
||||
);
|
||||
return fileContent;
|
||||
};
|
||||
|
||||
// init
|
||||
constructor(
|
||||
storageModel: StorageModel,
|
||||
settingsModel: SettingsModel,
|
||||
chatModel: ChatModel,
|
||||
) {
|
||||
this.chatModel = chatModel;
|
||||
this.settingsModel = settingsModel;
|
||||
this.storageModel = storageModel;
|
||||
|
||||
this.boardsAndTasksModel = new BoardsAndTasksModel(
|
||||
this.storageModel,
|
||||
this.settingsModel,
|
||||
chatModel,
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
// utility
|
||||
static generateFileContentId = (creationDate: string): string => {
|
||||
return creationDate + v4();
|
||||
};
|
||||
|
||||
static createFileContent = <T extends string>(
|
||||
fileId: string,
|
||||
type: T,
|
||||
): FileContent<T> => {
|
||||
const creationDate: string = createTimestamp();
|
||||
const fileContentId: string =
|
||||
FileModel.generateFileContentId(creationDate);
|
||||
|
||||
return {
|
||||
dataVersion: DATA_VERSION,
|
||||
|
||||
fileId,
|
||||
fileContentId,
|
||||
creationDate,
|
||||
type,
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// paths
|
||||
export enum FileModelSubPath {
|
||||
Data = "data",
|
||||
Model = "model",
|
||||
ModelView = "view",
|
||||
ModelTask = "tasks",
|
||||
ModelCalendar = "calendar",
|
||||
Data = "data",
|
||||
Model = "model",
|
||||
ModelView = "view",
|
||||
ModelTask = "tasks",
|
||||
ModelCalendar = "calendar",
|
||||
}
|
||||
|
||||
// types
|
||||
export interface FileContent<T extends string>
|
||||
extends ValidObject,
|
||||
StringEntryObject {
|
||||
fileId: string;
|
||||
fileContentId: string;
|
||||
creationDate: string;
|
||||
extends ValidObject, StringEntryObject {
|
||||
fileId: string;
|
||||
fileContentId: string;
|
||||
creationDate: string;
|
||||
|
||||
type: T;
|
||||
type: T;
|
||||
}
|
||||
|
||||
// references
|
||||
export const FileContentReference: FileContent<string> = {
|
||||
dataVersion: DATA_VERSION,
|
||||
dataVersion: DATA_VERSION,
|
||||
|
||||
fileId: "",
|
||||
fileContentId: "",
|
||||
creationDate: "",
|
||||
fileId: "",
|
||||
fileContentId: "",
|
||||
creationDate: "",
|
||||
|
||||
type: "",
|
||||
type: "",
|
||||
};
|
||||
|
||||
+257
-234
@@ -6,255 +6,278 @@ import StorageModel, { StorageModelSubPath, filePaths } from "./storageModel";
|
||||
import UDNFrontend, { Message } from "udn-frontend";
|
||||
|
||||
export default class ConnectionModel {
|
||||
udn: UDNFrontend;
|
||||
storageModel: StorageModel;
|
||||
udn: UDNFrontend;
|
||||
storageModel: StorageModel;
|
||||
reconnectInterval: number | undefined = undefined;
|
||||
shouldAttemptReconnect: boolean = false;
|
||||
|
||||
// data
|
||||
get isConnected(): boolean {
|
||||
return this.udn.ws != undefined && this.udn.ws.readyState == 1;
|
||||
}
|
||||
|
||||
get address(): string | undefined {
|
||||
return this.udn.ws?.url;
|
||||
}
|
||||
|
||||
connectionChangeHandlerManager: HandlerManager<void> = new HandlerManager();
|
||||
messageHandlerManager: HandlerManager<Message> = new HandlerManager();
|
||||
messageSentHandlerManager: HandlerManager<ChatMessage> = new HandlerManager();
|
||||
|
||||
channelsToSubscribe: Set<string> = new Set();
|
||||
|
||||
// handlers
|
||||
handleMessage = (data: Message): void => {
|
||||
this.messageHandlerManager.trigger(data);
|
||||
};
|
||||
|
||||
handleConnectionChange = (): void => {
|
||||
console.log("connection status:", this.isConnected, this.address);
|
||||
this.connectionChangeHandlerManager.trigger();
|
||||
|
||||
if (this.isConnected == false) return;
|
||||
if (this.address == undefined) return;
|
||||
|
||||
this.storeAddress(this.address);
|
||||
this.sendSubscriptionRequest();
|
||||
this.sendMessagesInOutbox();
|
||||
};
|
||||
|
||||
// connection
|
||||
connect = (address: string): void => {
|
||||
this.udn.connect(address);
|
||||
};
|
||||
|
||||
disconnect = (): void => {
|
||||
this.udn.disconnect();
|
||||
|
||||
// do not reconnect
|
||||
const reconnectAddressPath: string[] = StorageModel.getPath(
|
||||
StorageModelSubPath.ConnectionModel,
|
||||
filePaths.connectionModel.reconnectAddress
|
||||
);
|
||||
this.storageModel.remove(reconnectAddressPath);
|
||||
};
|
||||
|
||||
// mailbox
|
||||
getMailboxPath = (address: string): string[] => {
|
||||
const mailboxDirPath = StorageModel.getPath(
|
||||
StorageModelSubPath.ConnectionModel,
|
||||
filePaths.connectionModel.mailboxes
|
||||
);
|
||||
const mailboxFilePath = [...mailboxDirPath, address];
|
||||
return mailboxFilePath;
|
||||
};
|
||||
|
||||
requestNewMailbox = (): void => {
|
||||
console.log("requesting new mailbox");
|
||||
this.udn.requestMailbox();
|
||||
};
|
||||
|
||||
connectMailbox = (): void => {
|
||||
if (this.address == undefined) return;
|
||||
|
||||
const mailboxId = this.storageModel.read(this.getMailboxPath(this.address));
|
||||
console.log("connecting mailbox", mailboxId);
|
||||
if (mailboxId == null) return this.requestNewMailbox();
|
||||
|
||||
this.udn.connectMailbox(mailboxId);
|
||||
};
|
||||
|
||||
storeMailbox = (mailboxId: string): void => {
|
||||
if (this.address == undefined) return;
|
||||
this.storageModel.write(this.getMailboxPath(this.address), mailboxId);
|
||||
};
|
||||
|
||||
// subscription
|
||||
addChannel = (channel: string): void => {
|
||||
this.channelsToSubscribe.add(channel);
|
||||
this.sendSubscriptionRequest();
|
||||
};
|
||||
|
||||
sendSubscriptionRequest = (): void => {
|
||||
if (this.isConnected == false) return;
|
||||
|
||||
for (const channel of this.channelsToSubscribe) {
|
||||
this.udn.subscribe(channel);
|
||||
// data
|
||||
get isConnected(): boolean {
|
||||
return this.udn.ws != undefined && this.udn.ws.readyState == 1;
|
||||
}
|
||||
// update mailbox
|
||||
this.connectMailbox();
|
||||
};
|
||||
|
||||
// outbox
|
||||
getOutboxPath = (): string[] => {
|
||||
return StorageModel.getPath(
|
||||
StorageModelSubPath.ConnectionModel,
|
||||
filePaths.connectionModel.outbox
|
||||
);
|
||||
};
|
||||
get address(): string | undefined {
|
||||
return this.udn.ws?.url;
|
||||
}
|
||||
|
||||
getOutboxMessags = (): ChatMessage[] => {
|
||||
const outboxPath: string[] = this.getOutboxPath();
|
||||
const messageIds: string[] = this.storageModel.list(outboxPath);
|
||||
connectionChangeHandlerManager: HandlerManager<void> = new HandlerManager();
|
||||
messageHandlerManager: HandlerManager<Message> = new HandlerManager();
|
||||
messageSentHandlerManager: HandlerManager<ChatMessage> =
|
||||
new HandlerManager();
|
||||
|
||||
let chatMessages: ChatMessage[] = [];
|
||||
for (const messageId of messageIds) {
|
||||
const chatMessage: ChatMessage | null =
|
||||
this.storageModel.readStringifiable(
|
||||
[...outboxPath, messageId],
|
||||
ChatMessageReference
|
||||
channelsToSubscribe: Set<string> = new Set();
|
||||
|
||||
// handlers
|
||||
handleMessage = (data: Message): void => {
|
||||
this.messageHandlerManager.trigger(data);
|
||||
};
|
||||
|
||||
handleConnectionChange = (): void => {
|
||||
console.log("connection status:", this.isConnected, this.address);
|
||||
this.connectionChangeHandlerManager.trigger();
|
||||
|
||||
if (this.isConnected == false) {
|
||||
if (this.shouldAttemptReconnect == false) return;
|
||||
|
||||
this.reconnectInterval = setInterval(() => {
|
||||
if (this.isConnected == true) {
|
||||
clearInterval(this.reconnectInterval);
|
||||
return;
|
||||
}
|
||||
this.reconnect();
|
||||
}, 5000);
|
||||
|
||||
return;
|
||||
}
|
||||
if (this.address == undefined) return;
|
||||
|
||||
this.storeAddress(this.address);
|
||||
this.sendSubscriptionRequest();
|
||||
this.sendMessagesInOutbox();
|
||||
this.shouldAttemptReconnect = true;
|
||||
};
|
||||
|
||||
// connection
|
||||
connect = (address: string): void => {
|
||||
this.udn.connect(address);
|
||||
};
|
||||
|
||||
disconnect = (): void => {
|
||||
this.udn.disconnect();
|
||||
|
||||
// do not reconnect
|
||||
const reconnectAddressPath: string[] = StorageModel.getPath(
|
||||
StorageModelSubPath.ConnectionModel,
|
||||
filePaths.connectionModel.reconnectAddress,
|
||||
);
|
||||
this.storageModel.remove(reconnectAddressPath);
|
||||
this.shouldAttemptReconnect = false;
|
||||
};
|
||||
|
||||
if (chatMessage == null) continue;
|
||||
chatMessages.push(chatMessage);
|
||||
reconnect = (): void => {
|
||||
const reconnectAddressPath: string[] = this.getReconnectAddressPath();
|
||||
const reconnectAddress: string | null =
|
||||
this.storageModel.read(reconnectAddressPath);
|
||||
if (reconnectAddress == null) return;
|
||||
|
||||
console.log("reconnecting...");
|
||||
this.connect(reconnectAddress);
|
||||
};
|
||||
|
||||
// mailbox
|
||||
getMailboxPath = (address: string): string[] => {
|
||||
const mailboxDirPath = StorageModel.getPath(
|
||||
StorageModelSubPath.ConnectionModel,
|
||||
filePaths.connectionModel.mailboxes,
|
||||
);
|
||||
const mailboxFilePath = [...mailboxDirPath, address];
|
||||
return mailboxFilePath;
|
||||
};
|
||||
|
||||
requestNewMailbox = (): void => {
|
||||
console.log("requesting new mailbox");
|
||||
this.udn.requestMailbox();
|
||||
};
|
||||
|
||||
connectMailbox = (): void => {
|
||||
if (this.address == undefined) return;
|
||||
|
||||
const mailboxId = this.storageModel.read(
|
||||
this.getMailboxPath(this.address),
|
||||
);
|
||||
console.log("connecting mailbox", mailboxId);
|
||||
if (mailboxId == null) return this.requestNewMailbox();
|
||||
|
||||
this.udn.connectMailbox(mailboxId);
|
||||
};
|
||||
|
||||
storeMailbox = (mailboxId: string): void => {
|
||||
if (this.address == undefined) return;
|
||||
this.storageModel.write(this.getMailboxPath(this.address), mailboxId);
|
||||
};
|
||||
|
||||
// subscription
|
||||
addChannel = (channel: string): void => {
|
||||
this.channelsToSubscribe.add(channel);
|
||||
this.sendSubscriptionRequest();
|
||||
};
|
||||
|
||||
sendSubscriptionRequest = (): void => {
|
||||
if (this.isConnected == false) return;
|
||||
|
||||
for (const channel of this.channelsToSubscribe) {
|
||||
this.udn.subscribe(channel);
|
||||
}
|
||||
// update mailbox
|
||||
this.connectMailbox();
|
||||
};
|
||||
|
||||
// outbox
|
||||
getOutboxPath = (): string[] => {
|
||||
return StorageModel.getPath(
|
||||
StorageModelSubPath.ConnectionModel,
|
||||
filePaths.connectionModel.outbox,
|
||||
);
|
||||
};
|
||||
|
||||
getOutboxMessags = (): ChatMessage[] => {
|
||||
const outboxPath: string[] = this.getOutboxPath();
|
||||
const messageIds: string[] = this.storageModel.list(outboxPath);
|
||||
|
||||
let chatMessages: ChatMessage[] = [];
|
||||
for (const messageId of messageIds) {
|
||||
const chatMessage: ChatMessage | null =
|
||||
this.storageModel.readStringifiable(
|
||||
[...outboxPath, messageId],
|
||||
ChatMessageReference,
|
||||
);
|
||||
|
||||
if (chatMessage == null) continue;
|
||||
chatMessages.push(chatMessage);
|
||||
}
|
||||
|
||||
return chatMessages;
|
||||
};
|
||||
|
||||
addToOutbox = (chatMessage: ChatMessage): void => {
|
||||
const messagePath: string[] = [...this.getOutboxPath(), chatMessage.id];
|
||||
this.storageModel.writeStringifiable(messagePath, chatMessage);
|
||||
};
|
||||
|
||||
removeFromOutbox = (chatMessage: ChatMessage): void => {
|
||||
const messagePath: string[] = [...this.getOutboxPath(), chatMessage.id];
|
||||
this.storageModel.remove(messagePath);
|
||||
};
|
||||
|
||||
sendMessagesInOutbox = (): void => {
|
||||
const messages: ChatMessage[] = this.getOutboxMessags();
|
||||
|
||||
for (const message of messages) {
|
||||
const isSent: boolean = this.tryToSendMessage(message);
|
||||
if (isSent == false) return;
|
||||
|
||||
this.removeFromOutbox(message);
|
||||
}
|
||||
};
|
||||
|
||||
// messaging
|
||||
sendMessageOrStore = (chatMessage: ChatMessage): void => {
|
||||
const isSent = this.tryToSendMessage(chatMessage);
|
||||
if (isSent == true) return;
|
||||
|
||||
this.addToOutbox(chatMessage);
|
||||
};
|
||||
|
||||
tryToSendMessage = (chatMessage: ChatMessage): boolean => {
|
||||
const stringifiedBody: string = stringify(chatMessage);
|
||||
const isSent: boolean = this.sendPlainMessage(
|
||||
chatMessage.channel,
|
||||
stringifiedBody,
|
||||
);
|
||||
if (isSent) this.messageSentHandlerManager.trigger(chatMessage);
|
||||
return isSent;
|
||||
};
|
||||
|
||||
sendPlainMessage = (channel: string, body: string): boolean => {
|
||||
return this.udn.sendMessage(channel, body);
|
||||
};
|
||||
|
||||
// storage
|
||||
getPreviousAddressPath = (): string[] => {
|
||||
return StorageModel.getPath(
|
||||
StorageModelSubPath.ConnectionModel,
|
||||
filePaths.connectionModel.previousAddresses,
|
||||
);
|
||||
};
|
||||
|
||||
getAddressPath = (address: string): string[] => {
|
||||
const dirPath = this.getPreviousAddressPath();
|
||||
return [...dirPath, address];
|
||||
};
|
||||
|
||||
getReconnectAddressPath = (): string[] => {
|
||||
return StorageModel.getPath(
|
||||
StorageModelSubPath.ConnectionModel,
|
||||
filePaths.connectionModel.reconnectAddress,
|
||||
);
|
||||
};
|
||||
|
||||
storeAddress = (address: string): void => {
|
||||
// history
|
||||
const addressPath = this.getAddressPath(address);
|
||||
this.storageModel.write(addressPath, "");
|
||||
|
||||
// reconnect
|
||||
const reconnectAddressPath: string[] = this.getReconnectAddressPath();
|
||||
this.storageModel.write(reconnectAddressPath, address);
|
||||
};
|
||||
|
||||
removeAddress = (address: string): void => {
|
||||
const addressPath = this.getAddressPath(address);
|
||||
this.storageModel.remove(addressPath);
|
||||
};
|
||||
|
||||
get addresses(): string[] {
|
||||
const dirPath = this.getPreviousAddressPath();
|
||||
return this.storageModel.list(dirPath);
|
||||
}
|
||||
|
||||
return chatMessages;
|
||||
};
|
||||
// init
|
||||
constructor(storageModel: StorageModel) {
|
||||
// create frontend
|
||||
this.udn = new UDNFrontend();
|
||||
this.storageModel = storageModel;
|
||||
|
||||
addToOutbox = (chatMessage: ChatMessage): void => {
|
||||
const messagePath: string[] = [...this.getOutboxPath(), chatMessage.id];
|
||||
this.storageModel.writeStringifiable(messagePath, chatMessage);
|
||||
};
|
||||
// setup handlers
|
||||
this.udn.onmessage = (data: Message) => {
|
||||
this.handleMessage(data);
|
||||
};
|
||||
this.udn.onconnect = () => {
|
||||
this.handleConnectionChange();
|
||||
};
|
||||
this.udn.ondisconnect = () => {
|
||||
this.handleConnectionChange();
|
||||
};
|
||||
|
||||
removeFromOutbox = (chatMessage: ChatMessage): void => {
|
||||
const messagePath: string[] = [...this.getOutboxPath(), chatMessage.id];
|
||||
this.storageModel.remove(messagePath);
|
||||
};
|
||||
this.udn.onmailboxcreate = (mailboxId: string) => {
|
||||
console.log("created mailbox", mailboxId);
|
||||
this.storeMailbox(mailboxId);
|
||||
this.connectMailbox();
|
||||
};
|
||||
this.udn.onmailboxdelete = (mailboxId: string) => {
|
||||
console.log(`mailbox ${mailboxId} deleted`);
|
||||
this.requestNewMailbox();
|
||||
};
|
||||
this.udn.onmailboxconnect = (mailboxId: string) => {
|
||||
console.log(`using mailbox ${mailboxId}`);
|
||||
};
|
||||
|
||||
sendMessagesInOutbox = (): void => {
|
||||
const messages: ChatMessage[] = this.getOutboxMessags();
|
||||
|
||||
for (const message of messages) {
|
||||
const isSent: boolean = this.tryToSendMessage(message);
|
||||
if (isSent == false) return;
|
||||
|
||||
this.removeFromOutbox(message);
|
||||
this.reconnect();
|
||||
}
|
||||
};
|
||||
|
||||
// messaging
|
||||
sendMessageOrStore = (chatMessage: ChatMessage): void => {
|
||||
const isSent = this.tryToSendMessage(chatMessage);
|
||||
if (isSent == true) return;
|
||||
|
||||
this.addToOutbox(chatMessage);
|
||||
};
|
||||
|
||||
tryToSendMessage = (chatMessage: ChatMessage): boolean => {
|
||||
const stringifiedBody: string = stringify(chatMessage);
|
||||
const isSent: boolean = this.sendPlainMessage(
|
||||
chatMessage.channel,
|
||||
stringifiedBody
|
||||
);
|
||||
if (isSent) this.messageSentHandlerManager.trigger(chatMessage);
|
||||
return isSent;
|
||||
};
|
||||
|
||||
sendPlainMessage = (channel: string, body: string): boolean => {
|
||||
return this.udn.sendMessage(channel, body);
|
||||
};
|
||||
|
||||
// storage
|
||||
getPreviousAddressPath = (): string[] => {
|
||||
return StorageModel.getPath(
|
||||
StorageModelSubPath.ConnectionModel,
|
||||
filePaths.connectionModel.previousAddresses
|
||||
);
|
||||
};
|
||||
|
||||
getAddressPath = (address: string): string[] => {
|
||||
const dirPath = this.getPreviousAddressPath();
|
||||
return [...dirPath, address];
|
||||
};
|
||||
|
||||
getReconnectAddressPath = (): string[] => {
|
||||
return StorageModel.getPath(
|
||||
StorageModelSubPath.ConnectionModel,
|
||||
filePaths.connectionModel.reconnectAddress
|
||||
);
|
||||
};
|
||||
|
||||
storeAddress = (address: string): void => {
|
||||
// history
|
||||
const addressPath = this.getAddressPath(address);
|
||||
this.storageModel.write(addressPath, "");
|
||||
|
||||
// reconnect
|
||||
const reconnectAddressPath: string[] = this.getReconnectAddressPath();
|
||||
this.storageModel.write(reconnectAddressPath, address);
|
||||
};
|
||||
|
||||
removeAddress = (address: string): void => {
|
||||
const addressPath = this.getAddressPath(address);
|
||||
this.storageModel.remove(addressPath);
|
||||
};
|
||||
|
||||
get addresses(): string[] {
|
||||
const dirPath = this.getPreviousAddressPath();
|
||||
return this.storageModel.list(dirPath);
|
||||
}
|
||||
|
||||
// init
|
||||
constructor(storageModel: StorageModel) {
|
||||
// create frontend
|
||||
this.udn = new UDNFrontend();
|
||||
this.storageModel = storageModel;
|
||||
|
||||
// setup handlers
|
||||
this.udn.onmessage = (data: Message) => {
|
||||
this.handleMessage(data);
|
||||
};
|
||||
this.udn.onconnect = () => {
|
||||
this.handleConnectionChange();
|
||||
};
|
||||
this.udn.ondisconnect = () => {
|
||||
this.handleConnectionChange();
|
||||
};
|
||||
|
||||
this.udn.onmailboxcreate = (mailboxId: string) => {
|
||||
console.log("created mailbox", mailboxId);
|
||||
this.storeMailbox(mailboxId);
|
||||
this.connectMailbox();
|
||||
};
|
||||
this.udn.onmailboxdelete = (mailboxId: string) => {
|
||||
console.log(`mailbox ${mailboxId} deleted`);
|
||||
this.requestNewMailbox();
|
||||
};
|
||||
this.udn.onmailboxconnect = (mailboxId: string) => {
|
||||
console.log(`using mailbox ${mailboxId}`);
|
||||
};
|
||||
|
||||
// reconnect
|
||||
const reconnectAddressPath: string[] = this.getReconnectAddressPath();
|
||||
const reconnectAddress: string | null =
|
||||
storageModel.read(reconnectAddressPath);
|
||||
if (reconnectAddress != null) {
|
||||
this.connect(reconnectAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// paths
|
||||
export enum settingsModelStorageFiles {
|
||||
username = "user-name",
|
||||
firstDayOfWeek = "first-day-of-week",
|
||||
username = "user-name",
|
||||
firstDayOfWeek = "first-day-of-week",
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// this file is responsible for sending and handling files.
|
||||
|
||||
import {
|
||||
HandlerManager,
|
||||
generateRandomToken,
|
||||
parse,
|
||||
stringify,
|
||||
HandlerManager,
|
||||
generateRandomToken,
|
||||
parse,
|
||||
stringify,
|
||||
} from "../Utility/utility";
|
||||
import { decryptString, encryptString } from "../Utility/crypto";
|
||||
|
||||
@@ -14,118 +14,120 @@ import StorageModel from "./storageModel";
|
||||
import { checkMatchesObjectStructure } from "../Utility/typeSafety";
|
||||
|
||||
export default class FileTransferModel {
|
||||
storageModel: StorageModel;
|
||||
connectionModel: ConnectionModel;
|
||||
storageModel: StorageModel;
|
||||
connectionModel: ConnectionModel;
|
||||
|
||||
// data
|
||||
transferData: TransferData | undefined;
|
||||
fileHandlerManager: HandlerManager<string> = new HandlerManager();
|
||||
// data
|
||||
transferData: TransferData | undefined;
|
||||
fileHandlerManager: HandlerManager<string> = new HandlerManager();
|
||||
|
||||
// general
|
||||
generateTransferData = (): TransferData => {
|
||||
const transferData: TransferData = {
|
||||
channel: generateRandomToken(2).substring(0, 4),
|
||||
key: generateRandomToken(3).substring(0, 6),
|
||||
// general
|
||||
generateTransferData = (): TransferData => {
|
||||
const transferData: TransferData = {
|
||||
channel: generateRandomToken(2).substring(0, 4),
|
||||
key: generateRandomToken(3).substring(0, 6),
|
||||
};
|
||||
this.transferData = transferData;
|
||||
return transferData;
|
||||
};
|
||||
this.transferData = transferData;
|
||||
return transferData;
|
||||
};
|
||||
|
||||
prepareToReceive = (transferData: TransferData) => {
|
||||
this.connectionModel.addChannel(transferData.channel);
|
||||
this.transferData = transferData;
|
||||
};
|
||||
prepareToReceive = (transferData: TransferData) => {
|
||||
this.connectionModel.addChannel(transferData.channel);
|
||||
this.transferData = transferData;
|
||||
};
|
||||
|
||||
// handlers
|
||||
handleMessage = (data: Message): void => {
|
||||
if (this.transferData == undefined) return;
|
||||
if (data.messageBody == undefined) return;
|
||||
if (data.messageChannel != this.transferData.channel) return;
|
||||
// handlers
|
||||
handleMessage = (data: Message): void => {
|
||||
if (this.transferData == undefined) return;
|
||||
if (data.messageBody == undefined) return;
|
||||
if (data.messageChannel != this.transferData.channel) return;
|
||||
|
||||
this.handleFile(data.messageBody);
|
||||
};
|
||||
this.handleFile(data.messageBody);
|
||||
};
|
||||
|
||||
handleFile = async (encryptedFileData: string): Promise<void> => {
|
||||
if (this.transferData == undefined) return;
|
||||
handleFile = async (encryptedFileData: string): Promise<void> => {
|
||||
if (this.transferData == undefined) return;
|
||||
|
||||
const decrypted: string = await decryptString(
|
||||
encryptedFileData,
|
||||
this.transferData.key
|
||||
);
|
||||
console.log(decrypted);
|
||||
|
||||
const parsed: any = parse(decrypted);
|
||||
const isFileData: boolean = checkMatchesObjectStructure(
|
||||
parsed,
|
||||
FileDataReference
|
||||
);
|
||||
if (isFileData == false) return;
|
||||
|
||||
const fileData: FileData = parsed;
|
||||
this.storageModel.write(fileData.path, fileData.body);
|
||||
|
||||
const pathString: string = StorageModel.pathComponentsToString(
|
||||
...fileData.path
|
||||
);
|
||||
this.fileHandlerManager.trigger(pathString);
|
||||
};
|
||||
|
||||
// sending
|
||||
sendFiles = (
|
||||
directoryPaths: IteratorObject<string[]>,
|
||||
callback: (path: string) => void
|
||||
): void => {
|
||||
for (const directoryPath of directoryPaths) {
|
||||
this.storageModel.recurse(directoryPath, (filePath: string[]) => {
|
||||
this.sendFile(filePath);
|
||||
const pathString: string = StorageModel.pathComponentsToString(
|
||||
...filePath
|
||||
const decrypted: string = await decryptString(
|
||||
encryptedFileData,
|
||||
this.transferData.key,
|
||||
);
|
||||
callback(pathString);
|
||||
});
|
||||
}
|
||||
};
|
||||
console.log(decrypted);
|
||||
|
||||
sendFile = async (filePath: string[]): Promise<void> => {
|
||||
if (this.transferData == undefined) return;
|
||||
const fileContent: string | null = this.storageModel.read(filePath);
|
||||
if (fileContent == null) return;
|
||||
const parsed: any = parse(decrypted);
|
||||
const isFileData: boolean = checkMatchesObjectStructure(
|
||||
parsed,
|
||||
FileDataReference,
|
||||
);
|
||||
if (isFileData == false) return;
|
||||
|
||||
const fileData: FileData = {
|
||||
path: filePath,
|
||||
body: fileContent,
|
||||
const fileData: FileData = parsed;
|
||||
this.storageModel.write(fileData.path, fileData.body);
|
||||
|
||||
const pathString: string = StorageModel.pathComponentsToString(
|
||||
...fileData.path,
|
||||
);
|
||||
this.fileHandlerManager.trigger(pathString);
|
||||
};
|
||||
const stringifiedFileData: string = stringify(fileData);
|
||||
const encryptedFileData: string = await encryptString(
|
||||
stringifiedFileData,
|
||||
this.transferData.key
|
||||
);
|
||||
this.connectionModel.sendPlainMessage(
|
||||
this.transferData.channel,
|
||||
encryptedFileData
|
||||
);
|
||||
};
|
||||
|
||||
// init
|
||||
constructor(storageModel: StorageModel, connectionModel: ConnectionModel) {
|
||||
this.storageModel = storageModel;
|
||||
this.connectionModel = connectionModel;
|
||||
// sending
|
||||
sendFiles = (
|
||||
directoryPaths: IteratorObject<string[]>,
|
||||
callback: (path: string) => void,
|
||||
): void => {
|
||||
for (const directoryPath of directoryPaths) {
|
||||
this.storageModel.recurse(directoryPath, (filePath: string[]) => {
|
||||
this.sendFile(filePath);
|
||||
const pathString: string = StorageModel.pathComponentsToString(
|
||||
...filePath,
|
||||
);
|
||||
callback(pathString);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this.connectionModel.messageHandlerManager.addHandler(this.handleMessage);
|
||||
}
|
||||
sendFile = async (filePath: string[]): Promise<void> => {
|
||||
if (this.transferData == undefined) return;
|
||||
const fileContent: string | null = this.storageModel.read(filePath);
|
||||
if (fileContent == null) return;
|
||||
|
||||
const fileData: FileData = {
|
||||
path: filePath,
|
||||
body: fileContent,
|
||||
};
|
||||
const stringifiedFileData: string = stringify(fileData);
|
||||
const encryptedFileData: string = await encryptString(
|
||||
stringifiedFileData,
|
||||
this.transferData.key,
|
||||
);
|
||||
this.connectionModel.sendPlainMessage(
|
||||
this.transferData.channel,
|
||||
encryptedFileData,
|
||||
);
|
||||
};
|
||||
|
||||
// init
|
||||
constructor(storageModel: StorageModel, connectionModel: ConnectionModel) {
|
||||
this.storageModel = storageModel;
|
||||
this.connectionModel = connectionModel;
|
||||
|
||||
this.connectionModel.messageHandlerManager.addHandler(
|
||||
this.handleMessage,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export interface TransferData {
|
||||
channel: string;
|
||||
key: string;
|
||||
channel: string;
|
||||
key: string;
|
||||
}
|
||||
|
||||
export interface FileData {
|
||||
path: string[];
|
||||
body: string;
|
||||
path: string[];
|
||||
body: string;
|
||||
}
|
||||
|
||||
export const FileDataReference: FileData = {
|
||||
path: [""],
|
||||
body: "",
|
||||
path: [""],
|
||||
body: "",
|
||||
};
|
||||
|
||||
@@ -3,55 +3,55 @@
|
||||
import StorageModel, { StorageModelSubPath, filePaths } from "./storageModel";
|
||||
|
||||
export default class SettingsModel {
|
||||
storageModel: StorageModel;
|
||||
storageModel: StorageModel;
|
||||
|
||||
// data
|
||||
username: string;
|
||||
firstDayOfWeek: string;
|
||||
// data
|
||||
username: string;
|
||||
firstDayOfWeek: string;
|
||||
|
||||
// storage
|
||||
setName(newValue: string): void {
|
||||
this.username = newValue;
|
||||
const path = StorageModel.getPath(
|
||||
StorageModelSubPath.SettingsModel,
|
||||
filePaths.settingsModel.username
|
||||
);
|
||||
this.storageModel.write(path, newValue);
|
||||
}
|
||||
// storage
|
||||
setName(newValue: string): void {
|
||||
this.username = newValue;
|
||||
const path = StorageModel.getPath(
|
||||
StorageModelSubPath.SettingsModel,
|
||||
filePaths.settingsModel.username,
|
||||
);
|
||||
this.storageModel.write(path, newValue);
|
||||
}
|
||||
|
||||
setFirstDayOfWeek(newValue: string): void {
|
||||
this.firstDayOfWeek = newValue;
|
||||
const path = StorageModel.getPath(
|
||||
StorageModelSubPath.SettingsModel,
|
||||
filePaths.settingsModel.firstDayOfWeek
|
||||
);
|
||||
this.storageModel.write(path, newValue);
|
||||
}
|
||||
setFirstDayOfWeek(newValue: string): void {
|
||||
this.firstDayOfWeek = newValue;
|
||||
const path = StorageModel.getPath(
|
||||
StorageModelSubPath.SettingsModel,
|
||||
filePaths.settingsModel.firstDayOfWeek,
|
||||
);
|
||||
this.storageModel.write(path, newValue);
|
||||
}
|
||||
|
||||
// load
|
||||
loadUsernam(): void {
|
||||
const path = StorageModel.getPath(
|
||||
StorageModelSubPath.SettingsModel,
|
||||
filePaths.settingsModel.username
|
||||
);
|
||||
const content = this.storageModel.read(path);
|
||||
this.username = content ?? "";
|
||||
}
|
||||
// load
|
||||
loadUsernam(): void {
|
||||
const path = StorageModel.getPath(
|
||||
StorageModelSubPath.SettingsModel,
|
||||
filePaths.settingsModel.username,
|
||||
);
|
||||
const content = this.storageModel.read(path);
|
||||
this.username = content ?? "";
|
||||
}
|
||||
|
||||
loadFirstDayofWeek(): void {
|
||||
const path = StorageModel.getPath(
|
||||
StorageModelSubPath.SettingsModel,
|
||||
filePaths.settingsModel.firstDayOfWeek
|
||||
);
|
||||
const content = this.storageModel.read(path);
|
||||
this.firstDayOfWeek = content ?? "0";
|
||||
}
|
||||
loadFirstDayofWeek(): void {
|
||||
const path = StorageModel.getPath(
|
||||
StorageModelSubPath.SettingsModel,
|
||||
filePaths.settingsModel.firstDayOfWeek,
|
||||
);
|
||||
const content = this.storageModel.read(path);
|
||||
this.firstDayOfWeek = content ?? "0";
|
||||
}
|
||||
|
||||
// init
|
||||
constructor(storageModel: StorageModel) {
|
||||
this.storageModel = storageModel;
|
||||
// init
|
||||
constructor(storageModel: StorageModel) {
|
||||
this.storageModel = storageModel;
|
||||
|
||||
this.loadUsernam();
|
||||
this.loadFirstDayofWeek();
|
||||
}
|
||||
this.loadUsernam();
|
||||
this.loadFirstDayofWeek();
|
||||
}
|
||||
}
|
||||
|
||||
+229
-219
@@ -6,207 +6,217 @@ import { localeCompare, parseValidObject, stringify } from "../Utility/utility";
|
||||
export const PATH_COMPONENT_SEPARATOR = "\\";
|
||||
|
||||
export default class StorageModel {
|
||||
storageEntryTree: StorageEntry = {};
|
||||
storageEntryTree: StorageEntry = {};
|
||||
|
||||
// read
|
||||
read = (pathComponents: string[]): string | null => {
|
||||
const pathString: string = StorageModel.pathComponentsToString(
|
||||
...pathComponents
|
||||
);
|
||||
return localStorage.getItem(pathString);
|
||||
};
|
||||
// read
|
||||
read = (pathComponents: string[]): string | null => {
|
||||
const pathString: string = StorageModel.pathComponentsToString(
|
||||
...pathComponents,
|
||||
);
|
||||
return localStorage.getItem(pathString);
|
||||
};
|
||||
|
||||
list = (pathComponents: string[]): string[] => {
|
||||
let currentParent: StorageEntry = this.storageEntryTree;
|
||||
for (const component of pathComponents) {
|
||||
const nextParent: StorageEntry | undefined = currentParent[component];
|
||||
if (nextParent == undefined) return [];
|
||||
currentParent = nextParent;
|
||||
list = (pathComponents: string[]): string[] => {
|
||||
let currentParent: StorageEntry = this.storageEntryTree;
|
||||
for (const component of pathComponents) {
|
||||
const nextParent: StorageEntry | undefined =
|
||||
currentParent[component];
|
||||
if (nextParent == undefined) return [];
|
||||
currentParent = nextParent;
|
||||
}
|
||||
|
||||
return [...Object.keys(currentParent).sort(localeCompare)];
|
||||
};
|
||||
|
||||
// write
|
||||
write = (pathComponents: string[], value: string): void => {
|
||||
const pathString: string = StorageModel.pathComponentsToString(
|
||||
...pathComponents,
|
||||
);
|
||||
localStorage.setItem(pathString, value);
|
||||
this.updateTree(...pathComponents);
|
||||
};
|
||||
|
||||
remove = (
|
||||
pathComponents: string[],
|
||||
shouldInitialize: boolean = true,
|
||||
): void => {
|
||||
const pathString: string = StorageModel.pathComponentsToString(
|
||||
...pathComponents,
|
||||
);
|
||||
localStorage.removeItem(pathString);
|
||||
|
||||
if (shouldInitialize == true) {
|
||||
this.initializeTree();
|
||||
}
|
||||
};
|
||||
|
||||
rename = (
|
||||
sourcePathComponents: string[],
|
||||
destinationPathComponents: string[],
|
||||
shouldInitialize: boolean = true,
|
||||
): boolean => {
|
||||
const content: string | null = this.read(sourcePathComponents);
|
||||
if (content == null) return false;
|
||||
|
||||
this.write(destinationPathComponents, content);
|
||||
this.remove(sourcePathComponents);
|
||||
|
||||
if (shouldInitialize == true) {
|
||||
this.initializeTree();
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// recursion
|
||||
recurse = (rootDirectory: string[], fn: (path: string[]) => void): void => {
|
||||
loop_over_files: for (const key of Object.keys(localStorage)) {
|
||||
// get path of current entity
|
||||
const pathComponentsOfCurrentEntity: string[] =
|
||||
StorageModel.stringToPathComponents(key);
|
||||
|
||||
// exit if entity does not match
|
||||
loop_over_path_components: for (
|
||||
let i = 0;
|
||||
i < rootDirectory.length;
|
||||
i++
|
||||
) {
|
||||
if (!pathComponentsOfCurrentEntity[i]) continue loop_over_files;
|
||||
if (pathComponentsOfCurrentEntity[i] != rootDirectory[i])
|
||||
continue loop_over_files;
|
||||
}
|
||||
|
||||
// execute
|
||||
fn(pathComponentsOfCurrentEntity);
|
||||
}
|
||||
this.initializeTree();
|
||||
};
|
||||
|
||||
removeRecursively = (pathComponents: string[]): void => {
|
||||
this.recurse(pathComponents, (path: string[]) =>
|
||||
this.remove(path, false),
|
||||
);
|
||||
this.initializeTree();
|
||||
};
|
||||
|
||||
renameRecursively = (
|
||||
sourcePathComponents: string[],
|
||||
destinationPathComponents: string[],
|
||||
): void => {
|
||||
this.recurse(sourcePathComponents, (path: string[]) => {
|
||||
// assemble destinationPath
|
||||
const relativePathOfCurrentEntity = path.slice(
|
||||
sourcePathComponents.length,
|
||||
);
|
||||
const destinationPathComponentsOfCurrentEntity = [
|
||||
...destinationPathComponents,
|
||||
...relativePathOfCurrentEntity,
|
||||
];
|
||||
|
||||
// rename
|
||||
this.rename(path, destinationPathComponentsOfCurrentEntity, false);
|
||||
});
|
||||
this.initializeTree();
|
||||
};
|
||||
|
||||
// stringifiable
|
||||
writeStringifiable = (
|
||||
pathComponents: string[],
|
||||
value: ValidObject,
|
||||
): void => {
|
||||
const valueString: string = stringify(value);
|
||||
this.write(pathComponents, valueString);
|
||||
};
|
||||
|
||||
readStringifiable = <T extends ValidObject>(
|
||||
pathComponents: string[],
|
||||
reference: T,
|
||||
): T | null => {
|
||||
const valueString: string | null = this.read(pathComponents);
|
||||
if (!valueString) return null;
|
||||
|
||||
const object: any | null = parseValidObject(valueString, reference);
|
||||
if (object == null) return null;
|
||||
|
||||
return object;
|
||||
};
|
||||
|
||||
// cleaning
|
||||
removeJunk = (): void => {
|
||||
this.recurse([], (path: string[]) => {
|
||||
if (path[0] == DATA_VERSION) return;
|
||||
this.remove(path);
|
||||
});
|
||||
};
|
||||
|
||||
// tree
|
||||
initializeTree = (): void => {
|
||||
console.log("initializing tree");
|
||||
|
||||
this.storageEntryTree = {};
|
||||
for (const key of Object.keys(localStorage)) {
|
||||
const components: string[] =
|
||||
StorageModel.stringToPathComponents(key);
|
||||
this.updateTree(...components);
|
||||
}
|
||||
};
|
||||
|
||||
updateTree = (...pathComponents: string[]): void => {
|
||||
let currentParent: StorageEntry = this.storageEntryTree;
|
||||
for (const pathPart of pathComponents) {
|
||||
if (!currentParent[pathPart]) {
|
||||
currentParent[pathPart] = {};
|
||||
}
|
||||
|
||||
currentParent = currentParent[pathPart];
|
||||
}
|
||||
};
|
||||
|
||||
printTree = (): string => {
|
||||
return stringify(this.storageEntryTree);
|
||||
};
|
||||
|
||||
// init
|
||||
constructor() {
|
||||
this.initializeTree();
|
||||
}
|
||||
|
||||
return [...Object.keys(currentParent).sort(localeCompare)];
|
||||
};
|
||||
// utility
|
||||
static getFileName = (pathComponents: string[]): string => {
|
||||
return pathComponents[pathComponents.length - 1] || "\\";
|
||||
};
|
||||
|
||||
// write
|
||||
write = (pathComponents: string[], value: string): void => {
|
||||
const pathString: string = StorageModel.pathComponentsToString(
|
||||
...pathComponents
|
||||
);
|
||||
localStorage.setItem(pathString, value);
|
||||
this.updateTree(...pathComponents);
|
||||
};
|
||||
static getFileNameFromString = (pathString: string): string => {
|
||||
const pathComponents: string[] =
|
||||
this.stringToPathComponents(pathString);
|
||||
return pathComponents[pathComponents.length - 1] || "\\";
|
||||
};
|
||||
|
||||
remove = (
|
||||
pathComponents: string[],
|
||||
shouldInitialize: boolean = true
|
||||
): void => {
|
||||
const pathString: string = StorageModel.pathComponentsToString(
|
||||
...pathComponents
|
||||
);
|
||||
localStorage.removeItem(pathString);
|
||||
static pathComponentsToString = (...pathComponents: string[]): string => {
|
||||
return pathComponents
|
||||
.filter((x) => x != "")
|
||||
.join(PATH_COMPONENT_SEPARATOR);
|
||||
};
|
||||
|
||||
if (shouldInitialize == true) {
|
||||
this.initializeTree();
|
||||
static stringToPathComponents = (string: string): string[] => {
|
||||
return string.split(PATH_COMPONENT_SEPARATOR).filter((x) => x != "");
|
||||
};
|
||||
|
||||
static join = (...items: string[]): string => {
|
||||
let allComponents: string[] = [];
|
||||
for (const item of items) {
|
||||
const parts = this.stringToPathComponents(item);
|
||||
allComponents.push(...parts);
|
||||
}
|
||||
return StorageModel.pathComponentsToString(...allComponents);
|
||||
};
|
||||
|
||||
static getPath(
|
||||
locationName: StorageModelSubPath,
|
||||
filePath: string[],
|
||||
): string[] {
|
||||
return [DATA_VERSION, locationName, ...filePath];
|
||||
}
|
||||
};
|
||||
|
||||
rename = (
|
||||
sourcePathComponents: string[],
|
||||
destinationPathComponents: string[],
|
||||
shouldInitialize: boolean = true
|
||||
): boolean => {
|
||||
const content: string | null = this.read(sourcePathComponents);
|
||||
if (content == null) return false;
|
||||
|
||||
this.write(destinationPathComponents, content);
|
||||
this.remove(sourcePathComponents);
|
||||
|
||||
if (shouldInitialize == true) {
|
||||
this.initializeTree();
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// recursion
|
||||
recurse = (rootDirectory: string[], fn: (path: string[]) => void): void => {
|
||||
loop_over_files: for (const key of Object.keys(localStorage)) {
|
||||
// get path of current entity
|
||||
const pathComponentsOfCurrentEntity: string[] =
|
||||
StorageModel.stringToPathComponents(key);
|
||||
|
||||
// exit if entity does not match
|
||||
loop_over_path_components: for (
|
||||
let i = 0;
|
||||
i < rootDirectory.length;
|
||||
i++
|
||||
) {
|
||||
if (!pathComponentsOfCurrentEntity[i]) continue loop_over_files;
|
||||
if (pathComponentsOfCurrentEntity[i] != rootDirectory[i])
|
||||
continue loop_over_files;
|
||||
}
|
||||
|
||||
// execute
|
||||
fn(pathComponentsOfCurrentEntity);
|
||||
}
|
||||
this.initializeTree();
|
||||
};
|
||||
|
||||
removeRecursively = (pathComponents: string[]): void => {
|
||||
this.recurse(pathComponents, (path: string[]) => this.remove(path, false));
|
||||
this.initializeTree();
|
||||
};
|
||||
|
||||
renameRecursively = (
|
||||
sourcePathComponents: string[],
|
||||
destinationPathComponents: string[]
|
||||
): void => {
|
||||
this.recurse(sourcePathComponents, (path: string[]) => {
|
||||
// assemble destinationPath
|
||||
const relativePathOfCurrentEntity = path.slice(
|
||||
sourcePathComponents.length
|
||||
);
|
||||
const destinationPathComponentsOfCurrentEntity = [
|
||||
...destinationPathComponents,
|
||||
...relativePathOfCurrentEntity,
|
||||
];
|
||||
|
||||
// rename
|
||||
this.rename(path, destinationPathComponentsOfCurrentEntity, false);
|
||||
});
|
||||
this.initializeTree();
|
||||
};
|
||||
|
||||
// stringifiable
|
||||
writeStringifiable = (pathComponents: string[], value: ValidObject): void => {
|
||||
const valueString: string = stringify(value);
|
||||
this.write(pathComponents, valueString);
|
||||
};
|
||||
|
||||
readStringifiable = <T extends ValidObject>(
|
||||
pathComponents: string[],
|
||||
reference: T
|
||||
): T | null => {
|
||||
const valueString: string | null = this.read(pathComponents);
|
||||
if (!valueString) return null;
|
||||
|
||||
const object: any | null = parseValidObject(valueString, reference);
|
||||
if (object == null) return null;
|
||||
|
||||
return object;
|
||||
};
|
||||
|
||||
// cleaning
|
||||
removeJunk = (): void => {
|
||||
this.recurse([], (path: string[]) => {
|
||||
if (path[0] == DATA_VERSION) return;
|
||||
this.remove(path);
|
||||
})
|
||||
}
|
||||
|
||||
// tree
|
||||
initializeTree = (): void => {
|
||||
console.log("initializing tree");
|
||||
|
||||
this.storageEntryTree = {};
|
||||
for (const key of Object.keys(localStorage)) {
|
||||
const components: string[] = StorageModel.stringToPathComponents(key);
|
||||
this.updateTree(...components);
|
||||
}
|
||||
};
|
||||
|
||||
updateTree = (...pathComponents: string[]): void => {
|
||||
let currentParent: StorageEntry = this.storageEntryTree;
|
||||
for (const pathPart of pathComponents) {
|
||||
if (!currentParent[pathPart]) {
|
||||
currentParent[pathPart] = {};
|
||||
}
|
||||
|
||||
currentParent = currentParent[pathPart];
|
||||
}
|
||||
};
|
||||
|
||||
printTree = (): string => {
|
||||
return stringify(this.storageEntryTree);
|
||||
};
|
||||
|
||||
// init
|
||||
constructor() {
|
||||
this.initializeTree();
|
||||
}
|
||||
|
||||
// utility
|
||||
static getFileName = (pathComponents: string[]): string => {
|
||||
return pathComponents[pathComponents.length - 1] || "\\";
|
||||
};
|
||||
|
||||
static getFileNameFromString = (pathString: string): string => {
|
||||
const pathComponents: string[] = this.stringToPathComponents(pathString);
|
||||
return pathComponents[pathComponents.length - 1] || "\\";
|
||||
};
|
||||
|
||||
static pathComponentsToString = (...pathComponents: string[]): string => {
|
||||
return pathComponents.filter((x) => x != "").join(PATH_COMPONENT_SEPARATOR);
|
||||
};
|
||||
|
||||
static stringToPathComponents = (string: string): string[] => {
|
||||
return string.split(PATH_COMPONENT_SEPARATOR).filter((x) => x != "");
|
||||
};
|
||||
|
||||
static join = (...items: string[]): string => {
|
||||
let allComponents: string[] = [];
|
||||
for (const item of items) {
|
||||
const parts = this.stringToPathComponents(item);
|
||||
allComponents.push(...parts);
|
||||
}
|
||||
return StorageModel.pathComponentsToString(...allComponents);
|
||||
};
|
||||
|
||||
static getPath(
|
||||
locationName: StorageModelSubPath,
|
||||
filePath: string[]
|
||||
): string[] {
|
||||
return [DATA_VERSION, locationName, ...filePath];
|
||||
}
|
||||
}
|
||||
|
||||
// types
|
||||
@@ -214,37 +224,37 @@ export type StorageEntry = { [key: string]: StorageEntry };
|
||||
|
||||
// locations
|
||||
export enum StorageModelSubPath {
|
||||
Chat = "chat",
|
||||
Chat = "chat",
|
||||
|
||||
ConnectionModel = "connection",
|
||||
SettingsModel = "settings",
|
||||
ConnectionModel = "connection",
|
||||
SettingsModel = "settings",
|
||||
}
|
||||
|
||||
export const filePaths = {
|
||||
connectionModel: {
|
||||
socketAddress: ["socket-address"],
|
||||
reconnectAddress: ["reconnect-address"],
|
||||
outbox: ["outbox"],
|
||||
mailboxes: ["mailboxes"],
|
||||
connectionModel: {
|
||||
socketAddress: ["socket-address"],
|
||||
reconnectAddress: ["reconnect-address"],
|
||||
outbox: ["outbox"],
|
||||
mailboxes: ["mailboxes"],
|
||||
|
||||
previousAddresses: ["previous-addresses"],
|
||||
},
|
||||
previousAddresses: ["previous-addresses"],
|
||||
},
|
||||
|
||||
chat: {
|
||||
base: [],
|
||||
chatBase: (id: string) => [id],
|
||||
info: (id: string) => [...filePaths.chat.chatBase(id), "info"],
|
||||
color: (id: string) => [...filePaths.chat.chatBase(id), "color"],
|
||||
messages: (id: string) => [...filePaths.chat.chatBase(id), "messages"],
|
||||
lastUsedPage: (id: string) => [
|
||||
...filePaths.chat.chatBase(id),
|
||||
"last-used-page",
|
||||
],
|
||||
files: (id: string) => [...filePaths.chat.chatBase(id), "files"],
|
||||
},
|
||||
chat: {
|
||||
base: [],
|
||||
chatBase: (id: string) => [id],
|
||||
info: (id: string) => [...filePaths.chat.chatBase(id), "info"],
|
||||
color: (id: string) => [...filePaths.chat.chatBase(id), "color"],
|
||||
messages: (id: string) => [...filePaths.chat.chatBase(id), "messages"],
|
||||
lastUsedPage: (id: string) => [
|
||||
...filePaths.chat.chatBase(id),
|
||||
"last-used-page",
|
||||
],
|
||||
files: (id: string) => [...filePaths.chat.chatBase(id), "files"],
|
||||
},
|
||||
|
||||
settingsModel: {
|
||||
username: ["user-name"],
|
||||
firstDayOfWeek: ["first-day-of-week"],
|
||||
},
|
||||
settingsModel: {
|
||||
username: ["user-name"],
|
||||
firstDayOfWeek: ["first-day-of-week"],
|
||||
},
|
||||
};
|
||||
|
||||
+60
-60
@@ -3,113 +3,113 @@ const ENCRYPTION_ALG = "AES-GCM";
|
||||
|
||||
// minimal
|
||||
export async function encryptString(
|
||||
plaintext: string,
|
||||
passphrase: string
|
||||
plaintext: string,
|
||||
passphrase: string,
|
||||
): Promise<string> {
|
||||
if (!window.crypto.subtle) return plaintext;
|
||||
if (!window.crypto.subtle) return plaintext;
|
||||
|
||||
const iv = generateIV();
|
||||
const key = await importKey(passphrase, "encrypt");
|
||||
const encryptedArray = await encrypt(iv, key, plaintext);
|
||||
const iv = generateIV();
|
||||
const key = await importKey(passphrase, "encrypt");
|
||||
const encryptedArray = await encrypt(iv, key, plaintext);
|
||||
|
||||
const encryptionData = {
|
||||
iv: uInt8ToArray(iv),
|
||||
encryptedArray: uInt8ToArray(encryptedArray),
|
||||
};
|
||||
return btoa(JSON.stringify(encryptionData));
|
||||
const encryptionData = {
|
||||
iv: uInt8ToArray(iv),
|
||||
encryptedArray: uInt8ToArray(encryptedArray),
|
||||
};
|
||||
return btoa(JSON.stringify(encryptionData));
|
||||
}
|
||||
|
||||
export async function decryptString(
|
||||
cyphertext: string,
|
||||
passphrase: string
|
||||
cyphertext: string,
|
||||
passphrase: string,
|
||||
): Promise<string> {
|
||||
try {
|
||||
const encrypionData = JSON.parse(atob(cyphertext));
|
||||
const iv = arrayToUint8(encrypionData.iv);
|
||||
const encryptedArray = arrayToUint8(encrypionData.encryptedArray);
|
||||
try {
|
||||
const encrypionData = JSON.parse(atob(cyphertext));
|
||||
const iv = arrayToUint8(encrypionData.iv);
|
||||
const encryptedArray = arrayToUint8(encrypionData.encryptedArray);
|
||||
|
||||
const key = await importKey(passphrase, "decrypt");
|
||||
return await decrypt(iv, key, encryptedArray);
|
||||
} catch {
|
||||
return cyphertext;
|
||||
}
|
||||
const key = await importKey(passphrase, "decrypt");
|
||||
return await decrypt(iv, key, encryptedArray);
|
||||
} catch {
|
||||
return cyphertext;
|
||||
}
|
||||
}
|
||||
|
||||
// process
|
||||
export function encode(string: string): Uint8Array {
|
||||
return new TextEncoder().encode(string);
|
||||
return new TextEncoder().encode(string);
|
||||
}
|
||||
|
||||
export function decode(array: Uint8Array): string {
|
||||
return new TextDecoder("utf-8").decode(array);
|
||||
return new TextDecoder("utf-8").decode(array);
|
||||
}
|
||||
|
||||
export async function encrypt(
|
||||
iv: Uint8Array,
|
||||
key: CryptoKey,
|
||||
message: string
|
||||
iv: Uint8Array,
|
||||
key: CryptoKey,
|
||||
message: string,
|
||||
): Promise<Uint8Array> {
|
||||
const arrayBuffer = await window.crypto.subtle.encrypt(
|
||||
{ name: ENCRYPTION_ALG, iv },
|
||||
key,
|
||||
encode(message)
|
||||
);
|
||||
return new Uint8Array(arrayBuffer);
|
||||
const arrayBuffer = await window.crypto.subtle.encrypt(
|
||||
{ name: ENCRYPTION_ALG, iv },
|
||||
key,
|
||||
encode(message),
|
||||
);
|
||||
return new Uint8Array(arrayBuffer);
|
||||
}
|
||||
|
||||
export async function decrypt(
|
||||
iv: Uint8Array,
|
||||
key: CryptoKey,
|
||||
cyphertext: Uint8Array
|
||||
iv: Uint8Array,
|
||||
key: CryptoKey,
|
||||
cyphertext: Uint8Array,
|
||||
): Promise<string> {
|
||||
const arrayBuffer = await crypto.subtle.decrypt(
|
||||
{ name: ENCRYPTION_ALG, iv },
|
||||
key,
|
||||
cyphertext
|
||||
);
|
||||
return arrayBufferToString(arrayBuffer);
|
||||
const arrayBuffer = await crypto.subtle.decrypt(
|
||||
{ name: ENCRYPTION_ALG, iv },
|
||||
key,
|
||||
cyphertext,
|
||||
);
|
||||
return arrayBufferToString(arrayBuffer);
|
||||
}
|
||||
|
||||
export async function hash(encoded: Uint8Array): Promise<ArrayBuffer> {
|
||||
return await crypto.subtle.digest("SHA-256", encoded);
|
||||
return await crypto.subtle.digest("SHA-256", encoded);
|
||||
}
|
||||
|
||||
// iv
|
||||
export function generateIV(): Uint8Array {
|
||||
return crypto.getRandomValues(new Uint8Array(IV_SIZE));
|
||||
return crypto.getRandomValues(new Uint8Array(IV_SIZE));
|
||||
}
|
||||
|
||||
export function splitMergedString(mergedString: string): [Uint8Array, string] {
|
||||
const iv = mergedString.slice(0, IV_SIZE);
|
||||
const encryptedString = mergedString.slice(IV_SIZE);
|
||||
const iv = mergedString.slice(0, IV_SIZE);
|
||||
const encryptedString = mergedString.slice(IV_SIZE);
|
||||
|
||||
return [JSON.parse(iv), encryptedString];
|
||||
return [JSON.parse(iv), encryptedString];
|
||||
}
|
||||
|
||||
// key
|
||||
export async function importKey(
|
||||
passphrase: string,
|
||||
purpose: "encrypt" | "decrypt"
|
||||
passphrase: string,
|
||||
purpose: "encrypt" | "decrypt",
|
||||
): Promise<CryptoKey> {
|
||||
return await crypto.subtle.importKey(
|
||||
"raw",
|
||||
await hash(encode(passphrase)),
|
||||
{ name: ENCRYPTION_ALG },
|
||||
false,
|
||||
[purpose]
|
||||
);
|
||||
return await crypto.subtle.importKey(
|
||||
"raw",
|
||||
await hash(encode(passphrase)),
|
||||
{ name: ENCRYPTION_ALG },
|
||||
false,
|
||||
[purpose],
|
||||
);
|
||||
}
|
||||
|
||||
// conversion
|
||||
export function arrayBufferToString(arrayBuffer: ArrayBuffer): string {
|
||||
const uInt8Array = new Uint8Array(arrayBuffer);
|
||||
return decode(uInt8Array);
|
||||
const uInt8Array = new Uint8Array(arrayBuffer);
|
||||
return decode(uInt8Array);
|
||||
}
|
||||
|
||||
export function uInt8ToArray(uInt8Array: Uint8Array): number[] {
|
||||
return Array.from(uInt8Array);
|
||||
return Array.from(uInt8Array);
|
||||
}
|
||||
|
||||
export function arrayToUint8(array: number[]): Uint8Array {
|
||||
return new Uint8Array(array);
|
||||
return new Uint8Array(array);
|
||||
}
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
export const DATA_VERSION = "v2";
|
||||
|
||||
export function checkIsValidObject(object: any): boolean {
|
||||
return object.dataVersion == DATA_VERSION;
|
||||
return object.dataVersion == DATA_VERSION;
|
||||
}
|
||||
|
||||
export function checkMatchesObjectStructure(
|
||||
objectToCheck: any,
|
||||
reference: any
|
||||
objectToCheck: any,
|
||||
reference: any,
|
||||
): boolean {
|
||||
if (typeof reference != "object") {
|
||||
return typeof objectToCheck == typeof reference;
|
||||
}
|
||||
|
||||
for (const key of Object.keys(reference)) {
|
||||
const requiredType = typeof reference[key];
|
||||
const actualType = typeof objectToCheck[key];
|
||||
if (requiredType != actualType) return false;
|
||||
|
||||
if (Array.isArray(reference[key]) || reference[key] instanceof Set) {
|
||||
// only check if array is not empty
|
||||
if (objectToCheck[key].length == 0) continue;
|
||||
if (objectToCheck[key].size == 0) continue;
|
||||
|
||||
// check first item of array
|
||||
const [firstOfObjectToCheck] = objectToCheck[key];
|
||||
const [fisrtOfReference] = reference[key];
|
||||
const doesFirstItemMatch = checkMatchesObjectStructure(
|
||||
firstOfObjectToCheck,
|
||||
fisrtOfReference
|
||||
);
|
||||
if (doesFirstItemMatch == false) return false;
|
||||
} else if (requiredType == "object") {
|
||||
// recurse into objects
|
||||
const doesNestedObjectMatch = checkMatchesObjectStructure(
|
||||
objectToCheck[key],
|
||||
reference[key]
|
||||
);
|
||||
if (doesNestedObjectMatch == false) return false;
|
||||
if (typeof reference != "object") {
|
||||
return typeof objectToCheck == typeof reference;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
for (const key of Object.keys(reference)) {
|
||||
const requiredType = typeof reference[key];
|
||||
const actualType = typeof objectToCheck[key];
|
||||
if (requiredType != actualType) return false;
|
||||
|
||||
if (Array.isArray(reference[key]) || reference[key] instanceof Set) {
|
||||
// only check if array is not empty
|
||||
if (objectToCheck[key].length == 0) continue;
|
||||
if (objectToCheck[key].size == 0) continue;
|
||||
|
||||
// check first item of array
|
||||
const [firstOfObjectToCheck] = objectToCheck[key];
|
||||
const [fisrtOfReference] = reference[key];
|
||||
const doesFirstItemMatch = checkMatchesObjectStructure(
|
||||
firstOfObjectToCheck,
|
||||
fisrtOfReference,
|
||||
);
|
||||
if (doesFirstItemMatch == false) return false;
|
||||
} else if (requiredType == "object") {
|
||||
// recurse into objects
|
||||
const doesNestedObjectMatch = checkMatchesObjectStructure(
|
||||
objectToCheck[key],
|
||||
reference[key],
|
||||
);
|
||||
if (doesNestedObjectMatch == false) return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export interface ValidObject {
|
||||
dataVersion: "v2";
|
||||
dataVersion: "v2";
|
||||
}
|
||||
|
||||
+182
-181
@@ -1,277 +1,278 @@
|
||||
import { checkIsValidObject, checkMatchesObjectStructure } from "./typeSafety";
|
||||
|
||||
export interface Stringifiable {
|
||||
toString(): string;
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
// crypto
|
||||
export function generateRandomToken(length: number): string {
|
||||
const array = new Uint8Array(length);
|
||||
crypto.getRandomValues(array);
|
||||
return array.join("");
|
||||
const array = new Uint8Array(length);
|
||||
crypto.getRandomValues(array);
|
||||
return array.join("");
|
||||
}
|
||||
|
||||
// date
|
||||
export function createTimestamp(): string {
|
||||
return new Date().toISOString();
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
// filters etc
|
||||
export type StringEntryObject = { [key: string]: Stringifiable | undefined };
|
||||
|
||||
export function filterObjectsByStringEntries<T>(
|
||||
reference: StringEntryObject,
|
||||
converter: (object: T) => StringEntryObject,
|
||||
objects: T[]
|
||||
reference: StringEntryObject,
|
||||
converter: (object: T) => StringEntryObject,
|
||||
objects: T[],
|
||||
): Set<T> {
|
||||
const matches: Set<T> = new Set();
|
||||
const matches: Set<T> = new Set();
|
||||
|
||||
object_loop: for (const object of objects) {
|
||||
const doesMatch: boolean = checkDoesObjectMatchReference(
|
||||
reference,
|
||||
converter(object)
|
||||
);
|
||||
if (doesMatch) matches.add(object);
|
||||
}
|
||||
object_loop: for (const object of objects) {
|
||||
const doesMatch: boolean = checkDoesObjectMatchReference(
|
||||
reference,
|
||||
converter(object),
|
||||
);
|
||||
if (doesMatch) matches.add(object);
|
||||
}
|
||||
|
||||
return matches;
|
||||
return matches;
|
||||
}
|
||||
|
||||
export function checkDoesObjectMatchReference(
|
||||
reference: StringEntryObject,
|
||||
stringEntryObject: StringEntryObject
|
||||
reference: StringEntryObject,
|
||||
stringEntryObject: StringEntryObject,
|
||||
): boolean {
|
||||
reference_entry_loop: for (const referenceEntry of Object.entries(
|
||||
reference
|
||||
)) {
|
||||
const [referenceKey, referenceValue] = referenceEntry;
|
||||
const stringEntryObjectValue: Stringifiable | undefined =
|
||||
stringEntryObject[referenceKey];
|
||||
reference_entry_loop: for (const referenceEntry of Object.entries(
|
||||
reference,
|
||||
)) {
|
||||
const [referenceKey, referenceValue] = referenceEntry;
|
||||
const stringEntryObjectValue: Stringifiable | undefined =
|
||||
stringEntryObject[referenceKey];
|
||||
|
||||
if (referenceValue == undefined) return false;
|
||||
if (referenceValue == undefined) return false;
|
||||
|
||||
if (referenceValue[0] == "-") {
|
||||
const strippedReferenceValue: string = referenceValue
|
||||
.toString()
|
||||
.substring(1);
|
||||
// property may not exist
|
||||
if (
|
||||
strippedReferenceValue == "" &&
|
||||
stringEntryObjectValue != undefined &&
|
||||
stringEntryObjectValue != ""
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (referenceValue[0] == "-") {
|
||||
const strippedReferenceValue: string = referenceValue
|
||||
.toString()
|
||||
.substring(1);
|
||||
// property may not exist
|
||||
if (
|
||||
strippedReferenceValue == "" &&
|
||||
stringEntryObjectValue != undefined &&
|
||||
stringEntryObjectValue != ""
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// property may not match
|
||||
if (stringEntryObjectValue == strippedReferenceValue) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// property must exist but be anything
|
||||
if (
|
||||
referenceValue == "" &&
|
||||
(stringEntryObjectValue == undefined || stringEntryObjectValue == "")
|
||||
) {
|
||||
return false;
|
||||
} else if (referenceValue == "") {
|
||||
continue reference_entry_loop;
|
||||
}
|
||||
// property may not match
|
||||
if (stringEntryObjectValue == strippedReferenceValue) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// property must exist but be anything
|
||||
if (
|
||||
referenceValue == "" &&
|
||||
(stringEntryObjectValue == undefined ||
|
||||
stringEntryObjectValue == "")
|
||||
) {
|
||||
return false;
|
||||
} else if (referenceValue == "") {
|
||||
continue reference_entry_loop;
|
||||
}
|
||||
|
||||
// property must match
|
||||
if (stringEntryObjectValue != referenceValue) {
|
||||
return false;
|
||||
}
|
||||
// property must match
|
||||
if (stringEntryObjectValue != referenceValue) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function collectObjectValuesForKey<T>(
|
||||
key: string,
|
||||
converter: (object: T) => StringEntryObject,
|
||||
objects: T[]
|
||||
key: string,
|
||||
converter: (object: T) => StringEntryObject,
|
||||
objects: T[],
|
||||
): string[] {
|
||||
const values: Set<string> = new Set();
|
||||
const values: Set<string> = new Set();
|
||||
|
||||
for (const object of objects) {
|
||||
const stringEntryObject: StringEntryObject = converter(object);
|
||||
const stringEntryObjectValue: Stringifiable | undefined =
|
||||
stringEntryObject[key];
|
||||
if (stringEntryObjectValue == undefined || stringEntryObjectValue == "")
|
||||
continue;
|
||||
for (const object of objects) {
|
||||
const stringEntryObject: StringEntryObject = converter(object);
|
||||
const stringEntryObjectValue: Stringifiable | undefined =
|
||||
stringEntryObject[key];
|
||||
if (stringEntryObjectValue == undefined || stringEntryObjectValue == "")
|
||||
continue;
|
||||
|
||||
values.add(stringEntryObjectValue.toString());
|
||||
}
|
||||
values.add(stringEntryObjectValue.toString());
|
||||
}
|
||||
|
||||
return [...values.values()];
|
||||
return [...values.values()];
|
||||
}
|
||||
|
||||
export function filterObjectsByWords<T>(
|
||||
query: string,
|
||||
getStringsOfObject: (object: T) => string[],
|
||||
objects: T[]
|
||||
query: string,
|
||||
getStringsOfObject: (object: T) => string[],
|
||||
objects: T[],
|
||||
): Set<T> {
|
||||
const matches: Set<T> = new Set();
|
||||
const matches: Set<T> = new Set();
|
||||
|
||||
object_loop: for (const object of objects) {
|
||||
const doesMatch: boolean = checkDoesObjectMatchSearch(
|
||||
query,
|
||||
getStringsOfObject,
|
||||
object
|
||||
);
|
||||
if (doesMatch) matches.add(object);
|
||||
}
|
||||
object_loop: for (const object of objects) {
|
||||
const doesMatch: boolean = checkDoesObjectMatchSearch(
|
||||
query,
|
||||
getStringsOfObject,
|
||||
object,
|
||||
);
|
||||
if (doesMatch) matches.add(object);
|
||||
}
|
||||
|
||||
return matches;
|
||||
return matches;
|
||||
}
|
||||
|
||||
export function checkDoesObjectMatchSearch<T>(
|
||||
query: string,
|
||||
getStringsOfObject: (object: T) => string[],
|
||||
object: T
|
||||
query: string,
|
||||
getStringsOfObject: (object: T) => string[],
|
||||
object: T,
|
||||
): boolean {
|
||||
if (query == "") return true;
|
||||
if (query == "") return true;
|
||||
|
||||
const stringsInObject: string[] = getStringsOfObject(object);
|
||||
const wordsInObject: string[] = [];
|
||||
for (const string of stringsInObject) {
|
||||
const lowercaseWordsInString = string
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.split(" ")
|
||||
.filter((word) => word != "");
|
||||
wordsInObject.push(...lowercaseWordsInString);
|
||||
}
|
||||
|
||||
const lowercaseWordsInQuery = query
|
||||
.toLowerCase()
|
||||
.split(" ")
|
||||
.filter((word) => word != "");
|
||||
for (const queryWord of lowercaseWordsInQuery) {
|
||||
if (queryWord[0] == "-") {
|
||||
// exclusion
|
||||
const wordContent = queryWord.substring(1);
|
||||
if (wordsInObject.includes(wordContent)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (wordsInObject.includes(queryWord) == false) {
|
||||
return false;
|
||||
}
|
||||
const stringsInObject: string[] = getStringsOfObject(object);
|
||||
const wordsInObject: string[] = [];
|
||||
for (const string of stringsInObject) {
|
||||
const lowercaseWordsInString = string
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.split(" ")
|
||||
.filter((word) => word != "");
|
||||
wordsInObject.push(...lowercaseWordsInString);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
const lowercaseWordsInQuery = query
|
||||
.toLowerCase()
|
||||
.split(" ")
|
||||
.filter((word) => word != "");
|
||||
for (const queryWord of lowercaseWordsInQuery) {
|
||||
if (queryWord[0] == "-") {
|
||||
// exclusion
|
||||
const wordContent = queryWord.substring(1);
|
||||
if (wordsInObject.includes(wordContent)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (wordsInObject.includes(queryWord) == false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// handlers
|
||||
export type Handler<T> = (item: T) => void;
|
||||
|
||||
export class HandlerManager<T> {
|
||||
handlers: Set<Handler<T>> = new Set();
|
||||
handlers: Set<Handler<T>> = new Set();
|
||||
|
||||
// manage
|
||||
addHandler = (handler: Handler<T>): void => {
|
||||
this.handlers.add(handler);
|
||||
};
|
||||
// manage
|
||||
addHandler = (handler: Handler<T>): void => {
|
||||
this.handlers.add(handler);
|
||||
};
|
||||
|
||||
deleteHandler = (handler: Handler<T>): void => {
|
||||
this.handlers.delete(handler);
|
||||
};
|
||||
deleteHandler = (handler: Handler<T>): void => {
|
||||
this.handlers.delete(handler);
|
||||
};
|
||||
|
||||
// trigger
|
||||
trigger = (item: T): void => {
|
||||
for (const handler of this.handlers) {
|
||||
handler(item);
|
||||
}
|
||||
};
|
||||
// trigger
|
||||
trigger = (item: T): void => {
|
||||
for (const handler of this.handlers) {
|
||||
handler(item);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// sorting
|
||||
export class IndexManager<T> {
|
||||
private itemToString: (item: T) => string;
|
||||
private itemToString: (item: T) => string;
|
||||
|
||||
sortedStrings: string[] = [];
|
||||
sortedStrings: string[] = [];
|
||||
|
||||
// methods
|
||||
update = (items: T[]): void => {
|
||||
this.sortedStrings = [];
|
||||
// methods
|
||||
update = (items: T[]): void => {
|
||||
this.sortedStrings = [];
|
||||
|
||||
let strings: string[] = [];
|
||||
for (const item of items) {
|
||||
const string: string = this.itemToString(item);
|
||||
strings.push(string);
|
||||
let strings: string[] = [];
|
||||
for (const item of items) {
|
||||
const string: string = this.itemToString(item);
|
||||
strings.push(string);
|
||||
}
|
||||
|
||||
this.sortedStrings = strings.sort(localeCompare);
|
||||
};
|
||||
|
||||
getIndex = (item: T): number => {
|
||||
const string: string = this.itemToString(item);
|
||||
const index: number = this.sortedStrings.indexOf(string);
|
||||
return index;
|
||||
};
|
||||
|
||||
// init
|
||||
constructor(itemToString: (item: T) => string) {
|
||||
this.itemToString = itemToString;
|
||||
}
|
||||
|
||||
this.sortedStrings = strings.sort(localeCompare);
|
||||
};
|
||||
|
||||
getIndex = (item: T): number => {
|
||||
const string: string = this.itemToString(item);
|
||||
const index: number = this.sortedStrings.indexOf(string);
|
||||
return index;
|
||||
};
|
||||
|
||||
// init
|
||||
constructor(itemToString: (item: T) => string) {
|
||||
this.itemToString = itemToString;
|
||||
}
|
||||
}
|
||||
|
||||
// storage
|
||||
export function getLocalStorageItemAndClear(key: string): string | null {
|
||||
const value: string | null = localStorage.getItem(key);
|
||||
localStorage.removeItem(key);
|
||||
if (value != null) localStorage.setItem(`_${key}`, value);
|
||||
return value;
|
||||
const value: string | null = localStorage.getItem(key);
|
||||
localStorage.removeItem(key);
|
||||
if (value != null) localStorage.setItem(`_${key}`, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
// string & parsing
|
||||
export function stringify(data: any): string {
|
||||
return JSON.stringify(data, null, 4);
|
||||
return JSON.stringify(data, null, 4);
|
||||
}
|
||||
|
||||
export function padZero(string: string | undefined, length: number): string {
|
||||
return (string ?? "").padStart(length, "0");
|
||||
return (string ?? "").padStart(length, "0");
|
||||
}
|
||||
|
||||
export function parse(string: string): any {
|
||||
try {
|
||||
return JSON.parse(string);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
return JSON.parse(string);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export function parseValidObject<T>(string: string, reference: T): T | null {
|
||||
const parsed: any = parse(string);
|
||||
if (checkIsValidObject(parsed) == false) return null;
|
||||
const parsed: any = parse(string);
|
||||
if (checkIsValidObject(parsed) == false) return null;
|
||||
|
||||
const doesMatchReference: boolean = checkMatchesObjectStructure(
|
||||
parsed,
|
||||
reference
|
||||
);
|
||||
if (doesMatchReference == false) return null;
|
||||
const doesMatchReference: boolean = checkMatchesObjectStructure(
|
||||
parsed,
|
||||
reference,
|
||||
);
|
||||
if (doesMatchReference == false) return null;
|
||||
|
||||
return parsed;
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export function parseOrFallback(inputString: string): any {
|
||||
try {
|
||||
return JSON.parse(inputString);
|
||||
} catch {
|
||||
return inputString;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(inputString);
|
||||
} catch {
|
||||
return inputString;
|
||||
}
|
||||
}
|
||||
|
||||
export function parseArray(inputString: string): any[] {
|
||||
const parsed: any = parseOrFallback(inputString);
|
||||
if (Array.isArray(parsed) == false) return [];
|
||||
return parsed;
|
||||
const parsed: any = parseOrFallback(inputString);
|
||||
if (Array.isArray(parsed) == false) return [];
|
||||
return parsed;
|
||||
}
|
||||
|
||||
// sort
|
||||
export function localeCompare(a: string, b: string): number {
|
||||
return a.localeCompare(b);
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
|
||||
+239
-228
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+4
-4
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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 == ""
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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 ?? "",
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -23,4 +23,4 @@
|
||||
opacity: 0.1;
|
||||
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
+114
-114
@@ -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
@@ -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
@@ -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),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user