diff --git a/services/static-webserver/client/source/class/osparc/conversation/AddMessage.js b/services/static-webserver/client/source/class/osparc/conversation/AddMessage.js index 0917b472996..ddaba210ed9 100644 --- a/services/static-webserver/client/source/class/osparc/conversation/AddMessage.js +++ b/services/static-webserver/client/source/class/osparc/conversation/AddMessage.js @@ -36,8 +36,8 @@ qx.Class.define("osparc.conversation.AddMessage", { }, events: { - "commentAdded": "qx.event.type.Data", - "messageEdited": "qx.event.type.Data", + "messageAdded": "qx.event.type.Data", + "messageUpdated": "qx.event.type.Data", }, members: { @@ -227,7 +227,7 @@ qx.Class.define("osparc.conversation.AddMessage", { if (content) { osparc.study.Conversations.addMessage(this.__studyData["uuid"], this.__conversationId, content) .then(data => { - this.fireDataEvent("commentAdded", data); + this.fireDataEvent("messageAdded", data); commentField.getChildControl("text-area").setValue(""); }); } @@ -239,7 +239,7 @@ qx.Class.define("osparc.conversation.AddMessage", { if (content) { osparc.study.Conversations.editMessage(this.__studyData["uuid"], this.__conversationId, this.__message["messageId"], content) .then(data => { - this.fireDataEvent("messageEdited", data); + this.fireDataEvent("messageUpdated", data); commentField.getChildControl("text-area").setValue(""); }); } @@ -249,7 +249,7 @@ qx.Class.define("osparc.conversation.AddMessage", { if (userGid) { osparc.study.Conversations.notifyUser(this.__studyData["uuid"], this.__conversationId, userGid) .then(data => { - this.fireDataEvent("commentAdded", data); + this.fireDataEvent("messageAdded", data); const potentialCollaborators = osparc.store.Groups.getInstance().getPotentialCollaborators(); if (userGid in potentialCollaborators) { if ("getUserId" in potentialCollaborators[userGid]) { diff --git a/services/static-webserver/client/source/class/osparc/conversation/Conversation.js b/services/static-webserver/client/source/class/osparc/conversation/Conversation.js index ed897b011a2..9bb2cab5b76 100644 --- a/services/static-webserver/client/source/class/osparc/conversation/Conversation.js +++ b/services/static-webserver/client/source/class/osparc/conversation/Conversation.js @@ -27,6 +27,7 @@ qx.Class.define("osparc.conversation.Conversation", { this.base(arguments); this.__studyData = studyData; + this.__messages = []; if (conversationId) { this.setConversationId(conversationId); @@ -46,7 +47,7 @@ qx.Class.define("osparc.conversation.Conversation", { this.__buildLayout(); - this.fetchMessages(); + this.__reloadMessages(); }, properties: { @@ -64,6 +65,7 @@ qx.Class.define("osparc.conversation.Conversation", { members: { __studyData: null, + __messages: null, __nextRequestParams: null, __messagesTitle: null, __messagesList: null, @@ -159,24 +161,44 @@ qx.Class.define("osparc.conversation.Conversation", { }); this.__loadMoreMessages = new osparc.ui.form.FetchButton(this.tr("Load more messages...")); - this.__loadMoreMessages.addListener("execute", () => this.fetchMessages(false)); + this.__loadMoreMessages.addListener("execute", () => this.__reloadMessages(false)); this._add(this.__loadMoreMessages); if (osparc.data.model.Study.canIWrite(this.__studyData["accessRights"])) { const addMessages = new osparc.conversation.AddMessage(this.__studyData, this.getConversationId()); addMessages.setPaddingLeft(10); - addMessages.addListener("commentAdded", e => { + addMessages.addListener("messageAdded", e => { const data = e.getData(); if (data["conversationId"]) { this.setConversationId(data["conversationId"]); + this.addMessage(data); } - this.fetchMessages(); }); this._add(addMessages); } }, - fetchMessages: function(removeMessages = true) { + __getNextRequest: function() { + const params = { + url: { + studyId: this.__studyData["uuid"], + conversationId: this.getConversationId(), + offset: 0, + limit: 42 + } + }; + const nextRequestParams = this.__nextRequestParams; + if (nextRequestParams) { + params.url.offset = nextRequestParams.offset; + params.url.limit = nextRequestParams.limit; + } + const options = { + resolveWResponse: true + }; + return osparc.data.Resources.fetch("conversations", "getMessagesPage", params, options); + }, + + __reloadMessages: function(removeMessages = true) { if (this.getConversationId() === null) { this.__messagesTitle.setValue(this.tr("No messages yet")); this.__messagesList.hide(); @@ -189,15 +211,14 @@ qx.Class.define("osparc.conversation.Conversation", { this.__loadMoreMessages.setFetching(true); if (removeMessages) { + this.__messages = []; this.__messagesList.removeAll(); } this.__getNextRequest() .then(resp => { const messages = resp["data"]; - // it's not provided by the backend - messages.forEach(message => message["studyId"] = this.__studyData["uuid"]); - this.__addMessages(messages); + messages.forEach(message => this.addMessage(message)); this.__nextRequestParams = resp["_links"]["next"]; if (this.__nextRequestParams === null) { this.__loadMoreMessages.exclude(); @@ -206,48 +227,91 @@ qx.Class.define("osparc.conversation.Conversation", { .finally(() => this.__loadMoreMessages.setFetching(false)); }, - __getNextRequest: function() { - const params = { - url: { - studyId: this.__studyData["uuid"], - conversationId: this.getConversationId(), - offset: 0, - limit: 42 - } - }; - const nextRequestParams = this.__nextRequestParams; - if (nextRequestParams) { - params.url.offset = nextRequestParams.offset; - params.url.limit = nextRequestParams.limit; - } - const options = { - resolveWResponse: true - }; - return osparc.data.Resources.fetch("conversations", "getMessagesPage", params, options); - }, - - __addMessages: function(messages) { - const nMessages = messages.filter(msg => msg["type"] === "MESSAGE").length; + __updateMessagesNumber: function() { + const nMessages = this.__messages.filter(msg => msg["type"] === "MESSAGE").length; if (nMessages === 1) { this.__messagesTitle.setValue(this.tr("1 Message")); } else if (nMessages > 1) { this.__messagesTitle.setValue(nMessages + this.tr(" Messages")); } + }, - messages.forEach(message => { - let control = null; - switch (message["type"]) { - case "MESSAGE": - control = new osparc.conversation.MessageUI(message, this.__studyData); - control.addListener("messageEdited", () => this.fetchMessages()); - control.addListener("messageDeleted", () => this.fetchMessages()); - break; - case "NOTIFICATION": - control = new osparc.conversation.NotificationUI(message); - break; - } - if (control) { - this.__messagesList.add(control); + addMessage: function(message) { + // backend doesn't provide the projectId + message["projectId"] = this.__studyData["uuid"]; + + // ignore it if it was already there + const messageIndex = this.__messages.findIndex(msg => msg["messageId"] === message["messageId"]); + if (messageIndex !== -1) { + return; + } + + // determine insertion index for most‐recent‐first order + const newTime = new Date(message["created"]); + let insertAt = this.__messages.findIndex(m => new Date(m["created"]) < newTime); + if (insertAt === -1) { + insertAt = this.__messages.length; + } + + // Insert the message in the messages array + this.__messages.splice(insertAt, 0, message); + + // Add the UI element to the messages list + let control = null; + switch (message["type"]) { + case "MESSAGE": + control = new osparc.conversation.MessageUI(message, this.__studyData); + control.addListener("messageUpdated", e => this.updateMessage(e.getData())); + control.addListener("messageDeleted", e => this.deleteMessage(e.getData())); + break; + case "NOTIFICATION": + control = new osparc.conversation.NotificationUI(message); + break; + } + if (control) { + // insert into the UI at the same position + this.__messagesList.addAt(control, insertAt); + } + + this.__updateMessagesNumber(); + }, + + deleteMessage: function(message) { + // remove it from the messages array + const messageIndex = this.__messages.findIndex(msg => msg["messageId"] === message["messageId"]); + if (messageIndex === -1) { + return; + } + this.__messages.splice(messageIndex, 1); + + // Remove the UI element from the messages list + const children = this.__messagesList.getChildren(); + const controlIndex = children.findIndex( + ctrl => ("getMessage" in ctrl && ctrl.getMessage()["messageId"] === message["messageId"]) + ); + if (controlIndex > -1) { + this.__messagesList.remove(children[controlIndex]); + } + + this.__updateMessagesNumber(); + }, + + updateMessage: function(message) { + // backend doesn't provide the projectId + message["projectId"] = this.__studyData["uuid"]; + + // Replace the message in the messages array + const messageIndex = this.__messages.findIndex(msg => msg["messageId"] === message["messageId"]); + if (messageIndex === -1) { + return; + } + this.__messages[messageIndex] = message; + + // Update the UI element from the messages list + this.__messagesList.getChildren().forEach(control => { + if ("getMessage" in control && control.getMessage()["messageId"] === message["messageId"]) { + control.setMessage(message); + return; } }); }, diff --git a/services/static-webserver/client/source/class/osparc/conversation/MessageUI.js b/services/static-webserver/client/source/class/osparc/conversation/MessageUI.js index d5bc94db512..fdddc051d6b 100644 --- a/services/static-webserver/client/source/class/osparc/conversation/MessageUI.js +++ b/services/static-webserver/client/source/class/osparc/conversation/MessageUI.js @@ -26,17 +26,16 @@ qx.Class.define("osparc.conversation.MessageUI", { construct: function(message, studyData = null) { this.base(arguments); - this.__message = message; this.__studyData = studyData; - const isMyMessage = this.self().isMyMessage(this.__message); const layout = new qx.ui.layout.Grid(12, 4); layout.setColumnFlex(1, 1); // content - layout.setColumnFlex(isMyMessage ? 0 : 2, 3); // spacer this._setLayout(layout); this.setPadding(5); - this.__buildLayout(); + this.set({ + message, + }); }, statics: { @@ -46,15 +45,22 @@ qx.Class.define("osparc.conversation.MessageUI", { }, events: { - "messageEdited": "qx.event.type.Event", - "messageDeleted": "qx.event.type.Event", + "messageUpdated": "qx.event.type.Data", + "messageDeleted": "qx.event.type.Data", }, - members: { - __message: null, + properties: { + message: { + check: "Object", + init: null, + nullable: false, + apply: "__applyMessage", + }, + }, + members: { _createChildControlImpl: function(id) { - const isMyMessage = this.self().isMyMessage(this.__message); + const isMyMessage = this.self().isMyMessage(this.getMessage()); let control; switch (id) { case "thumbnail": @@ -140,20 +146,29 @@ qx.Class.define("osparc.conversation.MessageUI", { return control || this.base(arguments, id); }, - __buildLayout: function() { + __applyMessage: function(message) { + const isMyMessage = this.self().isMyMessage(message); + this._getLayout().setColumnFlex(isMyMessage ? 0 : 2, 3); // spacer + const thumbnail = this.getChildControl("thumbnail"); const userName = this.getChildControl("user-name"); - const date = new Date(this.__message["modified"]); - const date2 = osparc.utils.Utils.formatDateAndTime(date); + const createdDateData = new Date(message["created"]); + const createdDate = osparc.utils.Utils.formatDateAndTime(createdDateData); const lastUpdate = this.getChildControl("last-updated"); - lastUpdate.setValue(date2); + if (message["created"] === message["modified"]) { + lastUpdate.setValue(createdDate); + } else { + const updatedDateData = new Date(message["modified"]); + const updatedDate = osparc.utils.Utils.formatDateAndTime(updatedDateData); + lastUpdate.setValue(createdDate + " (" + this.tr("edited") + " "+ updatedDate + ")"); + } const messageContent = this.getChildControl("message-content"); - messageContent.setValue(this.__message["content"]); + messageContent.setValue(message["content"]); - osparc.store.Users.getInstance().getUser(this.__message["userGroupId"]) + osparc.store.Users.getInstance().getUser(message["userGroupId"]) .then(user => { if (user) { thumbnail.setSource(user.getThumbnail()); @@ -170,7 +185,7 @@ qx.Class.define("osparc.conversation.MessageUI", { this.getChildControl("spacer"); - if (this.self().isMyMessage(this.__message)) { + if (this.self().isMyMessage(message)) { const menuButton = this.getChildControl("menu-button"); const menu = new qx.ui.menu.Menu().set({ @@ -189,20 +204,24 @@ qx.Class.define("osparc.conversation.MessageUI", { }, __editMessage: function() { - const addMessage = new osparc.conversation.AddMessage(this.__studyData, this.__message["conversationId"], this.__message); + const message = this.getMessage(); + + const addMessage = new osparc.conversation.AddMessage(this.__studyData, message["conversationId"], message); const title = this.tr("Edit message"); const win = osparc.ui.window.Window.popUpInWindow(addMessage, title, 570, 135).set({ clickAwayClose: false, resizable: true, showClose: true, }); - addMessage.addListener("messageEdited", () => { + addMessage.addListener("messageUpdated", e => { win.close(); - this.fireDataEvent("messageEdited"); + this.fireDataEvent("messageUpdated", e.getData()); }); }, __deleteMessage: function() { + const message = this.getMessage(); + const win = new osparc.ui.window.Confirmation(this.tr("Delete message?")).set({ caption: this.tr("Delete"), confirmText: this.tr("Delete"), @@ -211,8 +230,8 @@ qx.Class.define("osparc.conversation.MessageUI", { win.open(); win.addListener("close", () => { if (win.getConfirmed()) { - osparc.study.Conversations.deleteMessage(this.__message["studyId"], this.__message["conversationId"], this.__message["messageId"]) - .then(() => this.fireEvent("messageDeleted")) + osparc.study.Conversations.deleteMessage(message) + .then(() => this.fireDataEvent("messageDeleted", message)) .catch(err => osparc.FlashMessenger.logError(err)); } }); diff --git a/services/static-webserver/client/source/class/osparc/conversation/NotificationUI.js b/services/static-webserver/client/source/class/osparc/conversation/NotificationUI.js index 670e3437e3d..34247eea09e 100644 --- a/services/static-webserver/client/source/class/osparc/conversation/NotificationUI.js +++ b/services/static-webserver/client/source/class/osparc/conversation/NotificationUI.js @@ -25,25 +25,32 @@ qx.Class.define("osparc.conversation.NotificationUI", { construct: function(message) { this.base(arguments); - this.__message = message; - - const isMyMessage = osparc.conversation.MessageUI.isMyMessage(this.__message); + const isMyMessage = osparc.conversation.MessageUI.isMyMessage(message); const layout = new qx.ui.layout.Grid(4, 4); layout.setColumnFlex(isMyMessage ? 0 : 3, 3); // spacer layout.setRowAlign(0, "center", "middle"); this._setLayout(layout); this.setPadding(5); - this.__buildLayout(); + this.set({ + message, + }); }, - members: { - __message: null, + properties: { + message: { + check: "Object", + init: null, + nullable: false, + apply: "__applyMessage", + }, + }, + members: { // spacer - date - content - (thumbnail-spacer) // (thumbnail-spacer) - content - date - spacer _createChildControlImpl: function(id) { - const isMyMessage = osparc.conversation.MessageUI.isMyMessage(this.__message); + const isMyMessage = osparc.conversation.MessageUI.isMyMessage(this.getMessage()); let control; switch (id) { case "thumbnail-spacer": @@ -87,19 +94,21 @@ qx.Class.define("osparc.conversation.NotificationUI", { return control || this.base(arguments, id); }, - __buildLayout: function() { + __applyMessage: function(message) { + this._removeAll(); + this.getChildControl("thumbnail-spacer"); - const isMyMessage = osparc.conversation.MessageUI.isMyMessage(this.__message); + const isMyMessage = osparc.conversation.MessageUI.isMyMessage(message); - const modifiedDate = new Date(this.__message["modified"]); + const modifiedDate = new Date(message["modified"]); const date = osparc.utils.Utils.formatDateAndTime(modifiedDate); const lastUpdate = this.getChildControl("last-updated"); lastUpdate.setValue(isMyMessage ? date + " -" : " - " + date); const messageContent = this.getChildControl("message-content"); - const notifierUserGroupId = parseInt(this.__message["userGroupId"]); - const notifiedUserGroupId = parseInt(this.__message["content"]); + const notifierUserGroupId = parseInt(message["userGroupId"]); + const notifiedUserGroupId = parseInt(message["content"]); let msgContent = "🔔 "; Promise.all([ osparc.store.Users.getInstance().getUser(notifierUserGroupId), diff --git a/services/static-webserver/client/source/class/osparc/study/Conversations.js b/services/static-webserver/client/source/class/osparc/study/Conversations.js index 0dc5a8b40c1..7d8a5800796 100644 --- a/services/static-webserver/client/source/class/osparc/study/Conversations.js +++ b/services/static-webserver/client/source/class/osparc/study/Conversations.js @@ -27,7 +27,11 @@ qx.Class.define("osparc.study.Conversations", { this._setLayout(new qx.ui.layout.VBox()); + this.__conversations = []; + this.fetchConversations(studyData); + + this.__listenToConversationWS(); }, statics: { @@ -109,12 +113,12 @@ qx.Class.define("osparc.study.Conversations", { .catch(err => osparc.FlashMessenger.logError(err)); }, - deleteMessage: function(studyId, conversationId, messageId) { + deleteMessage: function(message) { const params = { url: { - studyId, - conversationId, - messageId, + studyId: message["projectId"], + conversationId: message["conversationId"], + messageId: message["messageId"], }, }; return osparc.data.Resources.fetch("conversations", "deleteMessage", params) @@ -138,6 +142,9 @@ qx.Class.define("osparc.study.Conversations", { }, members: { + __conversations: null, + __wsHandlers: null, + _createChildControlImpl: function(id) { let control; switch (id) { @@ -156,6 +163,43 @@ qx.Class.define("osparc.study.Conversations", { return control || this.base(arguments, id); }, + __listenToConversationWS: function() { + this.__wsHandlers = []; + + const socket = osparc.wrapper.WebSocket.getInstance(); + [ + "conversation:message:created", + "conversation:message:updated", + "conversation:message:deleted", + ].forEach(eventName => { + const eventHandler = message => { + if (message) { + const conversationId = message["conversationId"]; + const conversation = this.__getConversation(conversationId); + if (conversation) { + switch (eventName) { + case "conversation:message:created": + conversation.addMessage(message); + break; + case "conversation:message:updated": + conversation.updateMessage(message); + break; + case "conversation:message:deleted": + conversation.deleteMessage(message); + break; + } + } + } + }; + socket.on(eventName, eventHandler, this); + this.__wsHandlers.push({ eventName, handler: eventHandler }); + }); + }, + + __getConversation: function(conversationId) { + return this.__conversations.find(conversation => conversation.getConversationId() === conversationId); + }, + fetchConversations: function(studyData) { const loadMoreButton = this.getChildControl("loading-button"); loadMoreButton.setFetching(true); @@ -192,6 +236,7 @@ qx.Class.define("osparc.study.Conversations", { this.fetchConversations(studyData); }; + this.__conversations = []; if (conversations.length === 0) { const noConversationTab = new osparc.conversation.Conversation(studyData); conversationPages.push(noConversationTab); @@ -199,13 +244,14 @@ qx.Class.define("osparc.study.Conversations", { noConversationTab.addListener("conversationDeleted", () => reloadConversations()); conversationsLayout.add(noConversationTab); } else { - conversations.forEach(conversation => { - const conversationId = conversation["conversationId"]; - const conversationTab = new osparc.conversation.Conversation(studyData, conversationId); - conversationPages.push(conversationTab); - conversationTab.setLabel(conversation["name"]); - conversationTab.addListener("conversationDeleted", () => reloadConversations()); - conversationsLayout.add(conversationTab); + conversations.forEach(conversationData => { + const conversationId = conversationData["conversationId"]; + const conversation = new osparc.conversation.Conversation(studyData, conversationId); + this.__conversations.push(conversation); + conversationPages.push(conversation); + conversation.setLabel(conversationData["name"]); + conversation.addListener("conversationDeleted", () => reloadConversations()); + conversationsLayout.add(conversation); }); } @@ -218,5 +264,15 @@ qx.Class.define("osparc.study.Conversations", { conversationsLayout.getChildControl("bar").add(newConversationButton); }, - } + }, + + destruct: function() { + const socket = osparc.wrapper.WebSocket.getInstance(); + if (this.__wsHandlers) { + this.__wsHandlers.forEach(({ eventName }) => { + socket.removeSlot(eventName); + }); + this.__wsHandlers = null; + } + }, });