Files
CT/dist/index.js
T
2024-07-22 12:39:26 +02:00

2571 lines
89 KiB
JavaScript

(() => {
// node_modules/uuid/dist/esm-browser/stringify.js
var byteToHex = [];
for (i = 0; i < 256; ++i) {
byteToHex.push((i + 256).toString(16).slice(1));
}
var i;
function unsafeStringify(arr, offset = 0) {
return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
}
// node_modules/uuid/dist/esm-browser/rng.js
var getRandomValues;
var rnds8 = new Uint8Array(16);
function rng() {
if (!getRandomValues) {
getRandomValues = typeof crypto !== "undefined" && crypto.getRandomValues && crypto.getRandomValues.bind(crypto);
if (!getRandomValues) {
throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");
}
}
return getRandomValues(rnds8);
}
// node_modules/uuid/dist/esm-browser/native.js
var randomUUID = typeof crypto !== "undefined" && crypto.randomUUID && crypto.randomUUID.bind(crypto);
var native_default = {
randomUUID
};
// node_modules/uuid/dist/esm-browser/v4.js
function v4(options, buf, offset) {
if (native_default.randomUUID && !buf && !options) {
return native_default.randomUUID();
}
options = options || {};
var rnds = options.random || (options.rng || rng)();
rnds[6] = rnds[6] & 15 | 64;
rnds[8] = rnds[8] & 63 | 128;
if (buf) {
offset = offset || 0;
for (var i = 0; i < 16; ++i) {
buf[offset + i] = rnds[i];
}
return buf;
}
return unsafeStringify(rnds);
}
var v4_default = v4;
// node_modules/bloatless-react/index.ts
var State = class {
_value;
_bindings = /* @__PURE__ */ new Set();
// init
constructor(initialValue) {
this._value = initialValue;
}
// value
get value() {
return this._value;
}
set value(newValue) {
if (this._value == newValue) return;
this._value = newValue;
this.callSubscriptions();
}
// subscriptions
callSubscriptions() {
this._bindings.forEach((fn) => fn(this._value));
}
subscribe(fn) {
this._bindings.add(fn);
fn(this._value);
}
subscribeSilent(fn) {
this._bindings.add(fn);
}
// stringify
toString() {
return JSON.stringify(this._value);
}
};
var ListState = class extends State {
additionHandlers = /* @__PURE__ */ new Set();
removalHandlers = /* @__PURE__ */ new Map();
// init
constructor(initialItems) {
super(new Set(initialItems));
}
// list
add(...items) {
items.forEach((item) => {
this.value.add(item);
this.additionHandlers.forEach((handler) => handler(item));
});
this.callSubscriptions();
}
remove(...items) {
items.forEach((item) => {
this.value.delete(item);
if (!this.removalHandlers.has(item)) return;
this.removalHandlers.get(item).forEach((handler) => handler(item));
this.removalHandlers.delete(item);
});
this.callSubscriptions();
}
clear() {
this.remove(...this.value.values());
}
// handlers
handleAddition(handler) {
this.additionHandlers.add(handler);
[...this.value.values()].forEach(handler);
}
handleRemoval(item, handler) {
if (!this.removalHandlers.has(item))
this.removalHandlers.set(item, /* @__PURE__ */ new Set());
this.removalHandlers.get(item).add(handler);
}
// stringification
toString() {
const array = [...this.value.values()];
const json = JSON.stringify(array);
return json;
}
};
var MapState = class extends State {
additionHandlers = /* @__PURE__ */ new Set();
removalHandlers = /* @__PURE__ */ new Map();
// init
constructor(initialItems) {
super(new Map(initialItems));
}
// list
set(key, item) {
this.remove(key);
this.value.set(key, item);
this.additionHandlers.forEach((handler) => handler(item));
this.callSubscriptions();
}
remove(key) {
const item = this.value.get(key);
if (!item) return;
this.value.delete(key);
this.callSubscriptions();
if (!this.removalHandlers.has(item)) return;
this.removalHandlers.get(item).forEach((handler) => handler(item));
this.removalHandlers.delete(item);
}
clear() {
[...this.value.keys()].forEach((key) => this.remove(key));
}
// handlers
handleAddition(handler) {
this.additionHandlers.add(handler);
[...this.value.values()].forEach(handler);
}
handleRemoval(item, handler) {
if (!this.removalHandlers.has(item))
this.removalHandlers.set(item, /* @__PURE__ */ new Set());
this.removalHandlers.get(item).add(handler);
}
// stringification
toString() {
const array = [...this.value.entries()];
const json = JSON.stringify(array);
return json;
}
};
function createProxyState(statesToSubscibe, fn) {
const proxyState = new State(fn());
statesToSubscibe.forEach(
(state) => state.subscribe(() => proxyState.value = fn())
);
return proxyState;
}
function createElement(tagName, attributes = {}, ...children) {
const element = document.createElement(tagName);
if (attributes != null)
Object.entries(attributes).forEach((entry) => {
const [attributename, value] = entry;
const [directiveKey, directiveValue] = attributename.split(":");
switch (directiveKey) {
case "on": {
switch (directiveValue) {
case "enter": {
element.addEventListener("keydown", (e) => {
if (e.key != "Enter") return;
value();
});
break;
}
default: {
element.addEventListener(directiveValue, value);
}
}
break;
}
case "subscribe": {
const state = value;
state.subscribe(
(newValue) => element[directiveValue] = newValue
);
break;
}
case "bind": {
const state = value;
state.subscribe(
(newValue) => element[directiveValue] = newValue
);
element.addEventListener(
"input",
() => state.value = element[directiveValue]
);
break;
}
case "toggle": {
if (value.subscribe) {
const state = value;
state.subscribe(
(newValue) => element.toggleAttribute(directiveValue, newValue)
);
} else {
element.toggleAttribute(directiveValue, value);
}
break;
}
case "set": {
const state = value;
state.subscribe(
(newValue) => element.setAttribute(directiveValue, newValue)
);
break;
}
case "children": {
switch (directiveValue) {
case "set": {
const state = value;
state.subscribe((newValue) => {
element.innerHTML = "";
element.append(...[newValue].flat());
});
break;
}
case "append":
case "prepend": {
try {
const [listState, toElement] = value;
listState.handleAddition((newItem) => {
const child = toElement(newItem);
listState.handleRemoval(
newItem,
() => child.remove()
);
if (directiveValue == "append") {
element.append(child);
} else if (directiveValue == "prepend") {
element.prepend(child);
}
});
} catch (error) {
throw `error: cannot process subscribe:children directive.
Error: ${error}
Usage: "subscribe:children={[list, converter]}"; you can find a more detailed example in the documentation.`;
}
}
}
break;
}
default:
element.setAttribute(attributename, value);
}
});
children.filter((x) => x).forEach((child) => element.append(child));
return element;
}
// src/Model/Utility/typeSafety.ts
var DATA_VERSION = "v2";
function checkIsValidObject(object) {
return object.dataVersion == DATA_VERSION;
}
function checkMatchesObjectStructure(objectToCheck, reference) {
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) {
if (objectToCheck[key].length == 0) continue;
if (objectToCheck[key].size == 0) continue;
const [firstOfObjectToCheck] = objectToCheck[key];
const [fisrtOfReference] = reference[key];
const doesFirstItemMatch = checkMatchesObjectStructure(
firstOfObjectToCheck,
fisrtOfReference
);
if (doesFirstItemMatch == false) return false;
} else if (requiredType == "object") {
const doesNestedObjectMatch = checkMatchesObjectStructure(
objectToCheck[key],
reference[key]
);
if (doesNestedObjectMatch == false) return false;
}
}
return true;
}
// src/Model/Utility/utility.ts
function createTimestamp() {
return (/* @__PURE__ */ new Date()).toISOString();
}
function stringify(data) {
return JSON.stringify(data, null, 4);
}
function parse(string) {
try {
return JSON.parse(string);
} catch {
return {};
}
}
function parseValidObject(string, reference) {
const parsed = parse(string);
if (checkIsValidObject(parsed) == false) return null;
const doesMatchReference = checkMatchesObjectStructure(
parsed,
reference
);
if (doesMatchReference == false) return null;
return parsed;
}
function localeCompare(a, b) {
return a.localeCompare(b);
}
// src/Model/Global/storageModel.ts
var PATH_COMPONENT_SEPARATOR = "\\";
var StorageModel = class _StorageModel {
storageEntryTree = {};
// basic
write = (pathComponents, value) => {
const pathString = _StorageModel.pathComponentsToString(
...pathComponents
);
localStorage.setItem(pathString, value);
this.updateTree(...pathComponents);
};
read = (pathComponents) => {
const pathString = _StorageModel.pathComponentsToString(
...pathComponents
);
return localStorage.getItem(pathString);
};
remove = (pathComponents, shouldInitialize = true) => {
const pathString = _StorageModel.pathComponentsToString(
...pathComponents
);
localStorage.removeItem(pathString);
if (shouldInitialize == true) {
this.initializeTree();
}
};
removeRecursively = (pathComponentsOfEntityToDelete) => {
a: for (const key of Object.keys(localStorage)) {
const pathComponentsOfCurrentEntity = _StorageModel.stringToPathComponents(key);
for (let i = 0; i < pathComponentsOfEntityToDelete.length; i++) {
if (!pathComponentsOfCurrentEntity[i]) continue a;
if (pathComponentsOfCurrentEntity[i] != pathComponentsOfEntityToDelete[i])
continue a;
}
this.remove(pathComponentsOfCurrentEntity, false);
}
this.initializeTree();
};
list = (pathComponents) => {
let currentParent = this.storageEntryTree;
for (const component of pathComponents) {
const nextParent = currentParent[component];
if (nextParent == void 0) return [];
currentParent = nextParent;
}
return [...Object.keys(currentParent).sort(localeCompare)];
};
// stringifiable
writeStringifiable = (pathComponents, value) => {
const valueString = stringify(value);
this.write(pathComponents, valueString);
};
readStringifiable = (pathComponents, reference) => {
const valueString = this.read(pathComponents);
if (!valueString) return null;
const object = parseValidObject(valueString, reference);
if (object == null) return null;
return object;
};
// tree
initializeTree = () => {
console.log("initializing tree");
this.storageEntryTree = {};
for (const key of Object.keys(localStorage)) {
const components = _StorageModel.stringToPathComponents(key);
this.updateTree(...components);
}
};
updateTree = (...pathComponents) => {
let currentParent = this.storageEntryTree;
for (const pathPart of pathComponents) {
if (!currentParent[pathPart]) {
currentParent[pathPart] = {};
}
currentParent = currentParent[pathPart];
}
};
printTree = () => {
return stringify(this.storageEntryTree);
};
// init
constructor() {
this.initializeTree();
}
// utility
static getFileName = (pathComponents) => {
return pathComponents[pathComponents.length - 1] || "\\";
};
static getFileNameFromString = (pathString) => {
const pathComponents = this.stringToPathComponents(pathString);
return pathComponents[pathComponents.length - 1] || "\\";
};
static pathComponentsToString = (...pathComponents) => {
return pathComponents.join(PATH_COMPONENT_SEPARATOR);
};
static stringToPathComponents = (string) => {
return string.split(PATH_COMPONENT_SEPARATOR).filter((x) => x != "");
};
static join = (...items) => {
let allComponents = [];
for (const item of items) {
const parts = this.stringToPathComponents(item);
allComponents.push(...parts);
}
return _StorageModel.pathComponentsToString(...allComponents);
};
static getPath(locationName, filePath) {
return [DATA_VERSION, storageLocations[locationName], ...filePath];
}
};
var storageLocations = {
connectionModel: "connection",
chat: "chat",
settingsModel: "settings"
};
var filePaths = {
connectionModel: {
socketAddress: ["socket-address"],
reconnectAddress: ["reconnect-address"],
outbox: ["outbox"],
mailboxes: ["mailboxes"],
previousAddresses: ["previous-addresses"]
},
chat: {
base: [],
chatBase: (id) => [id],
info: (id) => [...filePaths.chat.chatBase(id), "info"],
color: (id) => [...filePaths.chat.chatBase(id), "color"],
messages: (id) => [...filePaths.chat.chatBase(id), "messages"],
files: (id) => [...filePaths.chat.chatBase(id), "files"],
lastUsedPage: (id) => [
...filePaths.chat.chatBase(id),
"last-used-page"
]
},
settingsModel: {
username: ["user-name"],
firstDayOfWeek: ["first-day-of-week"]
}
};
// src/Model/Files/taskModel.ts
var TaskModel = class _TaskModel {
chatModel;
fileModel;
// handler
handleTaskFileContent = (file, fileContent) => {
if (checkMatchesObjectStructure(fileContent, TaskFileContentReference) == false)
return;
};
// methods
createTask = (board, name) => {
const file = FileModel.createFile();
const taskFileContent = _TaskModel.createTaskFileContent(
board,
name
);
this.saveTask(file, taskFileContent);
};
saveTask = (file, newTaskFileContent) => {
file.contentVersions.add(newTaskFileContent);
this.fileModel.addOrUpdateFile(file);
};
// init
constructor(chatModel, fileModel) {
this.chatModel = chatModel;
this.fileModel = fileModel;
}
// utility
static createTaskFileContent = (name, board) => {
const fileContent = FileModel.createFileContent("task");
return {
...fileContent,
name,
board
};
};
};
var TaskFileContentReference = {
dataVersion: DATA_VERSION,
id: "",
creationDate: "",
type: "task",
name: "",
board: ""
};
// src/Model/Files/fileModel.ts
var FileModel = class _FileModel {
chatModel;
storageModel;
taskModel;
// paths
getBasePath = () => {
return StorageModel.getPath(
"chat",
filePaths.chat.files(this.chatModel.id)
);
};
getFilePath = (fileId) => {
return [...this.getBasePath(), fileId];
};
getFileContentPath = (file, fileContentName) => {
const filePath = this.getFilePath(file.id);
return [...filePath, fileContentName];
};
// handler
handleStringifiedFile = (stringifiedFile) => {
const file = parseValidObject(stringifiedFile, FileReference);
if (file == null) return;
this.handleFileAndClearContents(file);
};
handleFileAndClearContents = (file) => {
for (const fileContent of file.contentVersions) {
this.storeFileContent(file, fileContent);
this.taskModel.handleTaskFileContent(file, fileContent);
file.contentVersions.delete(fileContent);
}
};
// methods
addOrUpdateFile = (file) => {
this.chatModel.sendMessage("", file);
this.handleFileAndClearContents(file);
};
// storage
storeFileContent = (file, fileContent) => {
const fileContentName = _FileModel.getFileContentName(fileContent);
const fileContentPath = this.getFileContentPath(
file,
fileContentName
);
const existingFileContent = this.storageModel.read(fileContentPath);
if (existingFileContent != null) return;
const stringifiedContent = stringify(fileContent);
this.storageModel.write(fileContentPath, stringifiedContent);
};
listFileIds = () => {
return this.storageModel.list(this.getBasePath());
};
getFile = (fileId) => {
const filePath = this.getFilePath(fileId);
const fileOrNull = this.storageModel.readStringifiable(
filePath,
FileReference
);
return fileOrNull;
};
listFileContents = (file) => {
const filePath = this.getFilePath(file.id);
return this.storageModel.list(filePath);
};
getLatestFileContentName = (fileContentNames) => {
return fileContentNames[fileContentNames.length - 1];
};
getFileContent = (file, fileContentName) => {
const filePath = this.getFileContentPath(file, fileContentName);
const fileContentOrNull = this.storageModel.readStringifiable(filePath, FileContentReference);
return fileContentOrNull;
};
// init
constructor(chatModel, storageModel2) {
this.chatModel = chatModel;
this.storageModel = storageModel2;
this.taskModel = new TaskModel(chatModel, this);
}
// utility
static getFileContentName = (fileContent) => {
return fileContent.creationDate + fileContent.id;
};
static createFile = () => {
return {
dataVersion: DATA_VERSION,
id: v4_default(),
contentVersions: /* @__PURE__ */ new Set()
};
};
static createFileContent = (type) => {
return {
dataVersion: DATA_VERSION,
id: v4_default(),
creationDate: createTimestamp(),
type
};
};
};
var FileContentReference = {
dataVersion: DATA_VERSION,
id: "",
creationDate: "",
type: ""
};
var FileReference = {
dataVersion: DATA_VERSION,
id: "",
contentVersions: /* @__PURE__ */ new Set([FileContentReference])
};
// src/Model/Utility/crypto.ts
var IV_SIZE = 12;
var ENCRYPTION_ALG = "AES-GCM";
async function encryptString(plaintext, passphrase) {
if (!window.crypto.subtle) return 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));
}
async function decryptString(cyphertext, passphrase) {
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;
}
}
function encode(string) {
return new TextEncoder().encode(string);
}
function decode(array) {
return new TextDecoder("utf-8").decode(array);
}
async function encrypt(iv, key, message) {
const arrayBuffer = await window.crypto.subtle.encrypt(
{ name: ENCRYPTION_ALG, iv },
key,
encode(message)
);
return new Uint8Array(arrayBuffer);
}
async function decrypt(iv, key, cyphertext) {
const arrayBuffer = await crypto.subtle.decrypt(
{ name: ENCRYPTION_ALG, iv },
key,
cyphertext
);
return arrayBufferToString(arrayBuffer);
}
async function hash(encoded) {
return await crypto.subtle.digest("SHA-256", encoded);
}
function generateIV() {
return crypto.getRandomValues(new Uint8Array(IV_SIZE));
}
async function importKey(passphrase, purpose) {
return await crypto.subtle.importKey(
"raw",
await hash(encode(passphrase)),
{ name: ENCRYPTION_ALG },
false,
[purpose]
);
}
function arrayBufferToString(arrayBuffer) {
const uInt8Array = new Uint8Array(arrayBuffer);
return decode(uInt8Array);
}
function uInt8ToArray(uInt8Array) {
return Array.from(uInt8Array);
}
function arrayToUint8(array) {
return new Uint8Array(array);
}
// src/ViewModel/colors.ts
var Color = /* @__PURE__ */ ((Color2) => {
Color2["Standard"] = "standard";
Color2["Coral"] = "coral";
Color2["Yellow"] = "yellow";
Color2["Mint"] = "mint";
Color2["LightBlue"] = "lightblue";
Color2["Blue"] = "blue";
Color2["purple"] = "purple";
return Color2;
})(Color || {});
// src/Model/Chat/chatModel.ts
var ChatModel = class _ChatModel {
connectionModel;
storageModel;
settingsModel;
chatListModel;
fileModel;
// data
id;
info;
color;
chatMessageHandler = () => {
};
// paths
getBasePath = () => {
return StorageModel.getPath("chat", filePaths.chat.chatBase(this.id));
};
getInfoPath = () => {
return StorageModel.getPath("chat", filePaths.chat.info(this.id));
};
getColorPath = () => {
return StorageModel.getPath("chat", filePaths.chat.color(this.id));
};
getMessageDirPath = () => {
return StorageModel.getPath("chat", filePaths.chat.messages(this.id));
};
getMessagePath = (id) => {
return [...this.getMessageDirPath(), id];
};
// sorting
get index() {
return this.chatListModel.getIndexOfPrimaryChannel(
this.info.primaryChannel
);
}
// settings
setPrimaryChannel = (primaryChannel) => {
this.info.primaryChannel = primaryChannel;
this.storeInfo();
this.chatListModel.updateIndices();
this.subscribe();
};
setSecondaryChannels = (secondaryChannels) => {
this.info.secondaryChannels = secondaryChannels;
this.storeInfo();
};
setEncryptionKey = (key) => {
this.info.encryptionKey = key;
this.storeInfo();
};
setColor = (color) => {
this.color = color;
this.storeColor();
};
// messaging
sendMessage = async (body, file) => {
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 = allChannels.join("/");
const chatMessage = await _ChatModel.createChatMessage(
combinedChannel,
senderName,
this.info.encryptionKey,
body,
file
);
this.addMessage(chatMessage);
this.connectionModel.sendMessageOrStore(chatMessage);
return true;
};
handleMessage = (body) => {
const chatMessage = parseValidObject(
body,
ChatMessageReference
);
if (chatMessage == null) return;
chatMessage.status = "received" /* Received */;
this.addMessage(chatMessage);
};
handleMessageSent = (chatMessage) => {
chatMessage.status = "sent" /* Sent */;
this.addMessage(chatMessage);
};
decryptMessage = async (chatMessage) => {
const decryptedBody = await decryptString(
chatMessage.body,
this.info.encryptionKey
);
const decryptedFile = await decryptString(
chatMessage.stringifiedFile ?? "",
this.info.encryptionKey
);
chatMessage.body = decryptedBody;
chatMessage.stringifiedFile = decryptedFile;
};
setMessageHandler = (handler) => {
this.chatMessageHandler = handler;
};
subscribe = () => {
this.connectionModel.addChannel(this.info.primaryChannel);
};
// storage
storeInfo = () => {
this.storageModel.writeStringifiable(this.getInfoPath(), this.info);
};
storeColor = () => {
this.storageModel.write(this.getColorPath(), this.color);
};
addMessage = async (chatMessage) => {
await this.decryptMessage(chatMessage);
if (chatMessage.body != "") {
const messagePath = this.getMessagePath(chatMessage.id);
this.storageModel.writeStringifiable(messagePath, chatMessage);
this.chatMessageHandler(chatMessage);
}
this.fileModel.handleStringifiedFile(chatMessage.stringifiedFile);
};
delete = () => {
this.chatListModel.untrackChat(this);
const dirPath = this.getBasePath();
this.storageModel.removeRecursively(dirPath);
};
// load
loadInfo = () => {
const info = this.storageModel.readStringifiable(
this.getInfoPath(),
ChatInfoReference
);
if (info != null) {
this.info = info;
} else {
this.info = _ChatModel.generateChatInfo("0");
}
};
loadColor = () => {
const path = this.getColorPath();
const color = this.storageModel.read(path);
if (!color) {
this.color = "standard" /* Standard */;
} else {
this.color = color;
}
};
get messages() {
const messageIds = this.storageModel.list(
this.getMessageDirPath()
);
if (!Array.isArray(messageIds)) return [];
const chatMessages = [];
for (const messageId of messageIds) {
const messagePath = this.getMessagePath(messageId);
const chatMessage = this.storageModel.readStringifiable(messagePath, ChatMessageReference);
chatMessages.push(chatMessage);
}
const sorted = chatMessages.sort(
(a, b) => a.dateSent.localeCompare(b.dateSent)
);
return sorted;
}
// init
constructor(storageModel2, connectionModel2, settingsModel2, chatListModel2, chatId) {
this.id = chatId;
this.connectionModel = connectionModel2;
this.settingsModel = settingsModel2;
this.storageModel = storageModel2;
this.chatListModel = chatListModel2;
this.loadInfo();
this.loadColor();
this.subscribe();
connectionModel2.setMessageSentHandler(this.handleMessageSent);
this.fileModel = new FileModel(this, this.storageModel);
}
// utility
static generateChatInfo = (primaryChannel) => {
return {
dataVersion: DATA_VERSION,
primaryChannel,
secondaryChannels: [],
encryptionKey: "",
hasUnreadMessages: false
};
};
static createChatMessage = async (channel, sender, encryptionKey, body, file) => {
const chatMessage = {
dataVersion: DATA_VERSION,
id: v4_default(),
channel,
sender,
body,
dateSent: createTimestamp(),
status: "outbox" /* Outbox */,
stringifiedFile: ""
};
if (file != void 0) {
const stringifiedFile = stringify(file);
chatMessage.stringifiedFile = stringifiedFile;
}
if (encryptionKey != "") {
chatMessage.body = await encryptString(chatMessage.body, encryptionKey);
chatMessage.stringifiedFile = await encryptString(
chatMessage.stringifiedFile,
encryptionKey
);
}
return chatMessage;
};
};
var ChatInfoReference = {
dataVersion: DATA_VERSION,
primaryChannel: "",
secondaryChannels: [""],
encryptionKey: "",
hasUnreadMessages: true
};
var ChatMessageReference = {
dataVersion: DATA_VERSION,
id: "",
channel: "",
sender: "",
body: "",
dateSent: "",
status: "",
stringifiedFile: ""
};
// src/Model/Chat/chatListModel.ts
var ChatListModel = class {
storageModel;
settingsModel;
connectionModel;
// data
chatModels = /* @__PURE__ */ new Set();
sortedPrimaryChannels = [];
// handlers
messageHandler = (data) => {
for (const chatModel of this.chatModels) {
const channel = data.messageChannel;
const body = data.messageBody;
if (channel == void 0) return;
if (body == void 0) return;
const allChannels = channel.split("/");
for (const channel2 of allChannels) {
if (channel2 != chatModel.info.primaryChannel) continue;
chatModel.handleMessage(body);
break;
}
}
};
// sorting
updateIndices = () => {
this.sortedPrimaryChannels = [];
let allChannels = [];
for (const chatModel of this.chatModels) {
allChannels.push(chatModel.info.primaryChannel);
}
this.sortedPrimaryChannels = allChannels.sort(localeCompare);
};
getIndexOfPrimaryChannel(primaryChannel) {
return this.sortedPrimaryChannels.indexOf(primaryChannel);
}
// storage
addChatModel = (chatModel) => {
this.chatModels.add(chatModel);
this.updateIndices();
};
createChat = (primaryChannel) => {
const id = v4_default();
const chatModel = new ChatModel(
this.storageModel,
this.connectionModel,
this.settingsModel,
this,
id
);
chatModel.setPrimaryChannel(primaryChannel);
this.addChatModel(chatModel);
return chatModel;
};
untrackChat = (chat) => {
this.chatModels.delete(chat);
this.updateIndices();
};
// load
loadChats = () => {
const chatDir = StorageModel.getPath("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(storageModel2, settingsModel2, connectionModel2) {
this.storageModel = storageModel2;
this.settingsModel = settingsModel2;
this.connectionModel = connectionModel2;
this.loadChats();
connectionModel2.setMessageHandler(this.messageHandler);
}
};
// src/ViewModel/chatMessageViewModel.ts
var ChatMessageViewModel = class {
chatViewModel;
// data
chatMessage;
channel;
sender;
dateSent;
body = new State("");
status = new State(void 0);
sentByUser;
// methods
copyMessage = () => {
navigator.clipboard.writeText(this.body.value);
};
resendMessage = () => {
this.chatViewModel.sendMessageFromBody(this.body.value);
};
decryptMessage = () => {
this.chatViewModel.decryptMessage(this);
};
// load
loadData = () => {
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(chatViewModel, chatMessage, sentByUser) {
this.chatViewModel = chatViewModel;
this.chatMessage = chatMessage;
this.sentByUser = sentByUser;
this.loadData();
}
};
// src/ViewModel/chatViewModel.ts
var ChatViewModel = class {
chatModel;
storageModel;
settingsViewModel;
chatListViewModel;
// state
index = new State(0);
primaryChannel = new State("");
primaryChannelInput = new State("");
secondaryChannels = new ListState();
newSecondaryChannelInput = new State("");
encryptionKeyInput = new State("");
shouldShowEncryptionKey = new State(false);
encryptionKeyInputType = createProxyState(
[this.shouldShowEncryptionKey],
() => this.shouldShowEncryptionKey.value == true ? "text" : "password"
);
color = new State("standard" /* Standard */);
selectedPage = new State(
"messages" /* Messages */
);
chatMessageViewModels = new MapState();
composingMessage = new State("");
// guards
cannotSetPrimaryChannel = createProxyState(
[this.primaryChannel, this.primaryChannelInput],
() => this.primaryChannelInput.value == "" || this.primaryChannelInput.value == this.primaryChannel.value
);
cannotAddSecondaryChannel = createProxyState(
[this.newSecondaryChannelInput],
() => this.newSecondaryChannelInput.value == ""
);
cannotSetEncryptionKey;
cannotSendMessage;
// sorting
updateIndex = () => {
this.index.value = this.chatModel.index;
};
// view
open = () => {
this.chatListViewModel.openChat(this);
this.loadMessages();
};
close = () => {
this.chatListViewModel.closeChat();
};
// add
addChatMessage = (chatMessage) => {
const chatMessageModel = new ChatMessageViewModel(
this,
chatMessage,
chatMessage.sender == this.settingsViewModel.username.value
);
const existingChatMessageViewModel = this.chatMessageViewModels.value.get(chatMessage.id);
if (existingChatMessageViewModel != void 0) {
existingChatMessageViewModel.body.value = chatMessage.body;
existingChatMessageViewModel.status.value = chatMessage.status;
} else {
this.chatMessageViewModels.set(chatMessage.id, chatMessageModel);
}
};
// settings
setPrimaryChannel = () => {
this.chatModel.setPrimaryChannel(this.primaryChannelInput.value);
this.primaryChannel.value = this.chatModel.info.primaryChannel;
this.chatListViewModel.updateIndices();
};
addSecondaryChannel = () => {
this.secondaryChannels.add(this.newSecondaryChannelInput.value);
this.newSecondaryChannelInput.value = "";
this.storeSecondaryChannels();
this.loadSecondaryChannels();
};
removeSecondaryChannel = (secondaryChannel) => {
this.secondaryChannels.remove(secondaryChannel);
this.storeSecondaryChannels();
};
storeSecondaryChannels = () => {
this.chatModel.setSecondaryChannels([
...this.secondaryChannels.value.values()
]);
};
setEncryptionKey = () => {
this.chatModel.setEncryptionKey(this.encryptionKeyInput.value);
this.encryptionKeyInput.callSubscriptions();
};
setColor = (newColor) => {
this.color.value = newColor;
this.chatModel.setColor(newColor);
};
remove = () => {
this.close();
this.chatModel.delete();
this.chatListViewModel.loadChats();
};
// messaging
sendMessage = () => {
if (this.cannotSendMessage.value == true) return;
this.sendMessageFromBody(this.composingMessage.value);
this.composingMessage.value = "";
};
sendMessageFromBody = (body) => {
this.chatModel.sendMessage(body);
};
decryptMessage = async (messageViewModel) => {
const chatMessage = messageViewModel.chatMessage;
await this.chatModel.decryptMessage(chatMessage);
this.chatModel.addMessage(chatMessage);
messageViewModel.loadData();
};
// load
loadPageSelection = () => {
const path = StorageModel.getPath(
"chat",
filePaths.chat.lastUsedPage(this.chatModel.id)
);
const lastUsedPage = this.storageModel.read(path);
if (lastUsedPage != null) {
this.selectedPage.value = lastUsedPage;
}
this.selectedPage.subscribeSilent((newPage) => {
this.storageModel.write(path, newPage);
});
};
loadSecondaryChannels = () => {
this.secondaryChannels.clear();
for (const secondaryChannel of this.chatModel.info.secondaryChannels.sort(
localeCompare
)) {
this.secondaryChannels.add(secondaryChannel);
}
};
loadMessages = () => {
for (const chatMessage of this.chatModel.messages) {
this.addChatMessage(chatMessage);
}
};
// init
constructor(chatModel, storageModel2, settingsViewModel2, chatListViewModel2) {
this.chatModel = chatModel;
this.storageModel = storageModel2;
this.settingsViewModel = settingsViewModel2;
this.chatListViewModel = chatListViewModel2;
chatModel.setMessageHandler((chatMessage) => {
this.addChatMessage(chatMessage);
});
this.primaryChannel.value = chatModel.info.primaryChannel;
this.primaryChannelInput.value = chatModel.info.primaryChannel;
this.encryptionKeyInput.value = chatModel.info.encryptionKey;
this.cannotSetEncryptionKey = createProxyState(
[this.encryptionKeyInput],
() => this.encryptionKeyInput.value == this.chatModel.info.encryptionKey
);
this.color.value = chatModel.color;
this.loadSecondaryChannels();
this.loadPageSelection();
this.updateIndex();
this.cannotSendMessage = createProxyState(
[this.settingsViewModel.username, this.composingMessage],
() => this.settingsViewModel.username.value == "" || this.composingMessage.value == ""
);
}
};
// src/ViewModel/chatListViewModel.ts
var ChatListViewModel = class {
chatListModel;
storageModel;
settingsViewModel;
// state
newChatPrimaryChannel = new State("");
chatViewModels = new ListState();
selectedChat = new State(void 0);
// guards
cannotCreateChat = createProxyState(
[this.newChatPrimaryChannel],
() => this.newChatPrimaryChannel.value == ""
);
// methods
createChat = () => {
const chatModel = this.chatListModel.createChat(
this.newChatPrimaryChannel.value
);
this.newChatPrimaryChannel.value = "";
const chatViewModel = this.createChatViewModel(chatModel);
this.chatViewModels.add(chatViewModel);
this.updateIndices();
};
untrackChat = (chatViewModel) => {
this.chatListModel.untrackChat(chatViewModel.chatModel);
this.chatViewModels.remove(chatViewModel);
};
createChatViewModel = (chatModel) => {
return new ChatViewModel(
chatModel,
this.storageModel,
this.settingsViewModel,
this
);
};
// view
openChat = (chatViewModel) => {
this.selectedChat.value = chatViewModel;
};
closeChat = () => {
this.selectedChat.value = void 0;
};
// sorting
updateIndices = () => {
for (const chatViewModel of this.chatViewModels.value) {
chatViewModel.updateIndex();
}
};
// load
loadChats = () => {
this.chatViewModels.clear();
for (const chatModel of this.chatListModel.chatModels.values()) {
const chatViewModel = this.createChatViewModel(chatModel);
this.chatViewModels.add(chatViewModel);
}
};
// init
constructor(chatListModel2, storageModel2, settingsViewModel2) {
this.chatListModel = chatListModel2;
this.storageModel = storageModel2;
this.settingsViewModel = settingsViewModel2;
this.loadChats();
}
};
// src/View/Components/chatViewToggleButton.tsx
function ChatViewToggleButton(label, icon, page, chatViewModel) {
function select() {
chatViewModel.selectedPage.value = page;
}
const isSelected = createProxyState(
[chatViewModel.selectedPage],
() => chatViewModel.selectedPage.value == page
);
return /* @__PURE__ */ createElement(
"button",
{
class: "ghost",
"aria-label": label,
"toggle:selected": isSelected,
"on:click": select
},
/* @__PURE__ */ createElement("span", { class: "icon" }, icon)
);
}
// src/View/translations.ts
var englishTranslations = {
general: {
closeButton: "Close",
deleteItemButtonAudioLabel: "delete item",
abortButton: "Abort",
confirmButton: "Confirm",
setButton: "Set"
},
regional: {
weekdays: {
full: [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday"
],
abbreviated: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
}
},
homePage: {
appName: "Comms",
///
overviewHeadline: "Overview",
serverAddress: "Server address",
serverAddressPlaceholder: "wss://192.168.0.69:3000",
connectAudioLabel: "connect to server",
disconnectAudioLabel: "disconnect from server",
manageConnectionsAudioLabel: "manage connections",
yourNameLabel: "Your name",
yourNamePlaceholder: "Jane Doe",
setNameButtonAudioLabel: "set name",
firstDayOfWeekLabel: "First day of week",
manageStorageButton: "Manage storage",
scrollToChatButton: "Chats",
///
backToOverviewAudioLabel: "go back to overview",
chatsHeadline: "Chats",
addChatAudioLabel: "name of new chat",
addChatPlaceholder: "Add chat",
addChatButton: "Add chat"
},
connectionModal: {
connectionModalHeadline: "Manage Connections",
///
connectButtonAudioLabel: "connect"
},
storage: {
noItemSelected: "No item selected",
notAFile: "(not a file)",
contentEmpty: "(empty)",
path: "Path",
content: "Content",
deleteItem: "Delete item"
},
chatPage: {
closeChatAudioLabe: "close chat",
chatSettingsAudioLabel: "chat settings",
pages: {
settings: "Settings",
messages: "Messages",
allObjects: "All objects",
kanban: "Kanban",
calendar: "Calendar",
progress: "Progress"
},
settings: {
settingsHeadline: "Settings",
primaryChannelLabel: "Primary channel",
setPrimaryChannelButtonAudioLabel: "set primary channel",
newSecondaryChannelPlaceholder: "Add secondary channel",
newSecondaryChannelAudioLabel: "name of new secondary channel",
addSecondaryChannelButtonAudioLabel: "add secondary channel",
encryptionKeyLabel: "Encryption key",
setEncryptionKeyButtonAudioLabel: "set encryption key",
showEncryptionKey: "Show encryption key",
deleteChatButton: "Delete entire chat"
},
message: {
composerInputPlaceholder: "Type a message...",
sendMessageButtonAudioLabel: "send message",
showMessageInfoButtonAudioLabel: "show message info",
messageInfoHeadline: "Message Info",
sentBy: "Sent by",
timeSent: "Time sent",
channel: "Channel",
messageContent: "Message content",
copyMessageButton: "Copy message",
resendMessageButton: "Resend message",
decryptMessageButton: "Decrypt message",
deleteMessageButton: "Delete message"
}
}
};
var allTranslations = {
en: englishTranslations
};
var language = navigator.language.substring(0, 2);
var translations = allTranslations[language] || allTranslations.en;
// src/View/Components/chatMessageInfoModal.tsx
function ChatMessageInfoModal(chatMessageViewModel, isOpen) {
function closeModal() {
isOpen.value = false;
}
return /* @__PURE__ */ createElement("div", { class: "modal", "toggle:open": isOpen }, /* @__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": closeModal,
"aria-label": translations.general.closeButton
},
translations.general.closeButton,
/* @__PURE__ */ createElement("span", { class: "icon" }, "close")
)));
}
// src/View/Components/chatMessage.tsx
function ChatMessage(chatMessageViewModel) {
const isInfoModalOpen = new State(false);
function openModal() {
isInfoModalOpen.value = true;
}
const statusIcon = createProxyState(
[chatMessageViewModel.status],
() => {
switch (chatMessageViewModel.status.value) {
case "outbox" /* Outbox */:
return "hourglass_top";
case "sent" /* Sent */:
return "check";
case "received" /* Received */:
return "done_all";
default:
return "warning";
}
}
);
return /* @__PURE__ */ createElement(
"div",
{
class: "message-bubble",
"toggle:sentbyuser": chatMessageViewModel.sentByUser
},
/* @__PURE__ */ createElement("div", { class: "main tile" }, /* @__PURE__ */ createElement("div", { class: "text-container" }, /* @__PURE__ */ createElement("span", { class: "sender-name ellipsis" }, chatMessageViewModel.sender), /* @__PURE__ */ createElement(
"span",
{
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(
"button",
{
"on:click": openModal,
"aria-label": translations.chatPage.message.showMessageInfoButtonAudioLabel
},
/* @__PURE__ */ createElement("span", { class: "icon" }, "info")
))),
ChatMessageInfoModal(chatMessageViewModel, isInfoModalOpen)
);
}
var ChatMessageViewModelToView = (chatMessageViewModel) => {
return ChatMessage(chatMessageViewModel);
};
// src/View/ChatPages/messagePage.tsx
function MessagePage(chatViewModel) {
const messageContainer = /* @__PURE__ */ createElement(
"div",
{
id: "message-container",
"children:append": [
chatViewModel.chatMessageViewModels,
ChatMessageViewModelToView
]
}
);
function scrollDown() {
messageContainer.scrollTop = messageContainer.scrollHeight;
}
function scrollDownIfApplicable() {
const scrollFromBottom = messageContainer.scrollHeight - (messageContainer.scrollTop + messageContainer.offsetHeight);
if (scrollFromBottom > 400) return;
scrollDown();
}
chatViewModel.chatMessageViewModels.subscribeSilent(scrollDownIfApplicable);
setTimeout(() => scrollDown(), 100);
return /* @__PURE__ */ createElement("div", { id: "message-page" }, /* @__PURE__ */ createElement("div", { class: "toolbar" }, /* @__PURE__ */ createElement("span", { "subscribe:innerText": chatViewModel.primaryChannel })), /* @__PURE__ */ createElement("div", { class: "content" }, messageContainer, /* @__PURE__ */ createElement("div", { id: "composer" }, /* @__PURE__ */ createElement("div", { class: "content-width-constraint" }, /* @__PURE__ */ createElement("div", { class: "input-width-constraint" }, /* @__PURE__ */ createElement(
"input",
{
"bind:value": chatViewModel.composingMessage,
"on:enter": chatViewModel.sendMessage,
placeholder: translations.chatPage.message.composerInputPlaceholder
}
), /* @__PURE__ */ createElement(
"button",
{
class: "primary",
"aria-label": translations.chatPage.message.sendMessageButtonAudioLabel,
"on:click": chatViewModel.sendMessage,
"toggle:disabled": chatViewModel.cannotSendMessage
},
/* @__PURE__ */ createElement("span", { class: "icon" }, "send")
))))));
}
// src/View/Components/dangerousActionButton.tsx
function DangerousActionButton(label, icon, action) {
const isActionRequested = new State(false);
const cannotConfirm = createProxyState(
[isActionRequested],
() => isActionRequested.value == false
);
function requestAction() {
isActionRequested.value = true;
}
function abort() {
isActionRequested.value = false;
}
return /* @__PURE__ */ createElement("div", { class: "flex-row" }, /* @__PURE__ */ createElement("button", { class: "flex", "on:click": abort, "toggle:hidden": cannotConfirm }, translations.general.abortButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "undo")), /* @__PURE__ */ createElement(
"button",
{
class: "danger flex",
"on:click": requestAction,
"toggle:hidden": isActionRequested
},
label,
/* @__PURE__ */ createElement("span", { class: "icon" }, icon)
), /* @__PURE__ */ createElement(
"button",
{
class: "danger flex",
"on:click": action,
"toggle:hidden": cannotConfirm
},
translations.general.confirmButton,
/* @__PURE__ */ createElement("span", { class: "icon" }, "warning")
));
}
// src/View/Components/deletableListItem.tsx
function DeletableListItem(text, primaryButton, ondelete) {
return /* @__PURE__ */ createElement("div", { class: "tile flex-row justify-apart align-center padding-0" }, /* @__PURE__ */ createElement("span", { class: "padding-h ellipsis" }, text), /* @__PURE__ */ createElement("div", { class: "flex-row justify-end" }, primaryButton, /* @__PURE__ */ createElement(
"button",
{
class: "danger",
"aria-label": translations.general.deleteItemButtonAudioLabel,
"on:click": ondelete
},
/* @__PURE__ */ createElement("span", { class: "icon" }, "delete")
)));
}
// src/View/ChatPages/settingsPage.tsx
function SettingsPage(chatViewModel) {
const secondaryChannelConverter = (secondaryChannel) => {
return DeletableListItem(secondaryChannel, /* @__PURE__ */ createElement("span", null), () => {
chatViewModel.removeSecondaryChannel(secondaryChannel);
});
};
return /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("div", { class: "toolbar" }, /* @__PURE__ */ createElement("span", null, translations.chatPage.settings.settingsHeadline)), /* @__PURE__ */ createElement("div", { class: "content" }, /* @__PURE__ */ createElement("label", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "forum"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.settings.primaryChannelLabel), /* @__PURE__ */ createElement(
"input",
{
"bind:value": chatViewModel.primaryChannelInput,
"on:enter": chatViewModel.setPrimaryChannel
}
))), /* @__PURE__ */ createElement("div", { class: "flex-row justify-end width-input" }, /* @__PURE__ */ createElement(
"button",
{
class: "width-50",
"aria-label": translations.chatPage.settings.setPrimaryChannelButtonAudioLabel,
"on:click": chatViewModel.setPrimaryChannel,
"toggle:disabled": chatViewModel.cannotSetPrimaryChannel
},
translations.general.setButton,
/* @__PURE__ */ createElement("span", { class: "icon" }, "check")
)), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "flex-row width-input margin-bottom" }, /* @__PURE__ */ createElement(
"input",
{
"aria-label": translations.chatPage.settings.newSecondaryChannelAudioLabel,
placeholder: translations.chatPage.settings.newSecondaryChannelPlaceholder,
"bind:value": chatViewModel.newSecondaryChannelInput,
"on:enter": chatViewModel.addSecondaryChannel
}
), /* @__PURE__ */ createElement(
"button",
{
class: "primary",
"aria-label": translations.chatPage.settings.addSecondaryChannelButtonAudioLabel,
"on:click": chatViewModel.addSecondaryChannel,
"toggle:disabled": chatViewModel.cannotAddSecondaryChannel
},
/* @__PURE__ */ createElement("span", { class: "icon" }, "add")
)), /* @__PURE__ */ createElement(
"div",
{
class: "flex-column gap width-input",
"children:append": [
chatViewModel.secondaryChannels,
secondaryChannelConverter
]
}
), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("label", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "key"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.chatPage.settings.encryptionKeyLabel), /* @__PURE__ */ createElement(
"input",
{
"bind:value": chatViewModel.encryptionKeyInput,
"on:enter": chatViewModel.setEncryptionKey,
"set:type": chatViewModel.encryptionKeyInputType
}
))), /* @__PURE__ */ createElement("div", { class: "flex-row justify-end width-input" }, /* @__PURE__ */ createElement(
"button",
{
class: "width-50",
"aria-label": translations.chatPage.settings.setEncryptionKeyButtonAudioLabel,
"on:click": chatViewModel.setEncryptionKey,
"toggle:disabled": chatViewModel.cannotSetEncryptionKey
},
translations.general.setButton,
/* @__PURE__ */ createElement("span", { class: "icon" }, "check")
)), /* @__PURE__ */ createElement("label", { class: "inline" }, /* @__PURE__ */ createElement(
"input",
{
type: "checkbox",
"bind:checked": chatViewModel.shouldShowEncryptionKey
}
), translations.chatPage.settings.showEncryptionKey), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "flex-row gap width-input" }, ...Object.values(Color).map((color) => {
const isSelected = createProxyState(
[chatViewModel.color],
() => chatViewModel.color.value == color
);
function setColor() {
chatViewModel.setColor(color);
}
return /* @__PURE__ */ createElement(
"button",
{
color,
class: "fill-color width-100 flex",
style: "height: 2rem",
"toggle:selected": isSelected,
"on:click": setColor
}
);
})), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "width-input" }, DangerousActionButton(
translations.chatPage.settings.deleteChatButton,
"chat_error",
chatViewModel.remove
))));
}
// src/View/chatPage.tsx
function ChatPage(chatViewModel) {
const mainContent = new State(/* @__PURE__ */ createElement("div", null));
chatViewModel.selectedPage.subscribe((selectedPage) => {
switch (selectedPage) {
case "settings" /* Settings */:
return mainContent.value = SettingsPage(chatViewModel);
default:
return mainContent.value = MessagePage(chatViewModel);
}
});
return /* @__PURE__ */ createElement("article", { id: "chat-page", "set:color": chatViewModel.color, class: "subtle-background" }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("div", { id: "ribbon" }, /* @__PURE__ */ createElement(
"button",
{
class: "ghost",
"aria-label": translations.chatPage.closeChatAudioLabe,
"on:click": chatViewModel.close
},
/* @__PURE__ */ createElement("span", { class: "icon" }, "close")
), /* @__PURE__ */ createElement("span", null, ChatViewToggleButton(
translations.chatPage.pages.calendar,
"calendar_month",
"calendar" /* Calendar */,
chatViewModel
), ChatViewToggleButton(
translations.chatPage.pages.progress,
"window",
"progress" /* Progress */,
chatViewModel
), ChatViewToggleButton(
translations.chatPage.pages.kanban,
"view_kanban",
"kanban" /* Kanban */,
chatViewModel
), ChatViewToggleButton(
translations.chatPage.pages.allObjects,
"deployed_code",
"all" /* AllObjects */,
chatViewModel
), ChatViewToggleButton(
translations.chatPage.pages.messages,
"forum",
"messages" /* Messages */,
chatViewModel
), ChatViewToggleButton(
translations.chatPage.pages.settings,
"settings",
"settings" /* Settings */,
chatViewModel
))), /* @__PURE__ */ createElement("div", { id: "main", "children:set": mainContent })));
}
// src/View/chatPageWrapper.tsx
function ChatPageWrapper(chatListViewModel2) {
const chatPageContent = createProxyState(
[chatListViewModel2.selectedChat],
() => {
if (chatListViewModel2.selectedChat.value == void 0) {
return /* @__PURE__ */ createElement("div", null);
} else {
return ChatPage(chatListViewModel2.selectedChat.value);
}
}
);
return /* @__PURE__ */ createElement(
"div",
{
id: "chat-page-wrapper",
"children:set": chatPageContent
}
);
}
// src/View/Modals/connectionModal.tsx
function ConnectionModal(connectionViewModel2) {
const previousAddressConverter = (address) => {
function connnect() {
connectionViewModel2.connectToAddress(address);
}
const cannotConnect = createProxyState(
[connectionViewModel2.isConnected],
() => connectionViewModel2.isConnected.value == true && connectionViewModel2.connectionModel.address == address
);
return DeletableListItem(
address,
/* @__PURE__ */ createElement(
"button",
{
class: "primary",
"on:click": connnect,
"toggle:disabled": cannotConnect,
"aria-label": translations.connectionModal.connectButtonAudioLabel
},
/* @__PURE__ */ createElement("span", { class: "icon" }, "link")
),
() => {
connectionViewModel2.removePreviousAddress(address);
}
);
};
return /* @__PURE__ */ createElement(
"div",
{
class: "modal",
"toggle:open": connectionViewModel2.isShowingConnectionModal
},
/* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("main", null, /* @__PURE__ */ createElement("h2", null, translations.connectionModal.connectionModalHeadline), /* @__PURE__ */ createElement(
"div",
{
class: "flex-column gap",
"children:append": [
connectionViewModel2.previousAddresses,
previousAddressConverter
]
}
)), /* @__PURE__ */ createElement("button", { "on:click": connectionViewModel2.hideConnectionModal }, translations.general.closeButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "close")))
);
}
// node_modules/udn-frontend/index.ts
var UDNFrontend = class {
ws;
// HANDLERS
connectionHandler = () => {
};
disconnectionHandler = () => {
};
messageHandler = (data) => {
};
mailboxHandler = (mailboxId) => {
};
mailboxConnectionHandler = (mailboxId) => {
};
mailboxDeleteHandler = (mailboxId) => {
};
// INIT
set onconnect(handler) {
this.connectionHandler = handler;
}
set ondisconnect(handler) {
this.disconnectionHandler = handler;
}
set onmessage(handler) {
this.messageHandler = handler;
}
set onmailboxcreate(handler) {
this.mailboxHandler = handler;
}
set onmailboxconnect(handler) {
this.mailboxConnectionHandler = handler;
}
set onmailboxdelete(handler) {
this.mailboxDeleteHandler = handler;
}
// UTILITY METHODS
send(messageObject) {
if (this.ws == void 0) return false;
if (this.ws.readyState != 1) return false;
const messageString = JSON.stringify(messageObject);
this.ws.send(messageString);
return true;
}
// PUBLIC METHODS
// connection
connect(address) {
try {
this.disconnect();
this.ws = new WebSocket(address);
this.ws.addEventListener("open", this.connectionHandler);
this.ws.addEventListener("close", this.disconnectionHandler);
this.ws.addEventListener("message", (message) => {
const dataString = message.data.toString();
const data = JSON.parse(dataString);
if (data.assignedMailboxId) {
return this.mailboxHandler(data.assignedMailboxId);
} else if (data.connectedMailboxId) {
return this.mailboxConnectionHandler(data.connectedMailboxId);
} else if (data.deletedMailbox) {
return this.mailboxDeleteHandler(data.deletedMailbox);
} else {
this.messageHandler(data);
}
});
} catch (error) {
console.error(error);
}
}
disconnect() {
this.ws?.close();
}
// message
sendMessage(channel, body) {
const messageObject = {
messageChannel: channel,
messageBody: body
};
return this.send(messageObject);
}
// subscription
subscribe(channel) {
const messageObject = {
subscribeChannel: channel
};
return this.send(messageObject);
}
unsubscribe(channel) {
const messageObject = {
unsubscribeChannel: channel
};
return this.send(messageObject);
}
// mailbox
requestMailbox() {
const messageObject = {
requestingMailboxSetup: true
};
return this.send(messageObject);
}
connectMailbox(mailboxId) {
const messageObject = {
requestedMailbox: mailboxId
};
return this.send(messageObject);
}
deleteMailbox(mailboxId) {
const messageObject = {
deletingMailbox: mailboxId
};
return this.send(messageObject);
}
};
// src/Model/Global/connectionModel.ts
var ConnectionModel = class {
udn;
storageModel;
// data
get isConnected() {
return this.udn.ws != void 0 && this.udn.ws.readyState == 1;
}
get address() {
return this.udn.ws?.url;
}
connectionChangeHandler = () => {
};
messageHandler = () => {
};
messageSentHandler = () => {
};
channelsToSubscribe = /* @__PURE__ */ new Set();
// handlers
handleMessage = (data) => {
this.messageHandler(data);
};
handleConnectionChange = () => {
console.log("connection status:", this.isConnected, this.address);
this.connectionChangeHandler();
if (this.isConnected == false) return;
if (!this.address) return;
this.connectMailbox();
this.storeAddress(this.address);
this.sendSubscriptionRequest();
this.sendMessagesInOutbox();
};
// connection
connect = (address) => {
this.udn.connect(address);
};
disconnect = () => {
this.udn.disconnect();
const reconnectAddressPath = StorageModel.getPath(
"connectionModel",
filePaths.connectionModel.reconnectAddress
);
this.storageModel.remove(reconnectAddressPath);
};
// mailbox
getMailboxPath = (address) => {
const mailboxDirPath = StorageModel.getPath(
"connectionModel",
filePaths.connectionModel.mailboxes
);
const mailboxFilePath = [...mailboxDirPath, address];
return mailboxFilePath;
};
requestNewMailbox = () => {
console.trace("requesting mailbox");
this.udn.requestMailbox();
};
connectMailbox = () => {
if (this.address == void 0) 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) => {
if (this.address == void 0) return;
this.storageModel.write(this.getMailboxPath(this.address), mailboxId);
};
// subscription
addChannel = (channel) => {
this.channelsToSubscribe.add(channel);
this.sendSubscriptionRequest();
};
sendSubscriptionRequest = () => {
if (this.isConnected == false) return;
for (const channel of this.channelsToSubscribe) {
this.udn.subscribe(channel);
}
this.connectMailbox();
};
// outbox
getOutboxPath = () => {
return StorageModel.getPath(
"connectionModel",
filePaths.connectionModel.outbox
);
};
getOutboxMessags = () => {
const outboxPath = this.getOutboxPath();
const messageIds = this.storageModel.list(outboxPath);
let chatMessages = [];
for (const messageId of messageIds) {
const chatMessage = this.storageModel.readStringifiable(
[...outboxPath, messageId],
ChatMessageReference
);
if (chatMessage == null) continue;
chatMessages.push(chatMessage);
}
return chatMessages;
};
addToOutbox = (chatMessage) => {
const messagePath = [...this.getOutboxPath(), chatMessage.id];
this.storageModel.writeStringifiable(messagePath, chatMessage);
};
removeFromOutbox = (chatMessage) => {
const messagePath = [...this.getOutboxPath(), chatMessage.id];
this.storageModel.remove(messagePath);
};
sendMessagesInOutbox = () => {
const messages = this.getOutboxMessags();
for (const message of messages) {
const isSent = this.tryToSendMessage(message);
if (isSent == false) return;
this.removeFromOutbox(message);
}
};
// messaging
sendMessageOrStore = (chatMessage) => {
const isSent = this.tryToSendMessage(chatMessage);
if (isSent == true) return;
this.addToOutbox(chatMessage);
};
tryToSendMessage = (chatMessage) => {
const stringifiedBody = stringify(chatMessage);
const isSent = this.udn.sendMessage(
chatMessage.channel,
stringifiedBody
);
if (isSent) this.messageSentHandler(chatMessage);
return isSent;
};
// storage
getPreviousAddressPath = () => {
return StorageModel.getPath(
"connectionModel",
filePaths.connectionModel.previousAddresses
);
};
getAddressPath = (address) => {
const dirPath = this.getPreviousAddressPath();
return [...dirPath, address];
};
getReconnectAddressPath = () => {
return StorageModel.getPath(
"connectionModel",
filePaths.connectionModel.reconnectAddress
);
};
storeAddress = (address) => {
const addressPath = this.getAddressPath(address);
this.storageModel.write(addressPath, "");
const reconnectAddressPath = this.getReconnectAddressPath();
this.storageModel.write(reconnectAddressPath, address);
};
removeAddress = (address) => {
const addressPath = this.getAddressPath(address);
this.storageModel.remove(addressPath);
};
get addresses() {
const dirPath = this.getPreviousAddressPath();
return this.storageModel.list(dirPath);
}
// other
setConnectionChangeHandler = (handler) => {
this.connectionChangeHandler = handler;
};
setMessageHandler = (handler) => {
this.messageHandler = handler;
};
setMessageSentHandler = (handler) => {
this.messageSentHandler = handler;
};
// init
constructor(storageModel2) {
this.udn = new UDNFrontend();
this.storageModel = storageModel2;
this.udn.onmessage = (data) => {
this.handleMessage(data);
};
this.udn.onconnect = () => {
this.handleConnectionChange();
};
this.udn.ondisconnect = () => {
this.handleConnectionChange();
};
this.udn.onmailboxcreate = (mailboxId) => {
console.log("created mailbox", mailboxId);
this.storeMailbox(mailboxId);
this.connectMailbox();
};
this.udn.onmailboxdelete = (mailboxId) => {
console.log(`mailbox ${mailboxId} deleted`);
this.requestNewMailbox();
};
this.udn.onmailboxconnect = (mailboxId) => {
console.log(`using mailbox ${mailboxId}`);
};
const reconnectAddressPath = this.getReconnectAddressPath();
const reconnectAddress = storageModel2.read(reconnectAddressPath);
if (reconnectAddress != null) {
this.connect(reconnectAddress);
}
}
};
// src/ViewModel/connectionViewModel.ts
var ConnectionViewModel = class {
connectionModel;
// state
serverAddressInput = new State("");
isConnected = new State(false);
isShowingConnectionModal = new State(false);
previousAddresses = new ListState();
// guards
cannotConnect = createProxyState(
[this.serverAddressInput, this.isConnected],
() => this.isConnected.value == true && this.serverAddressInput.value == this.connectionModel.address || this.serverAddressInput.value == ""
);
cannotDisonnect = createProxyState(
[this.isConnected],
() => this.isConnected.value == false
);
hasNoPreviousConnections = createProxyState(
[this.previousAddresses],
() => this.previousAddresses.value.size == 0
);
// handlers
connectionChangeHandler = () => {
this.isConnected.value = this.connectionModel.isConnected;
if (this.connectionModel.isConnected == false) return;
if (this.connectionModel.address == void 0) return;
this.serverAddressInput.value = this.connectionModel.address;
if (!this.previousAddresses.value.has(this.connectionModel.address)) {
this.previousAddresses.add(this.connectionModel.address);
}
};
// methods
connect = () => {
this.connectToAddress(this.serverAddressInput.value);
};
connectToAddress = (address) => {
this.connectionModel.connect(address);
};
disconnect = () => {
this.connectionModel.disconnect();
};
removePreviousAddress = (address) => {
this.connectionModel.removeAddress(address);
this.updatePreviousAddresses();
};
// view methods
showConnectionModal = () => {
this.isShowingConnectionModal.value = true;
};
hideConnectionModal = () => {
this.isShowingConnectionModal.value = false;
};
updatePreviousAddresses = () => {
this.previousAddresses.clear();
this.previousAddresses.add(...this.connectionModel.addresses);
};
// init
constructor(connectionModel2) {
this.connectionModel = connectionModel2;
this.updatePreviousAddresses();
connectionModel2.setConnectionChangeHandler(this.connectionChangeHandler);
}
};
// src/View/Components/option.tsx
function Option(text, value, selectedOnCreate) {
return /* @__PURE__ */ createElement("option", { value, "toggle:selected": selectedOnCreate }, text);
}
var StringToOption = (string) => {
return Option(string, string, false);
};
// src/View/Components/chatEntry.tsx
function ChatEntry(chatViewModel) {
const view = /* @__PURE__ */ createElement("button", { "set:color": chatViewModel.color, class: "chat-entry tile", "on:click": chatViewModel.open }, /* @__PURE__ */ createElement(
"span",
{
class: "shadow",
"subscribe:innerText": chatViewModel.primaryChannel
}
), /* @__PURE__ */ createElement("h2", { "subscribe:innerText": chatViewModel.primaryChannel }));
chatViewModel.index.subscribe((newIndex) => {
view.style.order = newIndex;
});
return view;
}
var ChatViewModelToChatEntry = (chatViewModel) => {
return ChatEntry(chatViewModel);
};
// src/View/homePage.tsx
function HomePage(storageViewModel2, settingsViewModel2, connectionViewModel2, chatListViewModel2) {
const overviewSection = /* @__PURE__ */ createElement("div", { id: "overview-section" }, /* @__PURE__ */ createElement("h2", null, translations.homePage.overviewHeadline), /* @__PURE__ */ createElement("label", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "cell_tower"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.homePage.serverAddress), /* @__PURE__ */ createElement(
"input",
{
list: "previous-connection-list",
placeholder: translations.homePage.serverAddressPlaceholder,
"bind:value": connectionViewModel2.serverAddressInput,
"on:enter": connectionViewModel2.connect
}
), /* @__PURE__ */ createElement(
"datalist",
{
hidden: true,
id: "previous-connection-list",
"children:append": [
connectionViewModel2.previousAddresses,
StringToOption
]
}
))), /* @__PURE__ */ createElement("div", { class: "flex-row" }, /* @__PURE__ */ createElement(
"button",
{
class: "danger flex justify-center",
"aria-label": translations.homePage.disconnectAudioLabel,
"on:click": connectionViewModel2.disconnect,
"toggle:disabled": connectionViewModel2.cannotDisonnect
},
/* @__PURE__ */ createElement("span", { class: "icon" }, "link_off")
), /* @__PURE__ */ createElement(
"button",
{
class: "flex justify-center",
"aria-label": translations.homePage.manageConnectionsAudioLabel,
"on:click": connectionViewModel2.showConnectionModal,
"toggle:disabled": connectionViewModel2.hasNoPreviousConnections
},
/* @__PURE__ */ createElement("span", { class: "icon" }, "history")
), /* @__PURE__ */ createElement(
"button",
{
class: "primary flex justify-center",
"aria-label": translations.homePage.connectAudioLabel,
"on:click": connectionViewModel2.connect,
"toggle:disabled": connectionViewModel2.cannotConnect
},
/* @__PURE__ */ createElement("span", { class: "icon" }, "link")
)), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("label", { class: "tile flex-no" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "account_circle"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translations.homePage.yourNameLabel), /* @__PURE__ */ createElement(
"input",
{
placeholder: translations.homePage.yourNamePlaceholder,
"bind:value": settingsViewModel2.usernameInput,
"on:enter": settingsViewModel2.setName
}
))), /* @__PURE__ */ createElement("div", { class: "flex-row justify-end" }, /* @__PURE__ */ createElement(
"button",
{
class: "width-50",
"on:click": settingsViewModel2.setName,
"toggle:disabled": settingsViewModel2.cannotSetName,
"aria-label": translations.homePage.setNameButtonAudioLabel
},
translations.general.setButton,
/* @__PURE__ */ createElement("span", { class: "icon" }, "check")
)), /* @__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.homePage.firstDayOfWeekLabel), /* @__PURE__ */ createElement("select", { "bind:value": settingsViewModel2.firstDayOfWeekInput }, ...translations.regional.weekdays.full.map(
(weekdayName, i) => Option(
weekdayName,
i.toString(),
i == settingsViewModel2.firstDayOfWeekInput.value
)
)), /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_drop_down"))), /* @__PURE__ */ createElement("hr", null), /* @__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",
{
placeholder: translations.homePage.addChatPlaceholder,
"aria-label": translations.homePage.addChatAudioLabel,
"bind:value": chatListViewModel2.newChatPrimaryChannel,
"on:enter": chatListViewModel2.createChat
}
), /* @__PURE__ */ createElement(
"button",
{
class: "primary",
"aria-label": translations.homePage.addChatButton,
"on:click": chatListViewModel2.createChat,
"toggle:disabled": chatListViewModel2.cannotCreateChat
},
/* @__PURE__ */ createElement("span", { class: "icon" }, "add")
)), /* @__PURE__ */ createElement(
"div",
{
id: "chat-grid",
"children:append": [
chatListViewModel2.chatViewModels,
ChatViewModelToChatEntry
]
}
));
function scrollToChat() {
chatSection.scrollIntoView();
}
return /* @__PURE__ */ createElement("article", { id: "home-page" }, /* @__PURE__ */ createElement("div", null, overviewSection, chatSection));
}
// src/Model/Global/settingsModel.ts
var SettingsModel = class {
storageModel;
// data
username;
firstDayOfWeek;
// storage
setName(newValue) {
this.username = newValue;
const path = StorageModel.getPath(
"settingsModel",
filePaths.settingsModel.username
);
this.storageModel.write(path, newValue);
}
setFirstDayOfWeek(newValue) {
this.firstDayOfWeek = newValue;
const path = StorageModel.getPath(
"settingsModel",
filePaths.settingsModel.firstDayOfWeek
);
this.storageModel.write(path, newValue);
}
// load
loadUsernam() {
const path = StorageModel.getPath(
"settingsModel",
filePaths.settingsModel.username
);
const content = this.storageModel.read(path);
this.username = content ?? "";
}
loadFirstDayofWeek() {
const path = StorageModel.getPath(
"settingsModel",
filePaths.settingsModel.firstDayOfWeek
);
const content = this.storageModel.read(path);
this.firstDayOfWeek = content ?? "0";
}
// init
constructor(storageModel2) {
this.storageModel = storageModel2;
this.loadUsernam();
this.loadFirstDayofWeek();
}
};
// src/ViewModel/settingsViewModel.ts
var SettingsViewModel = class {
settingsModel;
// state
username = new State("");
usernameInput = new State("");
firstDayOfWeekInput = new State("0");
// guards
cannotSetName = createProxyState(
[this.usernameInput],
() => this.usernameInput.value == "" || this.usernameInput.value == this.settingsModel.username
);
// set
setName = () => {
this.settingsModel.setName(this.usernameInput.value);
this.username.value = this.settingsModel.username;
this.usernameInput.callSubscriptions();
};
setFirstDayofWeek = () => {
this.settingsModel.setFirstDayOfWeek(this.firstDayOfWeekInput.value);
};
// init
constructor(settingsModel2) {
this.settingsModel = settingsModel2;
this.username.value = settingsModel2.username;
this.usernameInput.value = settingsModel2.username;
this.firstDayOfWeekInput.value = settingsModel2.firstDayOfWeek;
this.firstDayOfWeekInput.subscribe(this.setFirstDayofWeek);
}
};
// src/View/Components/directoryItemList.tsx
function DirectoryItemList(storageViewModel2, pathString = PATH_COMPONENT_SEPARATOR) {
const StringToDirectoryItemList = (pathString2) => DirectoryItemList(storageViewModel2, pathString2);
const path = StorageModel.stringToPathComponents(pathString);
const fileName = StorageModel.getFileName(path);
const items = new ListState();
const style = `text-indent: ${path.length * 2}rem`;
function loadItems() {
items.clear();
const directoryItems = storageViewModel2.storageModel.list(path);
for (const directoryItem of directoryItems) {
const itemPath = [...path, directoryItem];
const pathString2 = StorageModel.pathComponentsToString(...itemPath);
items.add(pathString2);
}
}
function select() {
storageViewModel2.selectedPath.value = pathString;
}
storageViewModel2.lastDeletedItemPath.subscribe((lastDeletedItemPath) => {
if (!items.value.has(lastDeletedItemPath)) return;
select();
setTimeout(() => loadItems(), 50);
});
const isSelected = createProxyState(
[storageViewModel2.selectedPath],
() => storageViewModel2.selectedPath.value == pathString
);
isSelected.subscribe(() => {
if (isSelected.value == false) return;
loadItems();
});
return /* @__PURE__ */ createElement("div", { class: "flex-column" }, /* @__PURE__ */ createElement(
"button",
{
class: "width-100 flex-1 clip",
"toggle:selected": isSelected,
"on:click": select
},
/* @__PURE__ */ createElement("span", { class: "ellipsis width-100 flex-1", style }, fileName)
), /* @__PURE__ */ createElement(
"div",
{
class: "flex-column",
"children:append": [items, StringToDirectoryItemList]
}
));
}
// src/View/Components/fileBrowser.tsx
function FileBrowser(storageViewModel2) {
const detailView = createProxyState(
[storageViewModel2.selectedPath],
() => {
if (storageViewModel2.selectedPath.value == PATH_COMPONENT_SEPARATOR)
return /* @__PURE__ */ createElement("span", { class: "secondary" }, translations.storage.noItemSelected);
return /* @__PURE__ */ createElement("div", { class: "flex-column gap" }, /* @__PURE__ */ createElement("div", { class: "tile flex-no" }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, translations.storage.path), /* @__PURE__ */ createElement(
"span",
{
class: "break-all",
"subscribe:innerText": storageViewModel2.selectedPath
}
))), /* @__PURE__ */ createElement("div", { class: "tile flex-no" }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, translations.storage.content), /* @__PURE__ */ createElement(
"code",
{
"subscribe:innerText": storageViewModel2.selectedFileContent
}
))), DangerousActionButton(
translations.storage.deleteItem,
"delete_forever",
storageViewModel2.deleteSelectedItem
));
}
);
const view = /* @__PURE__ */ createElement("div", { class: "file-browser" }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("div", { class: "scroll-area" }, DirectoryItemList(storageViewModel2)), /* @__PURE__ */ createElement("div", { class: "detail-button-wrapper" }, /* @__PURE__ */ createElement("button", { class: "ghost", "on:click": scrollToDetails }, /* @__PURE__ */ createElement(
"span",
{
class: "ellipsis",
"subscribe:innerText": storageViewModel2.selectedFileName
}
), /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_forward")))), /* @__PURE__ */ createElement("div", { class: "scroll-area", "children:set": detailView }));
function scrollToDetails() {
view.scrollLeft = view.scrollWidth;
}
return view;
}
// src/View/Modals/storageModal.tsx
function StorageModal(storageViewModel2) {
return /* @__PURE__ */ createElement("div", { class: "modal", "toggle:open": storageViewModel2.isShowingStorageModal }, /* @__PURE__ */ createElement("div", { style: "max-width: 64rem" }, /* @__PURE__ */ createElement("main", { class: "padding-0" }, FileBrowser(storageViewModel2)), /* @__PURE__ */ createElement("button", { "on:click": storageViewModel2.hideStorageModal }, translations.general.closeButton, /* @__PURE__ */ createElement("span", { class: "icon" }, "close"))));
}
// src/ViewModel/storageViewModel.ts
var StorageViewModel = class {
storageModel;
// state
isShowingStorageModal = new State(false);
selectedPath = new State(PATH_COMPONENT_SEPARATOR);
didMakeChanges = new State(false);
selectedFileName;
selectedFileContent;
lastDeletedItemPath = new State("");
// methods
getSelectedItemContent = () => {
const path = StorageModel.stringToPathComponents(this.selectedPath.value);
const content = this.storageModel.read(path);
return (content ?? translations.storage.notAFile) || translations.storage.contentEmpty;
};
deleteSelectedItem = () => {
const path = StorageModel.stringToPathComponents(this.selectedPath.value);
this.lastDeletedItemPath.value = this.selectedPath.value;
this.storageModel.removeRecursively(path);
this.didMakeChanges.value = true;
};
// view methods
showStorageModal = () => {
this.isShowingStorageModal.value = true;
};
hideStorageModal = () => {
if (this.didMakeChanges.value == true) {
window.location.reload();
return;
}
this.isShowingStorageModal.value = false;
};
// init
constructor(storageModel2) {
this.storageModel = storageModel2;
this.selectedFileName = createProxyState(
[this.selectedPath],
() => StorageModel.getFileNameFromString(this.selectedPath.value)
);
this.selectedFileContent = createProxyState(
[this.selectedPath],
() => this.getSelectedItemContent()
);
}
};
// src/index.tsx
var storageModel = new StorageModel();
var settingsModel = new SettingsModel(storageModel);
var connectionModel = new ConnectionModel(storageModel);
var chatListModel = new ChatListModel(
storageModel,
settingsModel,
connectionModel
);
var storageViewModel = new StorageViewModel(storageModel);
var settingsViewModel = new SettingsViewModel(settingsModel);
var connectionViewModel = new ConnectionViewModel(connectionModel);
var chatListViewModel = new ChatListViewModel(
chatListModel,
storageModel,
settingsViewModel
);
chatListViewModel.selectedChat.subscribe(() => {
document.body.toggleAttribute(
"showing-chat",
chatListViewModel.selectedChat.value != void 0
);
});
document.body.append(
/* @__PURE__ */ createElement("div", { id: "background-wrapper" }, /* @__PURE__ */ createElement("div", { id: "sky" }), /* @__PURE__ */ createElement("div", { id: "grass-1" }), /* @__PURE__ */ createElement("div", { id: "grass-2" }))
);
document.querySelector("main").append(
HomePage(
storageViewModel,
settingsViewModel,
connectionViewModel,
chatListViewModel
),
ChatPageWrapper(chatListViewModel),
ConnectionModal(connectionViewModel),
StorageModal(storageViewModel)
);
})();