From cb243bca62f6b3397f81f7bf3e229c387830ba20 Mon Sep 17 00:00:00 2001 From: marlon-erler Date: Sun, 21 Jul 2024 14:15:26 +0200 Subject: [PATCH] fix outbox, add indicator for message status --- dist/index.css | 15 ++++++- dist/index.js | 65 ++++++++++++++++++++++++----- src/Model/chatMessageViewModel.ts | 6 ++- src/Model/chatModel.ts | 28 ++++++++++--- src/Model/connectionModel.ts | 18 ++++++-- src/View/ChatPages/messagePage.css | 20 ++++++++- src/View/Components/chatMessage.tsx | 18 ++++++++ src/ViewModel/chatViewModel.ts | 18 +++++--- 8 files changed, 160 insertions(+), 28 deletions(-) diff --git a/dist/index.css b/dist/index.css index 83ef00ce..f4f5e32a 100644 --- a/dist/index.css +++ b/dist/index.css @@ -451,10 +451,19 @@ body[showing-chat] #chat-page-wrapper { padding: var(--control-padding); } .message-bubble .main .timestamp, +.message-bubble .main .timestamp .icon, .message-bubble .main .sender-name { font-size: 0.8rem; opacity: 0.6; } +.message-bubble .main .timestamp { + display: flex; + align-items: center; + gap: var(--gap-between-tiles); +} +.message-bubble .main .timestamp .icon { + display: none; +} .message-bubble .main .sender-name { font-weight: bold; } @@ -467,9 +476,13 @@ body[showing-chat] #chat-page-wrapper { .message-bubble[sentbyuser] { align-items: flex-end; } -.message-bubble[sentbyuser] .main { +.message-bubble[sentbyuser] .main, +.message-bubble[sentbyuser] .main .timestamp { flex-direction: row-reverse; } +.message-bubble[sentbyuser] .main .timestamp .icon { + display: flex; +} .message-bubble[sentbyuser] .main span { text-align: right; } diff --git a/dist/index.js b/dist/index.js index be0c42bb..8e2434b2 100644 --- a/dist/index.js +++ b/dist/index.js @@ -563,7 +563,8 @@ storeColor = () => { this.storageModel.store(this.colorPath, this.color); }; - addMessage = (chatMessage) => { + addMessage = async (chatMessage) => { + await this.decryptMessage(chatMessage); const messagePath = this.getMessagePath(chatMessage.id); this.storageModel.storeStringifiable(messagePath, chatMessage); this.chatMessageHandler(chatMessage); @@ -641,13 +642,19 @@ senderName, body ); + this.addMessage(chatMessage); this.connectionModel.sendMessageOrStore(chatMessage); return true; }; - handleMessage = async (body) => { + handleMessage = (body) => { const chatMessage = _ChatModel.parseMessage(body); if (chatMessage == null) return; - await this.decryptMessage(chatMessage); + chatMessage.status = "received" /* Received */; + this.addMessage(chatMessage); + }; + handleMessageSent = (chatMessage) => { + chatMessage.status = "sent" /* Sent */; + this.addMessage(chatMessage); }; decryptMessage = async (chatMessage) => { const decryptedBody = await decryptString( @@ -655,7 +662,6 @@ this.info.encryptionKey ); chatMessage.body = decryptedBody; - this.addMessage(chatMessage); }; setMessageHandler = (handler) => { this.chatMessageHandler = handler; @@ -673,6 +679,7 @@ this.restoreInfo(); this.restoreColor(); this.subscribe(); + connectionModel2.setMessageSentHandler(this.handleMessageSent); } // utility static generateChatInfo = (primaryChannel) => { @@ -690,7 +697,8 @@ channel, sender, body, - dateSent: createTimestamp() + dateSent: createTimestamp(), + status: "outbox" /* Outbox */ }; }; static parseMessage = (string) => { @@ -795,6 +803,7 @@ sender; dateSent; body = new State(""); + status = new State(void 0); sentByUser; // methods copyMessage = () => { @@ -812,6 +821,7 @@ this.sender = this.chatMessage.sender; this.dateSent = new Date(this.chatMessage.dateSent).toLocaleString(); this.body.value = this.chatMessage.body; + this.status.value = this.chatMessage.status; }; // init constructor(chatViewModel, chatMessage, sentByUser) { @@ -864,6 +874,7 @@ // view open = () => { this.chatListViewModel.openChat(this); + this.restoreMessages(); }; close = () => { this.chatListViewModel.closeChat(); @@ -878,6 +889,7 @@ const existingChatMessageViewModel = this.chatMessageViewModels.value.get(chatMessage.id); if (existingChatMessageViewModel != void 0) { existingChatMessageViewModel.body.value = chatMessage.body; + existingChatMessageViewModel.status.value = chatMessage.status; } else { this.chatMessageViewModels.set(chatMessage.id, chatMessageModel); } @@ -918,6 +930,7 @@ }; // messaging sendMessage = () => { + if (this.cannotSendMessage.value == true) return; this.sendMessageFromBody(this.composingMessage.value); this.composingMessage.value = ""; }; @@ -925,7 +938,9 @@ this.chatModel.sendMessage(body); }; decryptMessage = async (messageViewModel) => { - await this.chatModel.decryptMessage(messageViewModel.chatMessage); + const chatMessage = messageViewModel.chatMessage; + await this.chatModel.decryptMessage(chatMessage); + this.chatModel.addMessage(chatMessage); messageViewModel.loadData(); }; // restore @@ -947,6 +962,11 @@ this.secondaryChannels.add(secondaryChannel); } }; + restoreMessages = () => { + for (const chatMessage of this.chatModel.messages) { + this.addChatMessage(chatMessage); + } + }; // init constructor(chatModel, storageModel2, settingsViewModel2, chatListViewModel2) { this.chatModel = chatModel; @@ -967,9 +987,6 @@ this.restoreSecondaryChannels(); this.restorePageSelection(); this.updateIndex(); - for (const chatMessage of chatModel.messages) { - this.addChatMessage(chatMessage); - } this.cannotSendMessage = createProxyState( [this.settingsViewModel.username, this.composingMessage], () => this.settingsViewModel.username.value == "" || this.composingMessage.value == "" @@ -1189,6 +1206,21 @@ function openModal() { isInfoModalOpen.value = true; } + const statusIcon = createProxyState( + [chatMessageViewModel.status], + () => { + switch (chatMessageViewModel.status.value) { + case "outbox" /* Outbox */: + return "hourglass_top"; + case "sent" /* Sent */: + return "check"; + case "received" /* Received */: + return "done_all"; + default: + return "warning"; + } + } + ); return /* @__PURE__ */ createElement( "div", { @@ -1201,7 +1233,7 @@ class: "body", "subscribe:innerText": chatMessageViewModel.body } - ), /* @__PURE__ */ createElement("span", { class: "timestamp ellipsis" }, chatMessageViewModel.dateSent)), /* @__PURE__ */ createElement("div", { class: "button-container" }, /* @__PURE__ */ createElement( + ), /* @__PURE__ */ createElement("span", { class: "timestamp ellipsis" }, /* @__PURE__ */ createElement("span", { class: "icon", "subscribe:innerText": statusIcon }), chatMessageViewModel.dateSent)), /* @__PURE__ */ createElement("div", { class: "button-container" }, /* @__PURE__ */ createElement( "button", { "on:click": openModal, @@ -1528,6 +1560,7 @@ // UTILITY METHODS send(messageObject) { if (this.ws == void 0) return false; + if (this.ws.readyState != 1) return false; const messageString = JSON.stringify(messageObject); this.ws.send(messageString); return true; @@ -1617,6 +1650,8 @@ }; messageHandler = () => { }; + messageSentHandler = () => { + }; channelsToSubscribe = /* @__PURE__ */ new Set(); // connection connect = (address) => { @@ -1691,7 +1726,12 @@ }; tryToSendMessage = (chatMessage) => { const stringifiedBody = stringify(chatMessage); - return this.udn.sendMessage(chatMessage.channel, stringifiedBody); + const isSent = this.udn.sendMessage( + chatMessage.channel, + stringifiedBody + ); + if (isSent) this.messageSentHandler(chatMessage); + return isSent; }; // storage getAddressPath = (address) => { @@ -1720,6 +1760,9 @@ setMessageHandler = (handler) => { this.messageHandler = handler; }; + setMessageSentHandler = (handler) => { + this.messageSentHandler = handler; + }; // setup constructor(storageModel2) { this.udn = new UDNFrontend(); diff --git a/src/Model/chatMessageViewModel.ts b/src/Model/chatMessageViewModel.ts index 90057af2..417a90b8 100644 --- a/src/Model/chatMessageViewModel.ts +++ b/src/Model/chatMessageViewModel.ts @@ -2,7 +2,7 @@ import * as React from "bloatless-react"; -import { ChatMessage, ChatModel } from "./chatModel"; +import { ChatMessage, ChatMessageStatus, ChatModel } from "./chatModel"; import ChatViewModel from "../ViewModel/chatViewModel"; @@ -15,6 +15,9 @@ export default class ChatMessageViewModel { sender: string; dateSent: string; body: React.State = new React.State(""); + status: React.State = new React.State< + ChatMessageStatus | undefined + >(undefined); sentByUser: boolean; // methods @@ -34,6 +37,7 @@ export default class ChatMessageViewModel { this.sender = this.chatMessage.sender; this.dateSent = new Date(this.chatMessage.dateSent).toLocaleString(); this.body.value = this.chatMessage.body; + this.status.value = this.chatMessage.status; }; // init diff --git a/src/Model/chatModel.ts b/src/Model/chatModel.ts index 2a162ca0..5e2880d6 100644 --- a/src/Model/chatModel.ts +++ b/src/Model/chatModel.ts @@ -7,7 +7,6 @@ import { decryptString, encryptString } from "./Utility/crypto"; import ChatListModel from "./chatListModel"; import { Color } from "../ViewModel/colors"; import ConnectionModel from "./connectionModel"; -import { Message } from "udn-frontend"; import SettingsModel from "./settingsModel"; import { checkIsValidObject } from "./Utility/typeSafety"; import { v4 } from "uuid"; @@ -89,7 +88,8 @@ export class ChatModel { this.storageModel.store(this.colorPath, this.color); }; - addMessage = (chatMessage: ChatMessage): void => { + addMessage = async (chatMessage: ChatMessage): Promise => { + await this.decryptMessage(chatMessage); const messagePath: string[] = this.getMessagePath(chatMessage.id); this.storageModel.storeStringifiable(messagePath, chatMessage); this.chatMessageHandler(chatMessage); @@ -187,15 +187,22 @@ export class ChatModel { body ); + this.addMessage(chatMessage); this.connectionModel.sendMessageOrStore(chatMessage); return true; }; - handleMessage = async (body: string): Promise => { + handleMessage = (body: string): void => { const chatMessage: ChatMessage | null = ChatModel.parseMessage(body); if (chatMessage == null) return; - await this.decryptMessage(chatMessage); + chatMessage.status = ChatMessageStatus.Received; + this.addMessage(chatMessage); + }; + + handleMessageSent = (chatMessage: ChatMessage): void => { + chatMessage.status = ChatMessageStatus.Sent; + this.addMessage(chatMessage); }; decryptMessage = async (chatMessage: ChatMessage): Promise => { @@ -204,7 +211,6 @@ export class ChatModel { this.info.encryptionKey ); chatMessage.body = decryptedBody; - this.addMessage(chatMessage); }; setMessageHandler = (handler: (chatMessage: ChatMessage) => void): void => { @@ -232,6 +238,8 @@ export class ChatModel { this.restoreInfo(); this.restoreColor(); this.subscribe(); + + connectionModel.setMessageSentHandler(this.handleMessageSent); } // utility @@ -258,6 +266,8 @@ export class ChatModel { sender, body, dateSent: createTimestamp(), + + status: ChatMessageStatus.Outbox, }; }; @@ -281,6 +291,12 @@ export interface ChatInfo { hasUnreadMessages: boolean; } +export enum ChatMessageStatus { + Outbox = "outbox", + Sent = "sent", + Received = "received", +} + export interface ChatMessage { dataVersion: "v2"; @@ -291,6 +307,8 @@ export interface ChatMessage { body: string; dateSent: string; + status?: ChatMessageStatus; + stringifiedObject?: string; } diff --git a/src/Model/connectionModel.ts b/src/Model/connectionModel.ts index 1bef9178..b97461c1 100644 --- a/src/Model/connectionModel.ts +++ b/src/Model/connectionModel.ts @@ -22,6 +22,7 @@ export default class ConnectionModel { connectionChangeHandler: () => void = () => {}; messageHandler: (data: Message) => void = () => {}; + messageSentHandler: (chatMessage: ChatMessage) => void = () => {}; channelsToSubscribe: Set = new Set(); @@ -62,7 +63,7 @@ export default class ConnectionModel { sendSubscriptionRequest = (): void => { if (this.isConnected == false) return; - + for (const channel of this.channelsToSubscribe) { this.udn.subscribe(channel); } @@ -102,7 +103,7 @@ export default class ConnectionModel { const messages: ChatMessage[] = this.getOutboxMessags(); for (const message of messages) { - const isSent = this.tryToSendMessage(message); + const isSent: boolean = this.tryToSendMessage(message); if (isSent == false) return; this.removeFromOutbox(message); @@ -119,7 +120,12 @@ export default class ConnectionModel { tryToSendMessage = (chatMessage: ChatMessage): boolean => { const stringifiedBody: string = stringify(chatMessage); - return this.udn.sendMessage(chatMessage.channel, stringifiedBody); + const isSent: boolean = this.udn.sendMessage( + chatMessage.channel, + stringifiedBody + ); + if (isSent) this.messageSentHandler(chatMessage); + return isSent; }; // storage @@ -158,6 +164,12 @@ export default class ConnectionModel { this.messageHandler = handler; }; + setMessageSentHandler = ( + handler: (chatMessage: ChatMessage) => void + ): void => { + this.messageSentHandler = handler; + }; + // setup constructor(storageModel: StorageModel) { // create frontend diff --git a/src/View/ChatPages/messagePage.css b/src/View/ChatPages/messagePage.css index 5545132c..9a779d41 100644 --- a/src/View/ChatPages/messagePage.css +++ b/src/View/ChatPages/messagePage.css @@ -118,11 +118,22 @@ } .message-bubble .main .timestamp, +.message-bubble .main .timestamp .icon, .message-bubble .main .sender-name { font-size: 0.8rem; opacity: 0.6; } +.message-bubble .main .timestamp { + display: flex; + align-items: center; + gap: var(--gap-between-tiles); +} + +.message-bubble .main .timestamp .icon { + display: none; +} + .message-bubble .main .sender-name { font-weight: bold; } @@ -139,10 +150,15 @@ align-items: flex-end; } -.message-bubble[sentbyuser] .main { +.message-bubble[sentbyuser] .main, +.message-bubble[sentbyuser] .main .timestamp { flex-direction: row-reverse; } +.message-bubble[sentbyuser] .main .timestamp .icon { + display: flex; +} + .message-bubble[sentbyuser] .main span { text-align: right; -} +} \ No newline at end of file diff --git a/src/View/Components/chatMessage.tsx b/src/View/Components/chatMessage.tsx index 9d954b77..d9bf64b7 100644 --- a/src/View/Components/chatMessage.tsx +++ b/src/View/Components/chatMessage.tsx @@ -1,6 +1,7 @@ import * as React from "bloatless-react"; import { ChatMessageInfoModal } from "./chatMessageInfoModal"; +import { ChatMessageStatus } from "../../Model/chatModel"; import ChatMessageViewModel from "../../Model/chatMessageViewModel"; import { translations } from "../translations"; @@ -10,6 +11,22 @@ export function ChatMessage(chatMessageViewModel: ChatMessageViewModel) { isInfoModalOpen.value = true; } + const statusIcon = React.createProxyState( + [chatMessageViewModel.status], + () => { + switch (chatMessageViewModel.status.value) { + case ChatMessageStatus.Outbox: + return "hourglass_top"; + case ChatMessageStatus.Sent: + return "check"; + case ChatMessageStatus.Received: + return "done_all"; + default: + return "warning"; + } + } + ); + return (
+ {chatMessageViewModel.dateSent}
diff --git a/src/ViewModel/chatViewModel.ts b/src/ViewModel/chatViewModel.ts index 3d4940f1..87587f3d 100644 --- a/src/ViewModel/chatViewModel.ts +++ b/src/ViewModel/chatViewModel.ts @@ -63,6 +63,7 @@ export default class ChatViewModel { // view open = (): void => { this.chatListViewModel.openChat(this); + this.restoreMessages(); }; close = (): void => { @@ -81,6 +82,7 @@ export default class ChatViewModel { this.chatMessageViewModels.value.get(chatMessage.id); if (existingChatMessageViewModel != undefined) { existingChatMessageViewModel.body.value = chatMessage.body; + existingChatMessageViewModel.status.value = chatMessage.status; } else { this.chatMessageViewModels.set(chatMessage.id, chatMessageModel); } @@ -131,6 +133,8 @@ export default class ChatViewModel { // messaging sendMessage = (): void => { + if (this.cannotSendMessage.value == true) return; + this.sendMessageFromBody(this.composingMessage.value); this.composingMessage.value = ""; }; @@ -142,7 +146,9 @@ export default class ChatViewModel { decryptMessage = async ( messageViewModel: ChatMessageViewModel ): Promise => { - await this.chatModel.decryptMessage(messageViewModel.chatMessage); + const chatMessage: ChatMessage = messageViewModel.chatMessage; + await this.chatModel.decryptMessage(chatMessage); + this.chatModel.addMessage(chatMessage); messageViewModel.loadData(); }; @@ -168,6 +174,12 @@ export default class ChatViewModel { } }; + restoreMessages = (): void => { + for (const chatMessage of this.chatModel.messages) { + this.addChatMessage(chatMessage); + } + } + // init constructor( chatModel: ChatModel, @@ -202,10 +214,6 @@ export default class ChatViewModel { this.restorePageSelection(); this.updateIndex(); - for (const chatMessage of chatModel.messages) { - this.addChatMessage(chatMessage); - } - // guards this.cannotSendMessage = React.createProxyState( [this.settingsViewModel.username, this.composingMessage],