diff --git a/dist/index.js b/dist/index.js index e3882af6..6539eb70 100644 --- a/dist/index.js +++ b/dist/index.js @@ -346,6 +346,9 @@ messages(id) { return id + "messages"; }, + outbox(id) { + return id + "outbox"; + }, composingMessage(id) { return id + "composing-message"; } @@ -360,6 +363,8 @@ secondaryChannels; encryptionKey; messages; + outbox; + isOutBoxEmpty; composingMessage; primaryChannelInput; newSecondaryChannelName; @@ -385,6 +390,11 @@ ); this.encryptionKey = restoreState(storageKeys.encyptionKey(id), ""); this.messages = restoreListState(storageKeys.messages(id)); + this.outbox = restoreListState(storageKeys.outbox(id)); + this.isOutBoxEmpty = createProxyState( + [this.outbox], + () => this.outbox.value.size == 0 + ); this.composingMessage = restoreState( storageKeys.composingMessage(id), "" @@ -392,18 +402,12 @@ this.newSecondaryChannelName = new State(""); this.primaryChannelInput = new State(this.primaryChannel.value); this.cannotSendMessage = createProxyState( - [ - this.primaryChannel, - this.composingMessage, - this.isSubscribed, - isConnected, - senderName - ], - () => this.primaryChannel.value == "" || this.composingMessage.value == "" || senderName.value == "" || this.isSubscribed.value == false || isConnected.value == false + [this.primaryChannel, this.composingMessage, senderName], + () => this.primaryChannel.value == "" || this.composingMessage.value == "" || senderName.value == "" ); this.cannotResendMessage = createProxyState( - [this.primaryChannel, this.isSubscribed, isConnected, senderName], - () => this.primaryChannel.value == "" || senderName.value == "" || this.isSubscribed.value == false || isConnected.value == false + [this.primaryChannel, senderName], + () => this.primaryChannel.value == "" || senderName.value == "" ); this.cannotAddSecondaryChannel = createProxyState( [this.newSecondaryChannelName], @@ -447,6 +451,9 @@ }; handleSubscription = (isSubscribed) => { this.isSubscribed.value = isSubscribed; + if (isSubscribed == true) { + this.sendMessagesInOutbox(); + } }; handleMessage = (chatMessage) => { this.messages.add(chatMessage); @@ -455,14 +462,15 @@ // messages sendNewMessage = async () => { if (this.cannotSendMessage.value == true) return; - this.sendMessageText(this.composingMessage.value); + const message = await this.createMessage(this.composingMessage.value); + this.sendExistingMessage(message); this.composingMessage.value = ""; }; resendMessage = (message) => { if (this.cannotResendMessage.value == true) return; - this.sendMessageText(message.body); + this.sendExistingMessage(message); }; - sendMessageText = async (messageText) => { + createMessage = async (messageText) => { const secondaryChannelNames = [ ...this.secondaryChannels.value.values() ]; @@ -472,14 +480,26 @@ ]; const encrypted = this.encryptionKey.value == "" ? messageText : await encryptString(messageText, this.encryptionKey.value); const joinedChannelName = allChannelNames.join("/"); - const messageObject = { + return { channel: joinedChannelName, sender: senderName.value, body: encrypted, isoDate: (/* @__PURE__ */ new Date()).toISOString() }; - const messageString = JSON.stringify(messageObject); - UDN.sendMessage(joinedChannelName, messageString); + }; + sendExistingMessage = async (chatMessage) => { + if (isConnected.value == true && this.isSubscribed.value == true) { + const messageString = JSON.stringify(chatMessage); + UDN.sendMessage(chatMessage.channel, messageString); + } else { + this.outbox.add(chatMessage); + } + }; + sendMessagesInOutbox = () => { + this.outbox.value.forEach((message) => { + this.sendExistingMessage(message); + this.outbox.remove(message); + }); }; clearMessages = () => { this.messages.clear(); @@ -487,6 +507,9 @@ deleteMessage = (message) => { this.messages.remove(message); }; + deleteOutboxMessage = (message) => { + this.outbox.remove(message); + }; decryptReceivedMessage = async (message) => { message.body = await decryptString(message.body, this.encryptionKey.value); this.messages.callSubscriptions(); @@ -790,6 +813,7 @@ showKey: "Show key", removeChat: "Remove chat", clearChatMessages: "Clear chat messages", + messageInOutbox: "Pending", noChatSelected: "No chat selected", composerPlaceholder: "Type a message...", sendMessage: "Send", @@ -838,6 +862,7 @@ removeChat: "Eliminar chat", clearChatMessages: "Eliminar todos mensajes", noChatSelected: "Selecciona un chat", + messageInOutbox: "Pendiente", composerPlaceholder: "Escribe un mensaje...", sendMessage: "Enviar", resendMessage: "Enviar de nuevo", @@ -883,6 +908,7 @@ removeChat: "Chat l\xF6schen", clearChatMessages: "Nachrichtenverlauf leeren", noChatSelected: "Kein Chat ausgew\xE4hlt", + messageInOutbox: "Ausstehend", composerPlaceholder: "Neue Nachricht...", sendMessage: "Senden", resendMessage: "Erneut senden", @@ -1058,20 +1084,36 @@ /* @__PURE__ */ createElement("span", { class: "icon" }, "key") ), /* @__PURE__ */ createElement("button", { "aria-label": translation.deleteMessage, "on:click": remove }, /* @__PURE__ */ createElement("span", { class: "icon" }, "delete")))), /* @__PURE__ */ createElement("div", { class: "flex-column padding-h padding-bottom" }, /* @__PURE__ */ createElement("b", { class: "break-word", "subscribe:innerText": messageBody }), /* @__PURE__ */ createElement("span", { class: "secondary" }, /* @__PURE__ */ createElement("b", null, new Date(message.isoDate).toLocaleString()), /* @__PURE__ */ createElement("br", null), message.channel)))); }; - const listElement = /* @__PURE__ */ createElement( + const outboxMessageConverter = (message) => { + function remove() { + chat.deleteOutboxMessage(message); + } + return /* @__PURE__ */ createElement("div", { class: "tile width-100 flex-no padding-0" }, /* @__PURE__ */ createElement("div", { class: "flex-column" }, /* @__PURE__ */ createElement("div", { class: "flex-row justify-apart align-center" }, /* @__PURE__ */ createElement("b", { class: "error padding-h" }, translation.messageInOutbox), /* @__PURE__ */ createElement("span", { class: "flex-row secondary" }, /* @__PURE__ */ createElement("button", { "aria-label": translation.deleteMessage, "on:click": remove }, /* @__PURE__ */ createElement("span", { class: "icon" }, "delete")))), /* @__PURE__ */ createElement("div", { class: "flex-column padding-h padding-bottom" }, /* @__PURE__ */ createElement("b", { class: "break-word" }, message.body), /* @__PURE__ */ createElement("span", { class: "secondary" }, /* @__PURE__ */ createElement("b", null, new Date(message.isoDate).toLocaleString()), /* @__PURE__ */ createElement("br", null), message.channel)))); + }; + const messageList = /* @__PURE__ */ createElement( "div", { class: "flex-column gap", "children:append": [chat.messages, messageConverter] } ); - chat.messages.handleAddition(() => { - const scrollFromBottom = listElement.scrollHeight - (listElement.scrollTop + listElement.offsetHeight); + const outboxList = /* @__PURE__ */ createElement( + "div", + { + class: "flex-column gap", + "children:append": [chat.outbox, outboxMessageConverter] + } + ); + const listWrapper = /* @__PURE__ */ createElement("div", { class: "flex-column gap" }, messageList, outboxList); + function scrollToBottom() { + const scrollFromBottom = listWrapper.scrollHeight - (listWrapper.scrollTop + listWrapper.offsetHeight); if (scrollFromBottom > 400) return; - listElement.scrollTop = listElement.scrollHeight; - }); - setTimeout(() => listElement.scrollTop = listElement.scrollHeight, 50); - return listElement; + listWrapper.scrollTop = listWrapper.scrollHeight; + } + chat.messages.handleAddition(scrollToBottom); + chat.outbox.handleAddition(scrollToBottom); + setTimeout(() => listWrapper.scrollTop = listWrapper.scrollHeight, 50); + return listWrapper; } // src/Tabs/messageTab.tsx diff --git a/src/Model/chatModel.ts b/src/Model/chatModel.ts index 9526fe4b..6fc7f49c 100644 --- a/src/Model/chatModel.ts +++ b/src/Model/chatModel.ts @@ -34,6 +34,8 @@ export class Chat { encryptionKey: React.State; messages: React.ListState; + outbox: React.ListState; + isOutBoxEmpty: React.State; composingMessage: React.State; primaryChannelInput: React.State; @@ -66,6 +68,11 @@ export class Chat { ); this.encryptionKey = React.restoreState(storageKeys.encyptionKey(id), ""); this.messages = React.restoreListState(storageKeys.messages(id)); + this.outbox = React.restoreListState(storageKeys.outbox(id)); + this.isOutBoxEmpty = React.createProxyState( + [this.outbox], + () => this.outbox.value.size == 0 + ); // inputs this.composingMessage = React.restoreState( @@ -77,27 +84,15 @@ export class Chat { // guards this.cannotSendMessage = React.createProxyState( - [ - this.primaryChannel, - this.composingMessage, - this.isSubscribed, - isConnected, - senderName, - ], + [this.primaryChannel, this.composingMessage, senderName], () => this.primaryChannel.value == "" || this.composingMessage.value == "" || - senderName.value == "" || - this.isSubscribed.value == false || - isConnected.value == false + senderName.value == "" ); this.cannotResendMessage = React.createProxyState( - [this.primaryChannel, this.isSubscribed, isConnected, senderName], - () => - this.primaryChannel.value == "" || - senderName.value == "" || - this.isSubscribed.value == false || - isConnected.value == false + [this.primaryChannel, senderName], + () => this.primaryChannel.value == "" || senderName.value == "" ); this.cannotAddSecondaryChannel = React.createProxyState( [this.newSecondaryChannelName], @@ -149,6 +144,10 @@ export class Chat { handleSubscription = (isSubscribed: boolean): void => { this.isSubscribed.value = isSubscribed; + + if (isSubscribed == true) { + this.sendMessagesInOutbox(); + } }; handleMessage = (chatMessage: ChatMessage): void => { @@ -160,16 +159,17 @@ export class Chat { sendNewMessage = async (): Promise => { if (this.cannotSendMessage.value == true) return; - this.sendMessageText(this.composingMessage.value); + const message = await this.createMessage(this.composingMessage.value); + this.sendExistingMessage(message); this.composingMessage.value = ""; }; resendMessage = (message: ChatMessage) => { if (this.cannotResendMessage.value == true) return; - this.sendMessageText(message.body); + this.sendExistingMessage(message); }; - sendMessageText = async (messageText: string): Promise => { + createMessage = async (messageText: string): Promise => { // get channels const secondaryChannelNames: string[] = [ ...this.secondaryChannels.value.values(), @@ -187,16 +187,28 @@ export class Chat { // create object const joinedChannelName = allChannelNames.join("/"); - const messageObject: ChatMessage = { + return { channel: joinedChannelName, sender: senderName.value, body: encrypted, isoDate: new Date().toISOString(), }; - const messageString = JSON.stringify(messageObject); + }; - // send - UDN.sendMessage(joinedChannelName, messageString); + sendExistingMessage = async (chatMessage: ChatMessage): Promise => { + if (isConnected.value == true && this.isSubscribed.value == true) { + const messageString = JSON.stringify(chatMessage); + UDN.sendMessage(chatMessage.channel, messageString); + } else { + this.outbox.add(chatMessage); + } + }; + + sendMessagesInOutbox = () => { + this.outbox.value.forEach((message) => { + this.sendExistingMessage(message) + this.outbox.remove(message); + }); }; clearMessages = (): void => { @@ -207,6 +219,10 @@ export class Chat { this.messages.remove(message); }; + deleteOutboxMessage = (message: ChatMessage): void => { + this.outbox.remove(message); + }; + decryptReceivedMessage = async (message: ChatMessage): Promise => { message.body = await decryptString(message.body, this.encryptionKey.value); this.messages.callSubscriptions(); diff --git a/src/Views/threadView.tsx b/src/Views/threadView.tsx index 76413772..770ec83a 100644 --- a/src/Views/threadView.tsx +++ b/src/Views/threadView.tsx @@ -72,20 +72,68 @@ export function ThreadView(chat: Chat) { ); }; - const listElement = ( + const outboxMessageConverter: React.ListItemConverter = ( + message + ) => { + function remove() { + chat.deleteOutboxMessage(message); + } + + return ( +
+
+
+ {translation.messageInOutbox} + + + +
+
+ {message.body} + + {new Date(message.isoDate).toLocaleString()} +

+ {message.channel} +
+
+
+
+ ); + }; + + const messageList = (
); - chat.messages.handleAddition(() => { + const outboxList = ( +
+ ); + const listWrapper = ( +
+ {messageList} + {outboxList} +
+ ); + + function scrollToBottom() { const scrollFromBottom = - listElement.scrollHeight - - (listElement.scrollTop + listElement.offsetHeight); + listWrapper.scrollHeight - + (listWrapper.scrollTop + listWrapper.offsetHeight); if (scrollFromBottom > 400) return; - listElement.scrollTop = listElement.scrollHeight; - }); - setTimeout(() => (listElement.scrollTop = listElement.scrollHeight), 50); - return listElement; + listWrapper.scrollTop = listWrapper.scrollHeight; + } + + chat.messages.handleAddition(scrollToBottom); + chat.outbox.handleAddition(scrollToBottom); + setTimeout(() => (listWrapper.scrollTop = listWrapper.scrollHeight), 50); + + return listWrapper; } diff --git a/src/translations.ts b/src/translations.ts index 4d9eb333..416fea28 100644 --- a/src/translations.ts +++ b/src/translations.ts @@ -50,6 +50,8 @@ const englishTranslations = { removeChat: "Remove chat", clearChatMessages: "Clear chat messages", + messageInOutbox: "Pending", + noChatSelected: "No chat selected", composerPlaceholder: "Type a message...", @@ -117,6 +119,8 @@ const allTranslations: { [key: string]: typeof englishTranslations } = { noChatSelected: "Selecciona un chat", + messageInOutbox: "Pendiente", + composerPlaceholder: "Escribe un mensaje...", sendMessage: "Enviar", @@ -179,6 +183,8 @@ const allTranslations: { [key: string]: typeof englishTranslations } = { noChatSelected: "Kein Chat ausgewählt", + messageInOutbox: "Ausstehend", + composerPlaceholder: "Neue Nachricht...", sendMessage: "Senden", diff --git a/src/utility.ts b/src/utility.ts index c9d00706..a9587c1a 100644 --- a/src/utility.ts +++ b/src/utility.ts @@ -19,6 +19,10 @@ export const storageKeys = { return id + "messages"; }, + outbox(id: string): string { + return id + "outbox"; + }, + composingMessage(id: string): string { return id + "composing-message"; },