Merge pull request #2 from marlon-erler/Plugins

Add Objects
This commit is contained in:
Marlon Erler
2024-07-11 19:33:31 +02:00
committed by GitHub
22 changed files with 1452 additions and 184 deletions
BIN
View File
Binary file not shown.
+63
View File
@@ -2,3 +2,66 @@
[unread] { [unread] {
border-left: 10px solid var(--text-warn); border-left: 10px solid var(--text-warn);
} }
:root {
--thread-view-height-with-tools: 8rem;
--thread-view-width-with-tools: 20rem;
--footer-top-line: calc(var(--control-height) + var(--safe-area-bottom));
}
#message-tab {
flex-direction: column;
}
.message-tab-content-wrapper {
display: flex;
flex-direction: column;
}
.chat-object-view {
display: none;
}
[showingchattools] :is(.thread-view, .chat-object-view) {
overflow: scroll;
position: absolute;
padding: 0px;
}
[showingchattools] header {
border-bottom: 1px solid var(--border);
}
[showingchattools] .chat-object-view {
display: flex;
top: var(--header-height);
bottom: calc(var(--thread-view-height-with-tools) + var(--footer-top-line));
border-bottom: 1px solid var(--border);
}
[showingchattools] .thread-view {
display: flex;
height: var(--thread-view-height-with-tools);
top: unset;
bottom: var(--footer-top-line);
flex: 1;
}
@media (min-width: 640px) and (max-width: 1400px) {
main.split:has([showingchattools]) {
display: flex;
}
main.split:has([showingchattools]) #settings-tab {
display: none;
}
}
@media (min-width: 640px) {
[showingchattools] :is(.thread-view, .chat-object-view) {
top: var(--header-height);
bottom: var(--footer-top-line);
}
[showingchattools] .chat-object-view {
left: 0px;
right: var(--thread-view-width-with-tools);
border-bottom: none;
border-right: 1px solid var(--border);
}
[showingchattools] .thread-view {
display: flex;
left: unset;
right: 0px;
height: unset;
width: var(--thread-view-width-with-tools);
}
}
+528 -77
View File
@@ -76,6 +76,9 @@
this._bindings.add(fn); this._bindings.add(fn);
fn(this._value); fn(this._value);
} }
subscribeSilent(fn) {
this._bindings.add(fn);
}
// stringify // stringify
toString() { toString() {
return JSON.stringify(this._value); return JSON.stringify(this._value);
@@ -118,7 +121,48 @@
} }
// stringification // stringification
toString() { toString() {
const array = [...this.value]; 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)(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) {
this.removalHandlers.set(item, handler);
}
// stringification
toString() {
const array = [...this.value.entries()];
const json = JSON.stringify(array); const json = JSON.stringify(array);
return json; return json;
} }
@@ -130,6 +174,9 @@
); );
return proxyState; return proxyState;
} }
function bulkSubscribe(statesToSubscibe, fn) {
statesToSubscibe.forEach((state) => state.subscribeSilent(fn));
}
function persistState(localStorageKey, state) { function persistState(localStorageKey, state) {
state.subscribe(() => { state.subscribe(() => {
const stringifiedValue = state.toString(); const stringifiedValue = state.toString();
@@ -143,9 +190,8 @@
persistState(localStorageKey, state); persistState(localStorageKey, state);
return state; return state;
} }
function restoreListState(localStorageKey) { function restoreListState(localStorageKey, initialItems = []) {
const storedString = localStorage.getItem(localStorageKey) ?? ""; const storedString = localStorage.getItem(localStorageKey) ?? "";
let initialItems = [];
try { try {
const array = JSON.parse(storedString); const array = JSON.parse(storedString);
if (!Array.isArray(array)) throw ""; if (!Array.isArray(array)) throw "";
@@ -156,6 +202,18 @@
persistState(localStorageKey, state); persistState(localStorageKey, state);
return state; return state;
} }
function restoreMapState(localStorageKey, initialItems = []) {
const storedString = localStorage.getItem(localStorageKey) ?? "";
try {
const array = JSON.parse(storedString);
if (!Array.isArray(array)) throw "";
initialItems = array;
} catch {
}
const state = new MapState(initialItems);
persistState(localStorageKey, state);
return state;
}
function createElement(tagName, attributes = {}, ...children) { function createElement(tagName, attributes = {}, ...children) {
const element = document.createElement(tagName); const element = document.createElement(tagName);
if (attributes != null) if (attributes != null)
@@ -243,7 +301,7 @@
} }
}); });
} catch { } catch {
throw `error: cannot process subscribe:children directive because ListItemConverter is not defined. Usage: "subscribe:children={[list, converter]}"; you can find a more detailed example in the documentation`; throw `error: cannot process subscribe:children directive because StateItemConverter is not defined. Usage: "subscribe:children={[list, converter]}"; you can find a more detailed example in the documentation`;
} }
} }
} }
@@ -346,9 +404,15 @@
messages(id) { messages(id) {
return id + "messages"; return id + "messages";
}, },
objects(id) {
return id + "items";
},
outbox(id) { outbox(id) {
return id + "outbox"; return id + "outbox";
}, },
itemOutbox(id) {
return id + "item-outbox";
},
composingMessage(id) { composingMessage(id) {
return id + "composing-message"; return id + "composing-message";
} }
@@ -363,7 +427,9 @@
secondaryChannels; secondaryChannels;
encryptionKey; encryptionKey;
messages; messages;
objects;
outbox; outbox;
objectOutbox;
isOutBoxEmpty; isOutBoxEmpty;
composingMessage; composingMessage;
primaryChannelInput; primaryChannelInput;
@@ -374,6 +440,7 @@
cannotSetChannel; cannotSetChannel;
cannotUndoChannel; cannotUndoChannel;
cannotClearMessages; cannotClearMessages;
cannotClearObjects;
// init // init
constructor(id = UUID()) { constructor(id = UUID()) {
this.id = id; this.id = id;
@@ -390,7 +457,9 @@
); );
this.encryptionKey = restoreState(storageKeys.encyptionKey(id), ""); this.encryptionKey = restoreState(storageKeys.encyptionKey(id), "");
this.messages = restoreListState(storageKeys.messages(id)); this.messages = restoreListState(storageKeys.messages(id));
this.objects = restoreMapState(storageKeys.objects(id));
this.outbox = restoreListState(storageKeys.outbox(id)); this.outbox = restoreListState(storageKeys.outbox(id));
this.objectOutbox = restoreMapState(storageKeys.itemOutbox(id));
this.isOutBoxEmpty = createProxyState( this.isOutBoxEmpty = createProxyState(
[this.outbox], [this.outbox],
() => this.outbox.value.size == 0 () => this.outbox.value.size == 0
@@ -425,6 +494,10 @@
[this.messages], [this.messages],
() => this.messages.value.size == 0 () => this.messages.value.size == 0
); );
this.cannotClearObjects = createProxyState(
[this.objects],
() => this.objects.value.size == 0
);
} }
// general // general
deleteSelf = () => { deleteSelf = () => {
@@ -441,7 +514,17 @@
if (channels.indexOf(this.primaryChannel.value) == -1) return; if (channels.indexOf(this.primaryChannel.value) == -1) return;
if (data.subscribed != void 0) this.handleSubscription(data.subscribed); if (data.subscribed != void 0) this.handleSubscription(data.subscribed);
if (!data.messageBody) return; if (!data.messageBody) return;
const { sender, body, channel, isoDate } = JSON.parse(data.messageBody); const message = JSON.parse(data.messageBody);
const { sender, body, channel, isoDate, messageObjectString } = message;
if (messageObjectString) {
const decryptedMessageObjectString = await decryptString(
messageObjectString,
this.encryptionKey.value
);
const messageObject = JSON.parse(decryptedMessageObjectString);
if (messageObject.id && messageObject.title)
return this.handleMessageObject(messageObject);
}
this.handleMessage({ this.handleMessage({
sender, sender,
body: await decryptString(body, this.encryptionKey.value), body: await decryptString(body, this.encryptionKey.value),
@@ -459,21 +542,23 @@
this.messages.add(chatMessage); this.messages.add(chatMessage);
if (selectedChat.value != this) this.hasUnreadMessages.value = true; if (selectedChat.value != this) this.hasUnreadMessages.value = true;
}; };
handleMessageObject = (messageObject) => {
this.addObject(messageObject);
};
// sending
sendMessagesInOutbox = () => {
this.outbox.value.forEach(async (message) => {
const isSent = await this.sendMessage(message);
if (isSent == true) this.outbox.remove(message);
});
this.objectOutbox.value.forEach(async (messageObject) => {
const chatMessage = await this.createChatMessage("", messageObject);
const isSent = await this.sendMessage(chatMessage);
if (isSent == true) this.objectOutbox.remove(messageObject.id);
});
};
// messages // messages
sendNewMessage = async () => { createChatMessage = async (messageText, messageObject) => {
if (this.cannotSendMessage.value == true) return;
await this.sendMessageText(this.composingMessage.value);
this.composingMessage.value = "";
};
sendMessageText = async (text) => {
const message = await this.createMessage(text);
this.sendExistingMessage(message);
};
resendMessage = (message) => {
if (this.cannotResendMessage.value == true) return;
this.sendMessageText(message.body);
};
createMessage = async (messageText) => {
const secondaryChannelNames = [ const secondaryChannelNames = [
...this.secondaryChannels.value.values() ...this.secondaryChannels.value.values()
]; ];
@@ -482,42 +567,128 @@
...secondaryChannelNames ...secondaryChannelNames
]; ];
const joinedChannelName = allChannelNames.join("/"); const joinedChannelName = allChannelNames.join("/");
return { const chatMessage = {
channel: joinedChannelName, channel: joinedChannelName,
sender: senderName.value, sender: senderName.value,
body: messageText, body: messageText,
isoDate: (/* @__PURE__ */ new Date()).toISOString() isoDate: (/* @__PURE__ */ new Date()).toISOString()
}; };
if (messageObject)
chatMessage.messageObjectString = await encryptString(
JSON.stringify(messageObject),
this.encryptionKey.value
);
return chatMessage;
}; };
sendExistingMessage = async (chatMessage) => { sendMessageFromComposer = async () => {
if (isConnected.value == true && this.isSubscribed.value == true) { if (this.cannotSendMessage.value == true) return;
const encrypted = this.encryptionKey.value == "" ? chatMessage.body : await encryptString(chatMessage.body, this.encryptionKey.value); await this.sendMessageFromText(this.composingMessage.value);
chatMessage.body = encrypted; this.composingMessage.value = "";
const messageString = JSON.stringify(chatMessage);
UDN.sendMessage(chatMessage.channel, messageString);
} else {
this.outbox.add(chatMessage);
}
}; };
sendMessagesInOutbox = () => { sendMessageFromText = async (text) => {
this.outbox.value.forEach((message) => { const chatMessage = await this.createChatMessage(text);
this.sendExistingMessage(message); this.outbox.add(chatMessage);
this.outbox.remove(message); this.sendMessagesInOutbox();
}); };
resendMessage = (chatMessage) => {
if (this.cannotResendMessage.value == true) return;
this.sendMessageFromText(chatMessage.body);
}; };
clearMessages = () => { clearMessages = () => {
this.messages.clear(); this.messages.clear();
}; };
deleteMessage = (message) => { deleteMessage = (chatMessage) => {
this.messages.remove(message); this.messages.remove(chatMessage);
}; };
deleteOutboxMessage = (message) => { deleteOutboxMessage = (chatMessage) => {
this.outbox.remove(message); this.outbox.remove(chatMessage);
}; };
decryptReceivedMessage = async (message) => { decryptReceivedMessage = async (chatMessage) => {
message.body = await decryptString(message.body, this.encryptionKey.value); chatMessage.body = await decryptString(
chatMessage.body,
this.encryptionKey.value
);
this.messages.callSubscriptions(); this.messages.callSubscriptions();
}; };
sendMessage = async (chatMessage) => {
if (isConnected.value == false || this.isSubscribed.value == false)
return false;
const encryptedBody = this.encryptionKey.value == "" ? chatMessage.body : await encryptString(chatMessage.body, this.encryptionKey.value);
chatMessage.body = encryptedBody;
const messageString = JSON.stringify(chatMessage);
UDN.sendMessage(chatMessage.channel, messageString);
return true;
};
// objects
createObjectFromTitle = (title) => {
const firstObjectContent = this.createObjectContent();
const messageObject = {
id: UUID(),
title,
contentVersions: {}
};
this.addObjectContent(messageObject, firstObjectContent);
return messageObject;
};
createObjectContent = () => {
return {
id: UUID(),
isoDateVersionCreated: (/* @__PURE__ */ new Date()).toISOString()
};
};
addObjectContent = (messageObject, content) => {
messageObject.contentVersions[content.id] = content;
};
addObject = (messageObject) => {
const existingObject = this.objects.value.get(messageObject.id);
if (existingObject) {
existingObject.title = messageObject.title;
Object.values(messageObject.contentVersions).forEach((content) => {
this.addObjectContent(existingObject, content);
});
this.objects.set(existingObject.id, existingObject);
} else {
this.objects.set(messageObject.id, messageObject);
}
};
addObjectAndSend = (messageObject) => {
this.addObject(messageObject);
this.sendObject(messageObject);
};
sendObject = (messageObject) => {
this.objectOutbox.set(messageObject.id, messageObject);
this.sendMessagesInOutbox();
};
deleteObject = (messageObject) => {
this.objects.remove(messageObject.id);
this.objectOutbox.remove(messageObject.id);
};
resendObjects = () => {
this.objects.value.forEach((messageObject) => {
this.sendObject(messageObject);
});
};
clearObjects = () => {
this.objects.clear();
};
getSortedContents = (messageObject) => {
const contents = Object.values(messageObject.contentVersions);
contents.sort(
(a, b) => a.isoDateVersionCreated > b.isoDateVersionCreated ? 0 : 1
);
return contents;
};
getMostRecentContentId = (messageObject) => {
const contents = Object.values(messageObject.contentVersions);
return contents[contents.length - 1].id;
};
getMostRecentContent = (messageObject) => {
const id = this.getMostRecentContentId(messageObject);
return this.getObjectContentFromId(messageObject, id);
};
getObjectContentFromId = (messageObject, contentId) => {
return messageObject.contentVersions[contentId];
};
// channel // channel
setChannel = () => { setChannel = () => {
if (this.cannotSetChannel.value == true) return; if (this.cannotSetChannel.value == true) return;
@@ -755,6 +926,7 @@
var chats = new ListState(); var chats = new ListState();
var chatIds = restoreListState("chat-ids"); var chatIds = restoreListState("chat-ids");
var selectedChat = new State(void 0); var selectedChat = new State(void 0);
var isShowingChatTools = new State(false);
var newChatName = new State(""); var newChatName = new State("");
var cannotCreateChat = createProxyState( var cannotCreateChat = createProxyState(
[newChatName], [newChatName],
@@ -766,6 +938,7 @@
newChatName.value = ""; newChatName.value = "";
} }
function closeChatView() { function closeChatView() {
isShowingChatTools.value = false;
selectedChat.value = void 0; selectedChat.value = void 0;
document.getElementById("settings-tab")?.scrollIntoView(); document.getElementById("settings-tab")?.scrollIntoView();
} }
@@ -774,6 +947,9 @@
chat.hasUnreadMessages.value = false; chat.hasUnreadMessages.value = false;
document.getElementById("message-tab")?.scrollIntoView(); document.getElementById("message-tab")?.scrollIntoView();
} }
function toggleChatTools() {
isShowingChatTools.value = !isShowingChatTools.value;
}
chatIds.value.forEach((id) => chats.add(new Chat(id))); chatIds.value.forEach((id) => chats.add(new Chat(id)));
if (serverAddressInput.value != "" && didRequestConnection.value == true) { if (serverAddressInput.value != "" && didRequestConnection.value == true) {
connect(); connect();
@@ -783,9 +959,11 @@
var englishTranslations = { var englishTranslations = {
// general // general
set: "Set", set: "Set",
save: "Save",
back: "Back", back: "Back",
undoChanges: "Undo changes", undoChanges: "Undo changes",
close: "Close", close: "Close",
discard: "Discard",
// overview // overview
overview: "Overview", overview: "Overview",
connection: "Connection", connection: "Connection",
@@ -797,7 +975,7 @@
serverAddress: "Server Address", serverAddress: "Server Address",
serverAddressPlaceholder: "wss://192.168.0.69:3000", serverAddressPlaceholder: "wss://192.168.0.69:3000",
connectToServer: "Connect", connectToServer: "Connect",
disconnect: "Disonnect", disconnect: "Disconnect",
mailbox: "Mailbox", mailbox: "Mailbox",
requestMailbox: "Enable", requestMailbox: "Enable",
deleteMailbox: "Disable", deleteMailbox: "Disable",
@@ -805,7 +983,7 @@
primaryChannel: "Primary channel", primaryChannel: "Primary channel",
primaryChannelPlaceholder: "my-channel", primaryChannelPlaceholder: "my-channel",
addChat: "Add", addChat: "Add",
// messages // chat
showChatOptions: "show chat options", showChatOptions: "show chat options",
configureChatTitle: "Configure Chat", configureChatTitle: "Configure Chat",
secondaryChannel: "Secondary channel", secondaryChannel: "Secondary channel",
@@ -815,34 +993,53 @@
encryptionKey: "Encryption key", encryptionKey: "Encryption key",
encryptionKeyPlaceholder: "n10d2482dg283hg", encryptionKeyPlaceholder: "n10d2482dg283hg",
showKey: "Show key", showKey: "Show key",
removeChat: "Remove chat", clearObjects: "Delete all objects",
clearChatMessages: "Clear chat messages", clearChatMessages: "Clear chat messages",
removeChat: "Remove chat",
messageInOutbox: "Pending", messageInOutbox: "Pending",
noChatSelected: "No chat selected", noChatSelected: "No chat selected",
composerPlaceholder: "Type a message...", composerPlaceholder: "Type a message...",
sendMessage: "Send", sendMessage: "Send",
// messages
resendMessage: "Resend message", resendMessage: "Resend message",
decryptMessage: "Decrypt message", decryptMessage: "Decrypt message",
copyMessage: "Copy message", copyMessage: "Copy message",
deleteMessage: "Delete message" deleteMessage: "Delete message",
// objects
showObjects: "show objects",
createObject: "create object",
untitledObject: "Untitled Object",
viewAll: "All",
viewNotes: "Notes",
noObjects: "No objects",
noNotes: "No notes",
objectTitle: "Object title",
objectTitlePlaceholder: "My object",
objectVersion: "Object version",
noteContent: "Note",
noteContentPlaceholder: "Take a note...",
resendObjects: "Resend all objects",
deleteObject: "Delete object"
}; };
var allTranslations = { var allTranslations = {
en: englishTranslations, en: englishTranslations,
es: { es: {
// general // general
set: "Guardar", set: "Guardar",
save: "Guardar",
back: "Atr\xE1s", back: "Atr\xE1s",
undoChanges: "Deshacer", undoChanges: "Deshacer",
close: "Cerrar", close: "Cerrar",
discard: "Descartar",
// overview // overview
overview: "Resumen", overview: "Resumen",
connection: "Conexion", connection: "Conexi\xF3n",
chats: "Chats", chats: "Chats",
yourName: "Tu nombre", yourName: "Tu nombre",
namePlaceholder: "Juan P\xE9rez", namePlaceholder: "Juan P\xE9rez",
encryptionUnavailableTitle: "Cifrado no disponible", encryptionUnavailableTitle: "Cifrado no disponible",
encryptionUnavailableMessage: "Obt\xE9n esta aplicaci\xF3n a traves de HTTPS o contin\xFAa sin cifrado", encryptionUnavailableMessage: "Obt\xE9n esta aplicaci\xF3n a traves de HTTPS o contin\xFAa sin cifrado",
serverAddress: "Direccion del servidor", serverAddress: "Direcci\xF3n del servidor",
serverAddressPlaceholder: "wss://192.168.0.69:3000", serverAddressPlaceholder: "wss://192.168.0.69:3000",
connectToServer: "Conectar", connectToServer: "Conectar",
disconnect: "Desconectar", disconnect: "Desconectar",
@@ -853,33 +1050,48 @@
primaryChannel: "Canal principal", primaryChannel: "Canal principal",
primaryChannelPlaceholder: "mi-canal", primaryChannelPlaceholder: "mi-canal",
addChat: "A\xF1adir", addChat: "A\xF1adir",
// messages // chat
showChatOptions: "Mostrar opciones del chat", showChatOptions: "Mostrar opciones del chat",
configureChatTitle: "Configurar chat", configureChatTitle: "Configurar chat",
secondaryChannel: "Canal segundario", secondaryChannel: "Canal secundario",
secondaryChannelPlaceholder: "A\xF1adir canal segundario", secondaryChannelPlaceholder: "A\xF1adir canal secundario",
addSecondaryChannel: "A\xF1adir canal segundario", addSecondaryChannel: "A\xF1adir canal secundario",
removeSecondaryChannel: "Eliminar canal segundario", removeSecondaryChannel: "Eliminar canal secundario",
encryptionKey: "Clave de cifrado", encryptionKey: "Clave de cifrado",
encryptionKeyPlaceholder: "n10d2482dg283hg", encryptionKeyPlaceholder: "n10d2482dg283hg",
showKey: "Mostrar clave", showKey: "Mostrar clave",
clearObjects: "Eliminar todos los objetos",
clearChatMessages: "Eliminar todos los mensajes",
removeChat: "Eliminar chat", removeChat: "Eliminar chat",
clearChatMessages: "Eliminar todos mensajes",
noChatSelected: "Selecciona un chat",
messageInOutbox: "Pendiente", messageInOutbox: "Pendiente",
noChatSelected: "Selecciona un chat",
composerPlaceholder: "Escribe un mensaje...", composerPlaceholder: "Escribe un mensaje...",
sendMessage: "Enviar", sendMessage: "Enviar",
resendMessage: "Enviar de nuevo", // messages
resendMessage: "Reenviar mensaje",
decryptMessage: "Descifrar mensaje", decryptMessage: "Descifrar mensaje",
copyMessage: "Copiar mensaje", copyMessage: "Copiar mensaje",
deleteMessage: "Eliminar este mensaje" deleteMessage: "Eliminar este mensaje",
// objects
showObjects: "mostrar objetos",
createObject: "a\xF1adir nuevo objeto",
untitledObject: "Sin T\xEDtulo",
objectTitle: "T\xEDtulo",
objectTitlePlaceholder: "Mi objeto",
objectVersion: "Version del objeto",
noteContent: "Nota",
noteContentPlaceholder: "Toma nota...",
resendObjects: "Reenviar todos objetos",
deleteObject: "Eliminar objeto"
}, },
de: { de: {
// general // general
set: "OK", set: "OK",
save: "Sichern",
back: "Zur\xFCck", back: "Zur\xFCck",
undoChanges: "\xC4nderungen verwerfen", undoChanges: "\xC4nderungen verwerfen",
close: "Schlie\xDFen", close: "Schlie\xDFen",
discard: "Verwerfen",
// overview // overview
overview: "\xDCbersicht", overview: "\xDCbersicht",
connection: "Verbindung", connection: "Verbindung",
@@ -899,7 +1111,7 @@
primaryChannel: "Hauptkanal", primaryChannel: "Hauptkanal",
primaryChannelPlaceholder: "mein-kanal", primaryChannelPlaceholder: "mein-kanal",
addChat: "Hinzuf\xFCgen", addChat: "Hinzuf\xFCgen",
// messages // chat
showChatOptions: "Chatoptionen einblenden", showChatOptions: "Chatoptionen einblenden",
configureChatTitle: "Chat konfigurieren", configureChatTitle: "Chat konfigurieren",
secondaryChannel: "Zweitkanal", secondaryChannel: "Zweitkanal",
@@ -909,22 +1121,232 @@
encryptionKey: "Schl\xFCssel", encryptionKey: "Schl\xFCssel",
encryptionKeyPlaceholder: "n10d2482dg283hg", encryptionKeyPlaceholder: "n10d2482dg283hg",
showKey: "Schl\xFCssel anzeigen", showKey: "Schl\xFCssel anzeigen",
removeChat: "Chat l\xF6schen", clearObjects: "Alle Objekte l\xF6schen",
clearChatMessages: "Nachrichtenverlauf leeren", clearChatMessages: "Nachrichtenverlauf leeren",
noChatSelected: "Kein Chat ausgew\xE4hlt", removeChat: "Chat l\xF6schen",
messageInOutbox: "Ausstehend", messageInOutbox: "Ausstehend",
noChatSelected: "Kein Chat ausgew\xE4hlt",
composerPlaceholder: "Neue Nachricht...", composerPlaceholder: "Neue Nachricht...",
sendMessage: "Senden", sendMessage: "Senden",
// messages
resendMessage: "Erneut senden", resendMessage: "Erneut senden",
decryptMessage: "Nachricht entschl\xFCsseln", decryptMessage: "Nachricht entschl\xFCsseln",
copyMessage: "Nachricht kopieren", copyMessage: "Nachricht kopieren",
deleteMessage: "Nachricht l\xF6schen" deleteMessage: "Nachricht l\xF6schen",
// objects
showObjects: "Objekte anzeigen",
createObject: "Neues Objekt erstellen",
untitledObject: "Unbenannt",
objectTitle: "Titel",
objectTitlePlaceholder: "Mein Objekt",
objectVersion: "Version des Objekts",
noteContent: "Notiz",
noteContentPlaceholder: "Notiz eingeben...",
resendObjects: "Objekte erneut senden",
deleteObject: "Objekt l\xF6schen"
} }
}; };
var language = navigator.language.substring(0, 2); var language = navigator.language.substring(0, 2);
var translation = allTranslations[language] ?? allTranslations.en; var translation = allTranslations[language] ?? allTranslations.en;
// src/Views/chatOptionModal.tsx // src/Views/Objects/allObjectsView.tsx
function AllObjectsView(chat, selectedObject, isShowingObjectModal) {
const objectConverter = (messageObject) => {
function select() {
selectedObject.value = messageObject;
isShowingObjectModal.value = true;
}
return /* @__PURE__ */ createElement("button", { class: "tile", "on:click": select }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, messageObject.title), /* @__PURE__ */ createElement("span", { class: "secondary" }, messageObject.id)));
};
const content = createProxyState(
[chat.objects],
() => chat.objects.value.size == 0 ? /* @__PURE__ */ createElement("div", { class: "flex-column width-100 height-100 align-center justify-center secondary" }, translation.noObjects) : /* @__PURE__ */ createElement(
"div",
{
class: "grid gap padding",
style: "grid-template-columns: repeat(auto-fill, minmax(350px, 1fr))",
"children:prepend": [chat.objects, objectConverter]
}
)
);
return /* @__PURE__ */ createElement("div", { class: "width-100 height-100", "children:set": content });
}
// src/Views/Objects/noteObjectsView.tsx
function NoteObjectsView(chat, selectedObject, isShowingObjectModal) {
const objectConverter = (messageObject) => {
const latest = chat.getMostRecentContent(messageObject);
function select() {
selectedObject.value = messageObject;
isShowingObjectModal.value = true;
}
return /* @__PURE__ */ createElement("button", { class: "tile", "on:click": select }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("b", null, messageObject.title), /* @__PURE__ */ createElement("span", { class: "secondary ellipsis" }, latest.noteContent.split("\n")[0])));
};
const notes = new ListState();
chat.objects.subscribe(() => {
notes.clear();
chat.objects.value.forEach((messageObject) => {
const latest = chat.getMostRecentContent(messageObject);
if (!latest.noteContent) return;
notes.add(messageObject);
});
});
const content = createProxyState(
[chat.objects],
() => notes.value.size == 0 ? /* @__PURE__ */ createElement("div", { class: "flex-column width-100 height-100 align-center justify-center secondary" }, translation.noNotes) : /* @__PURE__ */ createElement(
"div",
{
class: "grid gap padding",
style: "grid-template-columns: repeat(auto-fill, minmax(350px, 1fr))",
"children:prepend": [notes, objectConverter]
}
)
);
return /* @__PURE__ */ createElement("div", { class: "width-100 height-100", "children:set": content });
}
// src/Views/Objects/objectDetailModal.tsx
function ObjectDetailModal(chat, messageObject, isPresented) {
const editingTitle = new State(messageObject.title);
const selectedMessageObjectId = new State(
chat.getMostRecentContentId(messageObject)
);
const selectedMessageObject = createProxyState(
[selectedMessageObjectId],
() => chat.getObjectContentFromId(messageObject, selectedMessageObjectId.value)
);
const didEditContent = new State(false);
const editingNoteContent = createProxyState(
[selectedMessageObject],
() => selectedMessageObject.value.noteContent ?? ""
);
bulkSubscribe(
[editingNoteContent],
() => didEditContent.value = true
);
function handleKeyDown(e) {
if (!e.metaKey && !e.ctrlKey) return;
switch (e.key) {
case "s":
e.preventDefault();
saveAndClose();
break;
}
}
function closeModal() {
isPresented.value = false;
}
function saveAndClose() {
messageObject.title = editingTitle.value;
if (didEditContent.value == true) {
chat.addObjectContent(messageObject, {
isoDateVersionCreated: (/* @__PURE__ */ new Date()).toISOString(),
id: UUID(),
noteContent: editingNoteContent.value
});
}
chat.addObjectAndSend(messageObject);
closeModal();
}
function deleteAndClose() {
chat.deleteObject(messageObject);
closeModal();
}
return /* @__PURE__ */ createElement("div", { class: "modal", "toggle:open": isPresented, "on:keydown": handleKeyDown }, /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("main", null, /* @__PURE__ */ createElement("h2", null, messageObject.title), /* @__PURE__ */ createElement("span", { class: "secondary" }, messageObject.id), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "flex-column gap" }, /* @__PURE__ */ createElement("label", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "label"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translation.objectTitle), /* @__PURE__ */ createElement(
"input",
{
autofocus: true,
"bind:value": editingTitle,
placeholder: translation.objectTitlePlaceholder
}
))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("label", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "history"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translation.objectVersion), /* @__PURE__ */ createElement("select", { "bind:value": selectedMessageObjectId }, ...chat.getSortedContents(messageObject).map((content) => /* @__PURE__ */ createElement("option", { value: content.id }, new Date(
content.isoDateVersionCreated
).toLocaleString()))), /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_drop_down"))), /* @__PURE__ */ createElement("label", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "sticky_note_2"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translation.noteContent), /* @__PURE__ */ createElement(
"textarea",
{
rows: "5",
"bind:value": editingNoteContent,
placeholder: translation.noteContentPlaceholder
}
))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("button", { class: "danger width-input", "on:click": deleteAndClose }, translation.deleteObject, /* @__PURE__ */ createElement("span", { class: "icon" }, "delete")))), /* @__PURE__ */ createElement("div", { class: "flex-row" }, /* @__PURE__ */ createElement("button", { class: "flex-1 width-100 danger", "on:click": closeModal }, translation.discard), /* @__PURE__ */ createElement("button", { class: "flex-1 width-100 primary", "on:click": saveAndClose }, translation.save, /* @__PURE__ */ createElement("span", { class: "icon" }, "save")))));
}
// src/Views/Objects/chatObjectView.tsx
var viewTypes = {
all: [translation.viewAll, "grid_view"],
notes: [translation.viewNotes, "sticky_note_2"]
};
function ChatObjectView(chat) {
const isShowingObjectModal = new State(false);
const selectedObject = new State(void 0);
const objectModal = createProxyState(
[chat.objects, selectedObject],
() => {
if (selectedObject.value == void 0) return /* @__PURE__ */ createElement("div", null);
return ObjectDetailModal(
chat,
selectedObject.value,
isShowingObjectModal
);
}
);
const selectedViewType = new State("all");
const mainView = createProxyState([selectedViewType], () => {
function getViewFunction() {
switch (selectedViewType.value) {
case "notes":
return NoteObjectsView;
default:
return AllObjectsView;
}
}
return getViewFunction()(chat, selectedObject, isShowingObjectModal);
});
function createObject() {
const newObject = chat.createObjectFromTitle(translation.untitledObject);
chat.addObjectAndSend(newObject);
selectedObject.value = newObject;
isShowingObjectModal.value = true;
}
return /* @__PURE__ */ createElement("div", { class: "chat-object-view flex-column" }, /* @__PURE__ */ createElement("div", { class: "flex-row align-center border-bottom" }, /* @__PURE__ */ createElement(
"button",
{
class: "primary height-100",
"on:click": createObject,
"aria-label": translation.createObject
},
/* @__PURE__ */ createElement("span", { class: "icon" }, "add")
), /* @__PURE__ */ createElement("div", { class: "padding-sm flex flex-row gap justify-center scroll-h width-100" }, ...Object.keys(viewTypes).map(
(key) => ViewTypeToggle(key, selectedViewType)
)), /* @__PURE__ */ createElement(
"button",
{
class: "height-100",
"on:click": createObject,
"aria-label": translation.createObject
},
/* @__PURE__ */ createElement("span", { class: "icon" }, "visibility")
)), /* @__PURE__ */ createElement(
"div",
{
class: "width-100 height-100 flex-1 scroll-h",
"children:set": mainView
}
), /* @__PURE__ */ createElement("div", { "children:set": objectModal }));
}
function ViewTypeToggle(key, selection) {
const [label, icon] = viewTypes[key];
function select() {
selection.value = key;
}
const isSelected = createProxyState(
[selection],
() => selection.value == key
);
return /* @__PURE__ */ createElement("button", { "aria-label": label, "on:click": select, "toggle:selected": isSelected }, /* @__PURE__ */ createElement("span", { class: "icon" }, icon));
}
// src/Views/Chat/chatOptionModal.tsx
function ChatOptionModal(chat, isPresented) { function ChatOptionModal(chat, isPresented) {
function closeModal() { function closeModal() {
isPresented.value = false; isPresented.value = false;
@@ -1011,7 +1433,16 @@
"bind:value": chat.encryptionKey, "bind:value": chat.encryptionKey,
"set:type": inputType "set:type": inputType
} }
))), /* @__PURE__ */ createElement("label", { class: "inline margin-0" }, /* @__PURE__ */ createElement("input", { type: "checkbox", "bind:checked": shouldShowKey }), translation.showKey)), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "flex-column gap" }, /* @__PURE__ */ createElement( ))), /* @__PURE__ */ createElement("label", { class: "inline margin-0" }, /* @__PURE__ */ createElement("input", { type: "checkbox", "bind:checked": shouldShowKey }), translation.showKey)), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "flex-column gap width-input" }, /* @__PURE__ */ createElement("button", { "on:click": chat.resendObjects }, translation.resendObjects, /* @__PURE__ */ createElement("span", { class: "icon" }, "replay"))), /* @__PURE__ */ createElement("hr", null), /* @__PURE__ */ createElement("div", { class: "flex-column gap width-input" }, /* @__PURE__ */ createElement(
"button",
{
class: "danger",
"on:click": chat.clearObjects,
"toggle:disabled": chat.cannotClearObjects
},
translation.clearObjects,
/* @__PURE__ */ createElement("span", { class: "icon" }, "deployed_code")
), /* @__PURE__ */ createElement(
"button", "button",
{ {
class: "danger", class: "danger",
@@ -1019,11 +1450,11 @@
"toggle:disabled": chat.cannotClearMessages "toggle:disabled": chat.cannotClearMessages
}, },
translation.clearChatMessages, translation.clearChatMessages,
/* @__PURE__ */ createElement("span", { class: "icon" }, "delete_sweep") /* @__PURE__ */ createElement("span", { class: "icon" }, "chat_error")
), /* @__PURE__ */ createElement("button", { class: "danger", "on:click": deleteChat }, translation.removeChat, /* @__PURE__ */ createElement("span", { class: "icon" }, "delete")))), /* @__PURE__ */ createElement("button", { "on:click": closeModal }, translation.close, /* @__PURE__ */ createElement("span", { class: "icon" }, "close")))); ), /* @__PURE__ */ createElement("button", { class: "danger", "on:click": deleteChat }, translation.removeChat, /* @__PURE__ */ createElement("span", { class: "icon" }, "delete_forever")))), /* @__PURE__ */ createElement("button", { "on:click": closeModal }, translation.close, /* @__PURE__ */ createElement("span", { class: "icon" }, "close"))));
} }
// src/Views/messageComposer.tsx // src/Views/Chat/messageComposer.tsx
function MessageComposer(chat) { function MessageComposer(chat) {
return /* @__PURE__ */ createElement("div", { class: "flex-row width-100" }, " ", /* @__PURE__ */ createElement( return /* @__PURE__ */ createElement("div", { class: "flex-row width-100" }, " ", /* @__PURE__ */ createElement(
"input", "input",
@@ -1032,20 +1463,20 @@
style: "max-width: unset", style: "max-width: unset",
placeholder: translation.composerPlaceholder, placeholder: translation.composerPlaceholder,
"bind:value": chat.composingMessage, "bind:value": chat.composingMessage,
"on:enter": chat.sendNewMessage "on:enter": chat.sendMessageFromComposer
} }
), /* @__PURE__ */ createElement( ), /* @__PURE__ */ createElement(
"button", "button",
{ {
class: "primary", class: "primary",
"on:click": chat.sendNewMessage, "on:click": chat.sendMessageFromComposer,
"toggle:disabled": chat.cannotSendMessage "toggle:disabled": chat.cannotSendMessage
}, },
/* @__PURE__ */ createElement("span", { class: "icon" }, "send") /* @__PURE__ */ createElement("span", { class: "icon" }, "send")
)); ));
} }
// src/Views/threadView.tsx // src/Views/Chat/threadView.tsx
function ThreadView(chat) { function ThreadView(chat) {
const messageConverter = (message) => { const messageConverter = (message) => {
function resendMessage() { function resendMessage() {
@@ -1108,15 +1539,19 @@
"children:append": [chat.outbox, outboxMessageConverter] "children:append": [chat.outbox, outboxMessageConverter]
} }
); );
const listWrapper = /* @__PURE__ */ createElement("div", { class: "flex-column gap" }, messageList, outboxList); const listWrapper = /* @__PURE__ */ createElement("div", { class: "thread-view flex-column gap" }, messageList, outboxList);
function scrollToBottom() { function scrollToBottom() {
const scrollFromBottom = listWrapper.scrollHeight - (listWrapper.scrollTop + listWrapper.offsetHeight);
if (scrollFromBottom > 400) return;
listWrapper.scrollTop = listWrapper.scrollHeight; listWrapper.scrollTop = listWrapper.scrollHeight;
} }
chat.messages.handleAddition(scrollToBottom); function scrollToBottomIfAppropriate() {
chat.outbox.handleAddition(scrollToBottom); const scrollFromBottom = listWrapper.scrollHeight - (listWrapper.scrollTop + listWrapper.offsetHeight);
setTimeout(() => listWrapper.scrollTop = listWrapper.scrollHeight, 50); if (scrollFromBottom > 400) return;
scrollToBottom();
}
chat.messages.handleAddition(scrollToBottomIfAppropriate);
chat.outbox.handleAddition(scrollToBottomIfAppropriate);
isShowingChatTools.subscribe(() => scrollToBottom());
setTimeout(() => scrollToBottom(), 50);
return listWrapper; return listWrapper;
} }
@@ -1132,6 +1567,14 @@
} }
return [ return [
/* @__PURE__ */ createElement("header", { class: "padding-0" }, /* @__PURE__ */ createElement("span", { class: "flex-row align-center" }, /* @__PURE__ */ createElement("button", { "aria-label": translation.back, "on:click": closeChatView }, /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_back")), /* @__PURE__ */ createElement("span", { "subscribe:innerText": chat.primaryChannel })), /* @__PURE__ */ createElement("span", null, /* @__PURE__ */ createElement( /* @__PURE__ */ createElement("header", { class: "padding-0" }, /* @__PURE__ */ createElement("span", { class: "flex-row align-center" }, /* @__PURE__ */ createElement("button", { "aria-label": translation.back, "on:click": closeChatView }, /* @__PURE__ */ createElement("span", { class: "icon" }, "arrow_back")), /* @__PURE__ */ createElement("span", { "subscribe:innerText": chat.primaryChannel })), /* @__PURE__ */ createElement("span", null, /* @__PURE__ */ createElement(
"button",
{
"aria-label": translation.showObjects,
"on:click": toggleChatTools,
"toggle:selected": isShowingChatTools
},
/* @__PURE__ */ createElement("span", { class: "icon" }, "deployed_code")
), /* @__PURE__ */ createElement(
"button", "button",
{ {
"aria-label": translation.showChatOptions, "aria-label": translation.showChatOptions,
@@ -1139,15 +1582,23 @@
}, },
/* @__PURE__ */ createElement("span", { class: "icon" }, "tune") /* @__PURE__ */ createElement("span", { class: "icon" }, "tune")
))), ))),
ChatObjectView(chat),
ThreadView(chat), ThreadView(chat),
/* @__PURE__ */ createElement("footer", null, MessageComposer(chat)), /* @__PURE__ */ createElement("footer", null, MessageComposer(chat)),
ChatOptionModal(chat, isShowingOptions) ChatOptionModal(chat, isShowingOptions)
]; ];
}); });
return /* @__PURE__ */ createElement("article", { id: "message-tab", "children:set": messageTabContent }); return /* @__PURE__ */ createElement(
"article",
{
"toggle:showingchattools": isShowingChatTools,
id: "message-tab",
"children:set": messageTabContent
}
);
} }
// src/Views/chatListSection.tsx // src/Views/Overview/chatListSection.tsx
var chatConverter = (chat) => { var chatConverter = (chat) => {
function select() { function select() {
selectChat(chat); selectChat(chat);
@@ -1194,7 +1645,7 @@
)); ));
} }
// src/Views/connectionSection.tsx // src/Views/Overview/connectionSection.tsx
var addressConverter = (address) => { var addressConverter = (address) => {
return /* @__PURE__ */ createElement("option", null, address); return /* @__PURE__ */ createElement("option", null, address);
}; };
@@ -1261,7 +1712,7 @@
))); )));
} }
// src/Views/personalSection.tsx // src/Views/Overview/personalSection.tsx
function PersonalSection() { function PersonalSection() {
return /* @__PURE__ */ createElement("div", { class: "flex-column" }, /* @__PURE__ */ createElement("label", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "account_circle"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translation.yourName), /* @__PURE__ */ createElement( return /* @__PURE__ */ createElement("div", { class: "flex-column" }, /* @__PURE__ */ createElement("label", { class: "tile" }, /* @__PURE__ */ createElement("span", { class: "icon" }, "account_circle"), /* @__PURE__ */ createElement("div", null, /* @__PURE__ */ createElement("span", null, translation.yourName), /* @__PURE__ */ createElement(
"input", "input",
+30 -6
View File
@@ -140,10 +140,12 @@ select,
@media (hover: hover) { @media (hover: hover) {
button:hover, button:hover,
select:hover, select:hover,
button.tile.ghost:hover, button.tile.ghost:hover {
[selected]:active {
background-color: var(--surface-hover); background-color: var(--surface-hover);
} }
[selected]:hover {
background-color: var(--surface);
}
} }
button:active, button:active,
select:active, select:active,
@@ -151,6 +153,9 @@ button.tile.ghost:active,
[selected] { [selected] {
background-color: var(--surface-active); background-color: var(--surface-active);
} }
[selected]:active {
background-color: var(--surface-hover);
}
header button { header button {
background-color: transparent; background-color: transparent;
} }
@@ -318,6 +323,10 @@ hr {
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
} }
.border-bottom {
border-bottom: 1px solid var(--border) !important;
}
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):focus:not( input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):focus:not(
label.tile * label.tile *
), ),
@@ -480,7 +489,7 @@ input[type="radio"],
} }
.justify-center { .justify-center {
justify-content: center !important; justify-content: safe center !important;
} }
.justify-end { .justify-end {
@@ -577,6 +586,10 @@ textarea,
padding: var(--control-padding) !important; padding: var(--control-padding) !important;
} }
.padding-sm {
padding: var(--gap-between-tiles) !important;
}
.modal main { .modal main {
padding: var(--modal-padding); padding: var(--modal-padding);
} }
@@ -597,7 +610,7 @@ label.tile:has(> span.icon) {
padding-left: var(--control-padding); padding-left: var(--control-padding);
} }
label.tile:has(> span.icon) input { label.tile:has(> span.icon) :is(input, select, textarea) {
padding-left: 0px; padding-left: 0px;
} }
@@ -730,8 +743,7 @@ button:not(.tile):not(header *),
footer, footer,
input:not([type="checkbox"]):not([type="radio"]), input:not([type="checkbox"]):not([type="radio"]),
select, select,
select + .icon, select + .icon {
textarea {
height: var(--control-height); height: var(--control-height);
} }
@@ -781,6 +793,10 @@ input[type="radio"] {
height: 16px; height: 16px;
} }
label.tile textarea {
resize: vertical;
}
/* /*
text text
*/ */
@@ -952,6 +968,14 @@ main {
overflow-x: scroll; overflow-x: scroll;
} }
.scroll-h {
overflow-x: scroll !important;
}
.scroll-v {
overflow-y: scroll !important;
}
.ellipsis { .ellipsis {
text-overflow: ellipsis; text-overflow: ellipsis;
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"dependencies": { "dependencies": {
"bloatless-react": "^1.3.1-beta1", "bloatless-react": "^1.3.7",
"esbuild": "latest", "esbuild": "latest",
"udn-frontend": "^1.0.7-beta1" "udn-frontend": "^1.0.7-beta1"
}, },
+198 -45
View File
@@ -21,6 +21,22 @@ export interface ChatMessage {
sender: string; sender: string;
body: string; body: string;
isoDate: string; isoDate: string;
messageObjectString?: string;
}
// object
export interface MessageObject {
id: string;
title: string;
contentVersions: { [key: string]: MessageObjectContent };
}
export interface MessageObjectContent {
isoDateVersionCreated: string;
id: string;
noteContent?: string;
} }
// chat // chat
@@ -34,7 +50,9 @@ export class Chat {
encryptionKey: React.State<string>; encryptionKey: React.State<string>;
messages: React.ListState<ChatMessage>; messages: React.ListState<ChatMessage>;
objects: React.MapState<MessageObject>;
outbox: React.ListState<ChatMessage>; outbox: React.ListState<ChatMessage>;
objectOutbox: React.MapState<MessageObject>;
isOutBoxEmpty: React.State<boolean>; isOutBoxEmpty: React.State<boolean>;
composingMessage: React.State<string>; composingMessage: React.State<string>;
@@ -47,6 +65,7 @@ export class Chat {
cannotSetChannel: React.State<boolean>; cannotSetChannel: React.State<boolean>;
cannotUndoChannel: React.State<boolean>; cannotUndoChannel: React.State<boolean>;
cannotClearMessages: React.State<boolean>; cannotClearMessages: React.State<boolean>;
cannotClearObjects: React.State<boolean>;
// init // init
constructor(id: string = React.UUID()) { constructor(id: string = React.UUID()) {
@@ -68,7 +87,9 @@ export class Chat {
); );
this.encryptionKey = React.restoreState(storageKeys.encyptionKey(id), ""); this.encryptionKey = React.restoreState(storageKeys.encyptionKey(id), "");
this.messages = React.restoreListState(storageKeys.messages(id)); this.messages = React.restoreListState(storageKeys.messages(id));
this.objects = React.restoreMapState(storageKeys.objects(id));
this.outbox = React.restoreListState(storageKeys.outbox(id)); this.outbox = React.restoreListState(storageKeys.outbox(id));
this.objectOutbox = React.restoreMapState(storageKeys.itemOutbox(id));
this.isOutBoxEmpty = React.createProxyState( this.isOutBoxEmpty = React.createProxyState(
[this.outbox], [this.outbox],
() => this.outbox.value.size == 0 () => this.outbox.value.size == 0
@@ -112,6 +133,10 @@ export class Chat {
[this.messages], [this.messages],
() => this.messages.value.size == 0 () => this.messages.value.size == 0
); );
this.cannotClearObjects = React.createProxyState(
[this.objects],
() => this.objects.value.size == 0
);
} }
// general // general
@@ -131,9 +156,21 @@ export class Chat {
if (channels.indexOf(this.primaryChannel.value) == -1) return; if (channels.indexOf(this.primaryChannel.value) == -1) return;
if (data.subscribed != undefined) this.handleSubscription(data.subscribed); if (data.subscribed != undefined) this.handleSubscription(data.subscribed);
if (!data.messageBody) return; if (!data.messageBody) return;
const { sender, body, channel, isoDate } = JSON.parse(data.messageBody);
const message: ChatMessage = JSON.parse(data.messageBody);
const { sender, body, channel, isoDate, messageObjectString } = message;
if (messageObjectString) {
const decryptedMessageObjectString = await decryptString(
messageObjectString,
this.encryptionKey.value
);
const messageObject = JSON.parse(decryptedMessageObjectString);
if (messageObject.id && messageObject.title)
return this.handleMessageObject(messageObject);
}
this.handleMessage({ this.handleMessage({
sender, sender,
body: await decryptString(body, this.encryptionKey.value), body: await decryptString(body, this.encryptionKey.value),
@@ -155,25 +192,28 @@ export class Chat {
if (selectedChat.value != this) this.hasUnreadMessages.value = true; if (selectedChat.value != this) this.hasUnreadMessages.value = true;
}; };
handleMessageObject = (messageObject: MessageObject): void => {
this.addObject(messageObject);
};
// sending
sendMessagesInOutbox = () => {
this.outbox.value.forEach(async (message) => {
const isSent = await this.sendMessage(message);
if (isSent == true) this.outbox.remove(message);
});
this.objectOutbox.value.forEach(async (messageObject) => {
const chatMessage = await this.createChatMessage("", messageObject);
const isSent = await this.sendMessage(chatMessage);
if (isSent == true) this.objectOutbox.remove(messageObject.id);
});
};
// messages // messages
sendNewMessage = async (): Promise<void> => { createChatMessage = async (
if (this.cannotSendMessage.value == true) return; messageText: string,
messageObject?: MessageObject
await this.sendMessageText(this.composingMessage.value); ): Promise<ChatMessage> => {
this.composingMessage.value = "";
};
sendMessageText = async (text: string): Promise<void> => {
const message = await this.createMessage(text);
this.sendExistingMessage(message);
};
resendMessage = (message: ChatMessage) => {
if (this.cannotResendMessage.value == true) return;
this.sendMessageText(message.body);
};
createMessage = async (messageText: string): Promise<ChatMessage> => {
// get channels // get channels
const secondaryChannelNames: string[] = [ const secondaryChannelNames: string[] = [
...this.secondaryChannels.value.values(), ...this.secondaryChannels.value.values(),
@@ -185,54 +225,167 @@ export class Chat {
// create object // create object
const joinedChannelName = allChannelNames.join("/"); const joinedChannelName = allChannelNames.join("/");
return { const chatMessage: ChatMessage = {
channel: joinedChannelName, channel: joinedChannelName,
sender: senderName.value, sender: senderName.value,
body: messageText, body: messageText,
isoDate: new Date().toISOString(), isoDate: new Date().toISOString(),
}; };
if (messageObject)
chatMessage.messageObjectString = await encryptString(
JSON.stringify(messageObject),
this.encryptionKey.value
);
return chatMessage;
}; };
sendExistingMessage = async (chatMessage: ChatMessage): Promise<void> => { sendMessageFromComposer = async (): Promise<void> => {
if (isConnected.value == true && this.isSubscribed.value == true) { if (this.cannotSendMessage.value == true) return;
const encrypted =
this.encryptionKey.value == ""
? chatMessage.body
: await encryptString(chatMessage.body, this.encryptionKey.value);
chatMessage.body = encrypted; await this.sendMessageFromText(this.composingMessage.value);
this.composingMessage.value = "";
const messageString = JSON.stringify(chatMessage);
UDN.sendMessage(chatMessage.channel, messageString);
} else {
this.outbox.add(chatMessage);
}
}; };
sendMessagesInOutbox = () => { sendMessageFromText = async (text: string): Promise<void> => {
this.outbox.value.forEach((message) => { const chatMessage = await this.createChatMessage(text);
this.sendExistingMessage(message); this.outbox.add(chatMessage);
this.outbox.remove(message); this.sendMessagesInOutbox();
}); };
resendMessage = (chatMessage: ChatMessage) => {
if (this.cannotResendMessage.value == true) return;
this.sendMessageFromText(chatMessage.body);
}; };
clearMessages = (): void => { clearMessages = (): void => {
this.messages.clear(); this.messages.clear();
}; };
deleteMessage = (message: ChatMessage): void => { deleteMessage = (chatMessage: ChatMessage): void => {
this.messages.remove(message); this.messages.remove(chatMessage);
}; };
deleteOutboxMessage = (message: ChatMessage): void => { deleteOutboxMessage = (chatMessage: ChatMessage): void => {
this.outbox.remove(message); this.outbox.remove(chatMessage);
}; };
decryptReceivedMessage = async (message: ChatMessage): Promise<void> => { decryptReceivedMessage = async (chatMessage: ChatMessage): Promise<void> => {
message.body = await decryptString(message.body, this.encryptionKey.value); chatMessage.body = await decryptString(
chatMessage.body,
this.encryptionKey.value
);
this.messages.callSubscriptions(); this.messages.callSubscriptions();
}; };
sendMessage = async (chatMessage: ChatMessage): Promise<boolean> => {
if (isConnected.value == false || this.isSubscribed.value == false)
return false;
const encryptedBody =
this.encryptionKey.value == ""
? chatMessage.body
: await encryptString(chatMessage.body, this.encryptionKey.value);
chatMessage.body = encryptedBody;
const messageString = JSON.stringify(chatMessage);
UDN.sendMessage(chatMessage.channel, messageString);
return true;
};
// objects
createObjectFromTitle = (title: string): MessageObject => {
const firstObjectContent = this.createObjectContent();
const messageObject: MessageObject = {
id: React.UUID(),
title,
contentVersions: {},
};
this.addObjectContent(messageObject, firstObjectContent);
return messageObject;
};
createObjectContent = () => {
return {
id: React.UUID(),
isoDateVersionCreated: new Date().toISOString(),
};
};
addObjectContent = (
messageObject: MessageObject,
content: MessageObjectContent
): void => {
messageObject.contentVersions[content.id] = content;
};
addObject = (messageObject: MessageObject): void => {
const existingObject = this.objects.value.get(messageObject.id);
if (existingObject) {
existingObject.title = messageObject.title;
Object.values(messageObject.contentVersions).forEach((content) => {
this.addObjectContent(existingObject, content);
});
this.objects.set(existingObject.id, existingObject);
} else {
this.objects.set(messageObject.id, messageObject);
}
};
addObjectAndSend = (messageObject: MessageObject): void => {
this.addObject(messageObject);
this.sendObject(messageObject);
};
sendObject = (messageObject: MessageObject): void => {
this.objectOutbox.set(messageObject.id, messageObject);
this.sendMessagesInOutbox();
};
deleteObject = (messageObject: MessageObject): void => {
this.objects.remove(messageObject.id);
this.objectOutbox.remove(messageObject.id);
};
resendObjects = (): void => {
this.objects.value.forEach((messageObject) => {
this.sendObject(messageObject);
});
};
clearObjects = (): void => {
this.objects.clear();
};
getSortedContents = (
messageObject: MessageObject
): MessageObjectContent[] => {
const contents = Object.values(messageObject.contentVersions);
contents.sort((a, b) =>
a.isoDateVersionCreated > b.isoDateVersionCreated ? 0 : 1
);
return contents;
};
getMostRecentContentId = (messageObject: MessageObject): string => {
const contents = Object.values(messageObject.contentVersions);
return contents[contents.length - 1].id;
};
getMostRecentContent = (messageObject: MessageObject): MessageObjectContent => {
const id = this.getMostRecentContentId(messageObject);
return this.getObjectContentFromId(messageObject, id);
}
getObjectContentFromId = (
messageObject: MessageObject,
contentId: string
): MessageObjectContent => {
return messageObject.contentVersions[contentId];
};
// channel // channel
setChannel = (): void => { setChannel = (): void => {
if (this.cannotSetChannel.value == true) return; if (this.cannotSetChannel.value == true) return;
+6
View File
@@ -139,6 +139,7 @@ export const chats = new React.ListState<Chat>();
export const chatIds = React.restoreListState<string>("chat-ids"); export const chatIds = React.restoreListState<string>("chat-ids");
export const selectedChat = new React.State<Chat | undefined>(undefined); export const selectedChat = new React.State<Chat | undefined>(undefined);
export const isShowingChatTools = new React.State(false);
export const newChatName = new React.State(""); export const newChatName = new React.State("");
export const cannotCreateChat = React.createProxyState( export const cannotCreateChat = React.createProxyState(
@@ -154,6 +155,7 @@ export function createChat() {
} }
export function closeChatView() { export function closeChatView() {
isShowingChatTools.value = false;
selectedChat.value = undefined; selectedChat.value = undefined;
document.getElementById("settings-tab")?.scrollIntoView(); document.getElementById("settings-tab")?.scrollIntoView();
} }
@@ -164,6 +166,10 @@ export function selectChat(chat: Chat) {
document.getElementById("message-tab")?.scrollIntoView(); document.getElementById("message-tab")?.scrollIntoView();
} }
export function toggleChatTools() {
isShowingChatTools.value = !isShowingChatTools.value;
}
chatIds.value.forEach((id) => chats.add(new Chat(id))); chatIds.value.forEach((id) => chats.add(new Chat(id)));
// INIT // INIT
+20 -5
View File
@@ -1,10 +1,11 @@
import * as React from "bloatless-react"; import * as React from "bloatless-react";
import { closeChatView, selectedChat } from "../Model/model"; import { closeChatView, isShowingChatTools, selectedChat, toggleChatTools } from "../Model/model";
import { ChatOptionModal } from "../Views/chatOptionModal"; import { ChatObjectView } from "../Views/Objects/chatObjectView";
import { MessageComposer } from "../Views/messageComposer"; import { ChatOptionModal } from "../Views/Chat/chatOptionModal";
import { ThreadView } from "../Views/threadView"; import { MessageComposer } from "../Views/Chat/messageComposer";
import { ThreadView } from "../Views/Chat/threadView";
import { translation } from "../translations"; import { translation } from "../translations";
export function MessageTab() { export function MessageTab() {
@@ -33,6 +34,13 @@ export function MessageTab() {
<span subscribe:innerText={chat.primaryChannel}></span> <span subscribe:innerText={chat.primaryChannel}></span>
</span> </span>
<span> <span>
<button
aria-label={translation.showObjects}
on:click={toggleChatTools}
toggle:selected={isShowingChatTools}
>
<span class="icon">deployed_code</span>
</button>
<button <button
aria-label={translation.showChatOptions} aria-label={translation.showChatOptions}
on:click={showOptions} on:click={showOptions}
@@ -42,6 +50,7 @@ export function MessageTab() {
</span> </span>
</header>, </header>,
ChatObjectView(chat),
ThreadView(chat), ThreadView(chat),
<footer>{MessageComposer(chat)}</footer>, <footer>{MessageComposer(chat)}</footer>,
@@ -50,5 +59,11 @@ export function MessageTab() {
]; ];
}); });
return <article id="message-tab" children:set={messageTabContent}></article>; return (
<article
toggle:showingchattools={isShowingChatTools}
id="message-tab"
children:set={messageTabContent}
></article>
);
} }
+3 -3
View File
@@ -2,9 +2,9 @@ import * as React from "bloatless-react";
import { isConnected, isEncryptionAvailable } from "../Model/model"; import { isConnected, isEncryptionAvailable } from "../Model/model";
import { ChatListSection } from "../Views/chatListSection"; import { ChatListSection } from "../Views/Overview/chatListSection";
import { ConnectionSection } from "../Views/connectionSection"; import { ConnectionSection } from "../Views/Overview/connectionSection";
import { PersonalSection } from "../Views/personalSection"; import { PersonalSection } from "../Views/Overview/personalSection";
import { translation } from "../translations"; import { translation } from "../translations";
export function OverviewTab() { export function OverviewTab() {
@@ -1,8 +1,8 @@
import * as React from "bloatless-react"; import * as React from "bloatless-react";
import { Chat } from "../Model/chatModel"; import { Chat } from "../../Model/chatModel";
import { closeChatView } from "../Model/model"; import { closeChatView } from "../../Model/model";
import { translation } from "../translations"; import { translation } from "../../translations";
export function ChatOptionModal(chat: Chat, isPresented: React.State<boolean>) { export function ChatOptionModal(chat: Chat, isPresented: React.State<boolean>) {
function closeModal() { function closeModal() {
@@ -20,7 +20,7 @@ export function ChatOptionModal(chat: Chat, isPresented: React.State<boolean>) {
shouldShowKey.value == true ? "text" : "password" shouldShowKey.value == true ? "text" : "password"
); );
const secondaryChannelConverter: React.ListItemConverter<string> = ( const secondaryChannelConverter: React.StateItemConverter<string> = (
secondaryChannel secondaryChannel
) => { ) => {
function remove() { function remove() {
@@ -131,19 +131,37 @@ export function ChatOptionModal(chat: Chat, isPresented: React.State<boolean>) {
<hr></hr> <hr></hr>
<div class="flex-column gap"> <div class="flex-column gap width-input">
<button on:click={chat.resendObjects}>
{translation.resendObjects}
<span class="icon">replay</span>
</button>
</div>
<hr></hr>
<div class="flex-column gap width-input">
<button
class="danger"
on:click={chat.clearObjects}
toggle:disabled={chat.cannotClearObjects}
>
{translation.clearObjects}
<span class="icon">deployed_code</span>
</button>
<button <button
class="danger" class="danger"
on:click={chat.clearMessages} on:click={chat.clearMessages}
toggle:disabled={chat.cannotClearMessages} toggle:disabled={chat.cannotClearMessages}
> >
{translation.clearChatMessages} {translation.clearChatMessages}
<span class="icon">delete_sweep</span> <span class="icon">chat_error</span>
</button> </button>
<button class="danger" on:click={deleteChat}> <button class="danger" on:click={deleteChat}>
{translation.removeChat} {translation.removeChat}
<span class="icon">delete</span> <span class="icon">delete_forever</span>
</button> </button>
</div> </div>
</main> </main>
@@ -1,7 +1,7 @@
import * as React from "bloatless-react"; import * as React from "bloatless-react";
import { Chat } from "../Model/chatModel"; import { Chat } from "../../Model/chatModel";
import { translation } from "../translations"; import { translation } from "../../translations";
export function MessageComposer(chat: Chat) { export function MessageComposer(chat: Chat) {
return ( return (
@@ -12,11 +12,11 @@ export function MessageComposer(chat: Chat) {
style="max-width: unset" style="max-width: unset"
placeholder={translation.composerPlaceholder} placeholder={translation.composerPlaceholder}
bind:value={chat.composingMessage} bind:value={chat.composingMessage}
on:enter={chat.sendNewMessage} on:enter={chat.sendMessageFromComposer}
></input> ></input>
<button <button
class="primary" class="primary"
on:click={chat.sendNewMessage} on:click={chat.sendMessageFromComposer}
toggle:disabled={chat.cannotSendMessage} toggle:disabled={chat.cannotSendMessage}
> >
<span class="icon">send</span> <span class="icon">send</span>
@@ -1,11 +1,12 @@
import * as React from "bloatless-react"; import * as React from "bloatless-react";
import { Chat, ChatMessage } from "../Model/chatModel"; import { Chat, ChatMessage } from "../../Model/chatModel";
import { translation } from "../translations"; import { isShowingChatTools } from "../../Model/model";
import { translation } from "../../translations";
export function ThreadView(chat: Chat) { export function ThreadView(chat: Chat) {
const messageConverter: React.ListItemConverter<ChatMessage> = (message) => { const messageConverter: React.StateItemConverter<ChatMessage> = (message) => {
function resendMessage() { function resendMessage() {
chat.resendMessage(message); chat.resendMessage(message);
} }
@@ -72,7 +73,7 @@ export function ThreadView(chat: Chat) {
); );
}; };
const outboxMessageConverter: React.ListItemConverter<ChatMessage> = ( const outboxMessageConverter: React.StateItemConverter<ChatMessage> = (
message message
) => { ) => {
function remove() { function remove() {
@@ -116,24 +117,29 @@ export function ThreadView(chat: Chat) {
></div> ></div>
); );
const listWrapper = ( const listWrapper = (
<div class="flex-column gap"> <div class="thread-view flex-column gap">
{messageList} {messageList}
{outboxList} {outboxList}
</div> </div>
); );
function scrollToBottom() { function scrollToBottom() {
listWrapper.scrollTop = listWrapper.scrollHeight;
}
function scrollToBottomIfAppropriate() {
const scrollFromBottom = const scrollFromBottom =
listWrapper.scrollHeight - listWrapper.scrollHeight -
(listWrapper.scrollTop + listWrapper.offsetHeight); (listWrapper.scrollTop + listWrapper.offsetHeight);
if (scrollFromBottom > 400) return; if (scrollFromBottom > 400) return;
listWrapper.scrollTop = listWrapper.scrollHeight; scrollToBottom();
} }
chat.messages.handleAddition(scrollToBottom); chat.messages.handleAddition(scrollToBottomIfAppropriate);
chat.outbox.handleAddition(scrollToBottom); chat.outbox.handleAddition(scrollToBottomIfAppropriate);
setTimeout(() => (listWrapper.scrollTop = listWrapper.scrollHeight), 50); isShowingChatTools.subscribe(() => scrollToBottom());
setTimeout(() => scrollToBottom(), 50);
return listWrapper; return listWrapper;
} }
+45
View File
@@ -0,0 +1,45 @@
import * as React from "bloatless-react";
import { Chat, MessageObject } from "../../Model/chatModel";
import { translation } from "../../translations";
export function AllObjectsView(
chat: Chat,
selectedObject: React.State<MessageObject | undefined>,
isShowingObjectModal: React.State<boolean>
) {
const objectConverter: React.StateItemConverter<MessageObject> = (
messageObject
) => {
function select() {
selectedObject.value = messageObject;
isShowingObjectModal.value = true;
}
return (
<button class="tile" on:click={select}>
<div>
<b>{messageObject.title}</b>
<span class="secondary">{messageObject.id}</span>
</div>
</button>
);
};
const content = React.createProxyState([chat.objects], () =>
chat.objects.value.size == 0 ? (
<div class="flex-column width-100 height-100 align-center justify-center secondary">
{translation.noObjects}
</div>
) : (
<div
class="grid gap padding"
style="grid-template-columns: repeat(auto-fill, minmax(350px, 1fr))"
children:prepend={[chat.objects, objectConverter]}
></div>
)
);
return <div class="width-100 height-100" children:set={content}></div>;
}
+111
View File
@@ -0,0 +1,111 @@
import * as React from "bloatless-react";
import { Chat, MessageObject } from "../../Model/chatModel";
import { AllObjectsView } from "./allObjectsView";
import { NoteObjectsView } from "./noteObjectsView";
import { ObjectDetailModal } from "./objectDetailModal";
import { translation } from "../../translations";
export const viewTypes = {
all: [translation.viewAll, "grid_view"],
notes: [translation.viewNotes, "sticky_note_2"],
};
export function ChatObjectView(chat: Chat) {
// selection and modal
const isShowingObjectModal = new React.State(false);
const selectedObject = new React.State<MessageObject | undefined>(undefined);
const objectModal = React.createProxyState(
[chat.objects, selectedObject],
() => {
if (selectedObject.value == undefined) return <div></div>;
return ObjectDetailModal(
chat,
selectedObject.value,
isShowingObjectModal
);
}
);
// view type
const selectedViewType = new React.State<keyof typeof viewTypes>("all");
const mainView = React.createProxyState([selectedViewType], () => {
function getViewFunction() {
switch (selectedViewType.value) {
case "notes":
return NoteObjectsView;
default:
return AllObjectsView;
}
}
return getViewFunction()(chat, selectedObject, isShowingObjectModal);
});
// methods
function createObject() {
const newObject = chat.createObjectFromTitle(translation.untitledObject);
chat.addObjectAndSend(newObject);
selectedObject.value = newObject;
isShowingObjectModal.value = true;
}
return (
<div class="chat-object-view flex-column">
<div class="flex-row align-center border-bottom">
<button
class="primary height-100"
on:click={createObject}
aria-label={translation.createObject}
>
<span class="icon">add</span>
</button>
<div class="padding-sm flex flex-row gap justify-center scroll-h width-100">
{...Object.keys(viewTypes).map((key: any) =>
ViewTypeToggle(key, selectedViewType)
)}
</div>
<button
class="height-100"
on:click={createObject}
aria-label={translation.createObject}
>
<span class="icon">visibility</span>
</button>
</div>
<div
class="width-100 height-100 flex-1 scroll-h"
children:set={mainView}
></div>
<div children:set={objectModal}></div>
</div>
);
}
function ViewTypeToggle(
key: keyof typeof viewTypes,
selection: React.State<keyof typeof viewTypes>
) {
const [label, icon] = viewTypes[key];
function select() {
selection.value = key;
}
const isSelected = React.createProxyState(
[selection],
() => selection.value == key
);
return (
<button aria-label={label} on:click={select} toggle:selected={isSelected}>
<span class="icon">{icon}</span>
</button>
);
}
+60
View File
@@ -0,0 +1,60 @@
import * as React from "bloatless-react";
import { Chat, MessageObject } from "../../Model/chatModel";
import { translation } from "../../translations";
export function NoteObjectsView(
chat: Chat,
selectedObject: React.State<MessageObject | undefined>,
isShowingObjectModal: React.State<boolean>
) {
const objectConverter: React.StateItemConverter<MessageObject> = (
messageObject
) => {
const latest = chat.getMostRecentContent(messageObject);
function select() {
selectedObject.value = messageObject;
isShowingObjectModal.value = true;
}
return (
<button class="tile" on:click={select}>
<div>
<b>{messageObject.title}</b>
<span class="secondary ellipsis">
{latest.noteContent!.split("\n")[0]}
</span>
</div>
</button>
);
};
const notes = new React.ListState<MessageObject>();
chat.objects.subscribe(() => {
notes.clear();
chat.objects.value.forEach((messageObject) => {
const latest = chat.getMostRecentContent(messageObject);
if (!latest.noteContent) return;
notes.add(messageObject);
});
});
const content = React.createProxyState([chat.objects], () =>
notes.value.size == 0 ? (
<div class="flex-column width-100 height-100 align-center justify-center secondary">
{translation.noNotes}
</div>
) : (
<div
class="grid gap padding"
style="grid-template-columns: repeat(auto-fill, minmax(350px, 1fr))"
children:prepend={[notes, objectConverter]}
></div>
)
);
return <div class="width-100 height-100" children:set={content}></div>;
}
+147
View File
@@ -0,0 +1,147 @@
import * as React from "bloatless-react";
import { Chat, MessageObject } from "../../Model/chatModel";
import { translation } from "../../translations";
export function ObjectDetailModal(
chat: Chat,
messageObject: MessageObject,
isPresented: React.State<boolean>
) {
// state
const editingTitle = new React.State(messageObject.title);
const selectedMessageObjectId = new React.State(
chat.getMostRecentContentId(messageObject)
);
const selectedMessageObject = React.createProxyState(
[selectedMessageObjectId],
() =>
chat.getObjectContentFromId(messageObject, selectedMessageObjectId.value)
);
const didEditContent = new React.State(false);
const editingNoteContent = React.createProxyState(
[selectedMessageObject],
() => selectedMessageObject.value.noteContent ?? ""
);
React.bulkSubscribe(
[editingNoteContent],
() => (didEditContent.value = true)
);
// methods
function handleKeyDown(e: KeyboardEvent) {
if (!e.metaKey && !e.ctrlKey) return;
switch (e.key) {
case "s":
e.preventDefault();
saveAndClose();
break;
}
}
function closeModal() {
isPresented.value = false;
}
function saveAndClose() {
messageObject.title = editingTitle.value;
if (didEditContent.value == true) {
chat.addObjectContent(messageObject, {
isoDateVersionCreated: new Date().toISOString(),
id: React.UUID(),
noteContent: editingNoteContent.value,
});
}
chat.addObjectAndSend(messageObject);
closeModal();
}
function deleteAndClose() {
chat.deleteObject(messageObject);
closeModal();
}
return (
<div class="modal" toggle:open={isPresented} on:keydown={handleKeyDown}>
<div>
<main>
<h2>{messageObject.title}</h2>
<span class="secondary">{messageObject.id}</span>
<hr></hr>
<div class="flex-column gap">
<label class="tile">
<span class="icon">label</span>
<div>
<span>{translation.objectTitle}</span>
<input
autofocus
bind:value={editingTitle}
placeholder={translation.objectTitlePlaceholder}
></input>
</div>
</label>
<hr></hr>
<label class="tile">
<span class="icon">history</span>
<div>
<span>{translation.objectVersion}</span>
<select bind:value={selectedMessageObjectId}>
{...chat
.getSortedContents(messageObject)
.map((content) => (
<option value={content.id}>
{new Date(
content.isoDateVersionCreated
).toLocaleString()}
</option>
))}
</select>
<span class="icon">arrow_drop_down</span>
</div>
</label>
<label class="tile">
<span class="icon">sticky_note_2</span>
<div>
<span>{translation.noteContent}</span>
<textarea
rows="5"
bind:value={editingNoteContent}
placeholder={translation.noteContentPlaceholder}
></textarea>
</div>
</label>
<hr></hr>
<button class="danger width-input" on:click={deleteAndClose}>
{translation.deleteObject}
<span class="icon">delete</span>
</button>
</div>
</main>
<div class="flex-row">
<button class="flex-1 width-100 danger" on:click={closeModal}>
{translation.discard}
</button>
<button class="flex-1 width-100 primary" on:click={saveAndClose}>
{translation.save}
<span class="icon">save</span>
</button>
</div>
</div>
</div>
);
}
@@ -7,12 +7,12 @@ import {
newChatName, newChatName,
selectChat, selectChat,
selectedChat, selectedChat,
} from "../Model/model"; } from "../../Model/model";
import { Chat } from "../Model/chatModel"; import { Chat } from "../../Model/chatModel";
import { translation } from "../translations"; import { translation } from "../../translations";
const chatConverter: React.ListItemConverter<Chat> = (chat) => { const chatConverter: React.StateItemConverter<Chat> = (chat) => {
function select() { function select() {
selectChat(chat); selectChat(chat);
} }
@@ -13,11 +13,11 @@ import {
requestMailbox, requestMailbox,
resetAddressInput, resetAddressInput,
serverAddressInput, serverAddressInput,
} from "../Model/model"; } from "../../Model/model";
import { translation } from "../translations"; import { translation } from "../../translations";
const addressConverter: React.ListItemConverter<string> = (address) => { const addressConverter: React.StateItemConverter<string> = (address) => {
return <option>{address}</option>; return <option>{address}</option>;
}; };
@@ -1,7 +1,7 @@
import * as React from "bloatless-react"; import * as React from "bloatless-react";
import { senderName } from "../Model/model"; import { senderName } from "../../Model/model";
import { translation } from "../translations"; import { translation } from "../../translations";
export function PersonalSection() { export function PersonalSection() {
return ( return (
+86
View File
@@ -1,3 +1,89 @@
[unread] { [unread] {
border-left: 10px solid var(--text-warn); border-left: 10px solid var(--text-warn);
} }
/* TOOLS */
:root {
--thread-view-height-with-tools: 8rem;
--thread-view-width-with-tools: 20rem;
--footer-top-line: calc(var(--control-height) + var(--safe-area-bottom));
}
#message-tab {
flex-direction: column;
}
.message-tab-content-wrapper {
display: flex;
flex-direction: column;
}
.chat-object-view {
display: none;
}
[showingchattools] :is(.thread-view, .chat-object-view) {
overflow: scroll;
position: absolute;
padding: 0px;
}
[showingchattools] header {
border-bottom: 1px solid var(--border);
}
[showingchattools] .chat-object-view {
display: flex;
top: var(--header-height);
bottom: calc(var(--thread-view-height-with-tools) + var(--footer-top-line));
border-bottom: 1px solid var(--border);
}
[showingchattools] .thread-view {
display: flex;
height: var(--thread-view-height-with-tools);
top: unset;
bottom: var(--footer-top-line);
flex: 1;
}
@media (min-width: 640px) and (max-width: 1400px) {
main.split:has([showingchattools]) {
display: flex;
}
main.split:has([showingchattools]) #settings-tab {
display: none;
}
}
@media (min-width: 640px) {
[showingchattools] :is(.thread-view, .chat-object-view) {
top: var(--header-height);
bottom: var(--footer-top-line);
}
[showingchattools] .chat-object-view {
left: 0px;
right: var(--thread-view-width-with-tools);
border-bottom: none;
border-right: 1px solid var(--border);
}
[showingchattools] .thread-view {
display: flex;
left: unset;
right: 0px;
height: unset;
width: var(--thread-view-width-with-tools);
}
}
+93 -18
View File
@@ -1,9 +1,11 @@
const englishTranslations = { const englishTranslations = {
// general // general
set: "Set", set: "Set",
save: "Save",
back: "Back", back: "Back",
undoChanges: "Undo changes", undoChanges: "Undo changes",
close: "Close", close: "Close",
discard: "Discard",
// overview // overview
overview: "Overview", overview: "Overview",
@@ -22,7 +24,7 @@ const englishTranslations = {
serverAddressPlaceholder: "wss://192.168.0.69:3000", serverAddressPlaceholder: "wss://192.168.0.69:3000",
connectToServer: "Connect", connectToServer: "Connect",
disconnect: "Disonnect", disconnect: "Disconnect",
mailbox: "Mailbox", mailbox: "Mailbox",
requestMailbox: "Enable", requestMailbox: "Enable",
@@ -34,7 +36,7 @@ const englishTranslations = {
primaryChannelPlaceholder: "my-channel", primaryChannelPlaceholder: "my-channel",
addChat: "Add", addChat: "Add",
// messages // chat
showChatOptions: "show chat options", showChatOptions: "show chat options",
configureChatTitle: "Configure Chat", configureChatTitle: "Configure Chat",
@@ -47,8 +49,9 @@ const englishTranslations = {
encryptionKeyPlaceholder: "n10d2482dg283hg", encryptionKeyPlaceholder: "n10d2482dg283hg",
showKey: "Show key", showKey: "Show key",
removeChat: "Remove chat", clearObjects: "Delete all objects",
clearChatMessages: "Clear chat messages", clearChatMessages: "Clear chat messages",
removeChat: "Remove chat",
messageInOutbox: "Pending", messageInOutbox: "Pending",
@@ -57,10 +60,32 @@ const englishTranslations = {
composerPlaceholder: "Type a message...", composerPlaceholder: "Type a message...",
sendMessage: "Send", sendMessage: "Send",
// messages
resendMessage: "Resend message", resendMessage: "Resend message",
decryptMessage: "Decrypt message", decryptMessage: "Decrypt message",
copyMessage: "Copy message", copyMessage: "Copy message",
deleteMessage: "Delete message", deleteMessage: "Delete message",
// objects
showObjects: "show objects",
createObject: "create object",
untitledObject: "Untitled Object",
viewAll: "All",
viewNotes: "Notes",
noObjects: "No objects",
noNotes: "No notes",
objectTitle: "Object title",
objectTitlePlaceholder: "My object",
objectVersion: "Object version",
noteContent: "Note",
noteContentPlaceholder: "Take a note...",
resendObjects: "Resend all objects",
deleteObject: "Delete object",
}; };
const allTranslations: { [key: string]: typeof englishTranslations } = { const allTranslations: { [key: string]: typeof englishTranslations } = {
@@ -68,14 +93,16 @@ const allTranslations: { [key: string]: typeof englishTranslations } = {
es: { es: {
// general // general
set: "Guardar", set: "Guardar",
save: "Guardar",
back: "Atrás", back: "Atrás",
undoChanges: "Deshacer", undoChanges: "Deshacer",
close: "Cerrar", close: "Cerrar",
discard: "Descartar",
// overview // overview
overview: "Resumen", overview: "Resumen",
connection: "Conexion", connection: "Conexión",
chats: "Chats", chats: "Chats",
yourName: "Tu nombre", yourName: "Tu nombre",
@@ -85,7 +112,7 @@ const allTranslations: { [key: string]: typeof englishTranslations } = {
encryptionUnavailableMessage: encryptionUnavailableMessage:
"Obtén esta aplicación a traves de HTTPS o continúa sin cifrado", "Obtén esta aplicación a traves de HTTPS o continúa sin cifrado",
serverAddress: "Direccion del servidor", serverAddress: "Dirección del servidor",
serverAddressPlaceholder: "wss://192.168.0.69:3000", serverAddressPlaceholder: "wss://192.168.0.69:3000",
connectToServer: "Conectar", connectToServer: "Conectar",
@@ -101,40 +128,65 @@ const allTranslations: { [key: string]: typeof englishTranslations } = {
primaryChannelPlaceholder: "mi-canal", primaryChannelPlaceholder: "mi-canal",
addChat: "Añadir", addChat: "Añadir",
// messages // chat
showChatOptions: "Mostrar opciones del chat", showChatOptions: "Mostrar opciones del chat",
configureChatTitle: "Configurar chat", configureChatTitle: "Configurar chat",
secondaryChannel: "Canal segundario", secondaryChannel: "Canal secundario",
secondaryChannelPlaceholder: "Añadir canal segundario", secondaryChannelPlaceholder: "Añadir canal secundario",
addSecondaryChannel: "Añadir canal segundario", addSecondaryChannel: "Añadir canal secundario",
removeSecondaryChannel: "Eliminar canal segundario", removeSecondaryChannel: "Eliminar canal secundario",
encryptionKey: "Clave de cifrado", encryptionKey: "Clave de cifrado",
encryptionKeyPlaceholder: "n10d2482dg283hg", encryptionKeyPlaceholder: "n10d2482dg283hg",
showKey: "Mostrar clave", showKey: "Mostrar clave",
clearObjects: "Eliminar todos los objetos",
clearChatMessages: "Eliminar todos los mensajes",
removeChat: "Eliminar chat", removeChat: "Eliminar chat",
clearChatMessages: "Eliminar todos mensajes",
noChatSelected: "Selecciona un chat",
messageInOutbox: "Pendiente", messageInOutbox: "Pendiente",
noChatSelected: "Selecciona un chat",
composerPlaceholder: "Escribe un mensaje...", composerPlaceholder: "Escribe un mensaje...",
sendMessage: "Enviar", sendMessage: "Enviar",
resendMessage: "Enviar de nuevo", // messages
resendMessage: "Reenviar mensaje",
decryptMessage: "Descifrar mensaje", decryptMessage: "Descifrar mensaje",
copyMessage: "Copiar mensaje", copyMessage: "Copiar mensaje",
deleteMessage: "Eliminar este mensaje", deleteMessage: "Eliminar este mensaje",
// objects
showObjects: "mostrar objetos",
createObject: "añadir nuevo objeto",
untitledObject: "Sin Título",
viewAll: "Todos",
viewNotes: "Notas",
noObjects: "Sin objetos",
noNotes: "Sin notas",
objectTitle: "Título",
objectTitlePlaceholder: "Mi objeto",
objectVersion: "Version del objeto",
noteContent: "Nota",
noteContentPlaceholder: "Toma nota...",
resendObjects: "Reenviar todos objetos",
deleteObject: "Eliminar objeto",
}, },
de: { de: {
// general // general
set: "OK", set: "OK",
save: "Sichern",
back: "Zurück", back: "Zurück",
undoChanges: "Änderungen verwerfen", undoChanges: "Änderungen verwerfen",
close: "Schließen", close: "Schließen",
discard: "Verwerfen",
// overview // overview
overview: "Übersicht", overview: "Übersicht",
@@ -165,7 +217,7 @@ const allTranslations: { [key: string]: typeof englishTranslations } = {
primaryChannelPlaceholder: "mein-kanal", primaryChannelPlaceholder: "mein-kanal",
addChat: "Hinzufügen", addChat: "Hinzufügen",
// messages // chat
showChatOptions: "Chatoptionen einblenden", showChatOptions: "Chatoptionen einblenden",
configureChatTitle: "Chat konfigurieren", configureChatTitle: "Chat konfigurieren",
@@ -178,20 +230,43 @@ const allTranslations: { [key: string]: typeof englishTranslations } = {
encryptionKeyPlaceholder: "n10d2482dg283hg", encryptionKeyPlaceholder: "n10d2482dg283hg",
showKey: "Schlüssel anzeigen", showKey: "Schlüssel anzeigen",
removeChat: "Chat löschen", clearObjects: "Alle Objekte löschen",
clearChatMessages: "Nachrichtenverlauf leeren", clearChatMessages: "Nachrichtenverlauf leeren",
removeChat: "Chat löschen",
noChatSelected: "Kein Chat ausgewählt",
messageInOutbox: "Ausstehend", messageInOutbox: "Ausstehend",
noChatSelected: "Kein Chat ausgewählt",
composerPlaceholder: "Neue Nachricht...", composerPlaceholder: "Neue Nachricht...",
sendMessage: "Senden", sendMessage: "Senden",
// messages
resendMessage: "Erneut senden", resendMessage: "Erneut senden",
decryptMessage: "Nachricht entschlüsseln", decryptMessage: "Nachricht entschlüsseln",
copyMessage: "Nachricht kopieren", copyMessage: "Nachricht kopieren",
deleteMessage: "Nachricht löschen", deleteMessage: "Nachricht löschen",
// objects
showObjects: "Objekte anzeigen",
createObject: "Neues Objekt erstellen",
untitledObject: "Unbenannt",
viewAll: "Alle",
viewNotes: "Notizen",
noObjects: "Keine Objekte",
noNotes: "Keine Notizen",
objectTitle: "Titel",
objectTitlePlaceholder: "Mein Objekt",
objectVersion: "Version des Objekts",
noteContent: "Notiz",
noteContentPlaceholder: "Notiz eingeben...",
resendObjects: "Objekte erneut senden",
deleteObject: "Objekt löschen",
}, },
}; };
+8
View File
@@ -19,10 +19,18 @@ export const storageKeys = {
return id + "messages"; return id + "messages";
}, },
objects(id: string): string {
return id + "items";
},
outbox(id: string): string { outbox(id: string): string {
return id + "outbox"; return id + "outbox";
}, },
itemOutbox(id: string): string {
return id + "item-outbox";
},
composingMessage(id: string): string { composingMessage(id: string): string {
return id + "composing-message"; return id + "composing-message";
}, },