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 a3b6e7a2b6cd..89fc5f3db4fe 100644 --- a/services/static-webserver/client/source/class/osparc/conversation/AddMessage.js +++ b/services/static-webserver/client/source/class/osparc/conversation/AddMessage.js @@ -69,7 +69,7 @@ qx.Class.define("osparc.conversation.AddMessage", { }); break; } - case "thumbnail": { + case "avatar": { control = osparc.utils.Utils.createThumbnail(32); const authStore = osparc.auth.Data.getInstance(); control.set({ @@ -95,6 +95,7 @@ qx.Class.define("osparc.conversation.AddMessage", { break; case "add-comment-button": control = new qx.ui.form.Button(null, "@FontAwesome5Solid/arrow-up/16").set({ + toolTipText: this.tr("Ctrl+Enter"), backgroundColor: "input_background", allowGrowX: false, alignX: "right", @@ -135,7 +136,7 @@ qx.Class.define("osparc.conversation.AddMessage", { }, __buildLayout: function() { - this.getChildControl("thumbnail"); + this.getChildControl("avatar"); this.getChildControl("comment-field"); this.getChildControl("add-comment-button"); }, diff --git a/services/static-webserver/client/source/class/osparc/conversation/Conversation.js b/services/static-webserver/client/source/class/osparc/conversation/Conversation.js new file mode 100644 index 000000000000..e36342ce1953 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/conversation/Conversation.js @@ -0,0 +1,244 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2025 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + + +qx.Class.define("osparc.conversation.Conversation", { + extend: qx.ui.core.Widget, + + /** + * @param conversation {osparc.data.model.Conversation} Conversation + */ + construct: function(conversation) { + this.base(arguments); + + this._messages = []; + + this._setLayout(new qx.ui.layout.VBox(5)); + + this._buildLayout(); + + if (conversation) { + this.setConversation(conversation); + } + }, + + properties: { + conversation: { + check: "osparc.data.model.Conversation", + init: null, + nullable: true, + event: "changeConversation", + apply: "_applyConversation", + }, + }, + + events: { + "messagesChanged": "qx.event.type.Event", + }, + + members: { + _messages: null, + + _createChildControlImpl: function(id) { + let control; + switch (id) { + case "spacer-top": + control = new qx.ui.core.Spacer(); + this._addAt(control, 0, { + flex: 100 // high number to keep even a one message list at the bottom + }); + break; + case "messages-container-scroll": + control = new qx.ui.container.Scroll(); + this._addAt(control, 1, { + flex: 1 + }); + break; + case "messages-container": + control = new qx.ui.container.Composite(new qx.ui.layout.VBox(5)).set({ + alignY: "middle" + }); + this.getChildControl("messages-container-scroll").add(control); + break; + case "load-more-button": + control = new osparc.ui.form.FetchButton(this.tr("Load more messages...")); + control.addListener("execute", () => this.__reloadMessages(false)); + this._addAt(control, 2); + break; + case "add-message": + control = new osparc.conversation.AddMessage().set({ + padding: 5, + }); + this.bind("conversation", control, "conversationId", { + converter: conversation => conversation ? conversation.getConversationId() : null + }); + this._addAt(control, 3); + break; + } + return control || this.base(arguments, id); + }, + + _buildLayout: function() { + this.getChildControl("spacer-top"); + this.getChildControl("messages-container"); + this.getChildControl("add-message"); + }, + + _applyConversation: function(conversation) { + this.__reloadMessages(true); + + if (conversation) { + conversation.addListener("messageAdded", e => { + const data = e.getData(); + this.addMessage(data); + }); + conversation.addListener("messageUpdated", e => { + const data = e.getData(); + this.updateMessage(data); + }); + conversation.addListener("messageDeleted", e => { + const data = e.getData(); + this.deleteMessage(data); + }); + } + }, + + __reloadMessages: function(removeMessages = true) { + if (removeMessages) { + this.clearAllMessages(); + } + + const loadMoreMessages = this.getChildControl("load-more-button"); + if (this.getConversation() === null) { + loadMoreMessages.hide(); + return; + } + + loadMoreMessages.show(); + loadMoreMessages.setFetching(true); + this.getConversation().getNextMessages() + .then(resp => { + const messages = resp["data"]; + messages.forEach(message => this.addMessage(message)); + if (resp["_links"]["next"] === null && loadMoreMessages) { + loadMoreMessages.exclude(); + } + }) + .finally(() => loadMoreMessages.setFetching(false)); + }, + + _createMessageUI: function(message) { + return new osparc.conversation.MessageUI(message); + }, + + getMessages: function() { + return this._messages; + }, + + clearAllMessages: function() { + this._messages = []; + this.getChildControl("messages-container").removeAll(); + + this.fireEvent("messagesChanged"); + }, + + addMessage: function(message) { + // 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 latest‐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 = this._createMessageUI(message); + 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 + const messagesContainer = this.getChildControl("messages-container"); + messagesContainer.addAt(control, insertAt); + } + + // scroll to bottom + // add timeout to ensure the scroll happens after the UI is updated + setTimeout(() => { + const messagesScroll = this.getChildControl("messages-container-scroll"); + messagesScroll.scrollToY(messagesScroll.getChildControl("pane").getScrollMaxY()); + }, 50); + + this.fireEvent("messagesChanged"); + }, + + 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 messagesContainer = this.getChildControl("messages-container"); + const children = messagesContainer.getChildren(); + const controlIndex = children.findIndex( + ctrl => ("getMessage" in ctrl && ctrl.getMessage()["messageId"] === message["messageId"]) + ); + if (controlIndex > -1) { + messagesContainer.remove(children[controlIndex]); + } + + this.fireEvent("messagesChanged"); + }, + + updateMessage: function(message) { + // 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 + const messagesContainer = this.getChildControl("messages-container"); + const messageUI = messagesContainer.getChildren().find(control => { + return "getMessage" in control && control.getMessage()["messageId"] === message["messageId"]; + }); + if (messageUI) { + // Force a new reference + messageUI.setMessage(Object.assign({}, message)); + } + }, + } +}); 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 be5c1129e1d3..d7ded5e9d36c 100644 --- a/services/static-webserver/client/source/class/osparc/conversation/MessageUI.js +++ b/services/static-webserver/client/source/class/osparc/conversation/MessageUI.js @@ -64,7 +64,7 @@ qx.Class.define("osparc.conversation.MessageUI", { const isMyMessage = this.self().isMyMessage(this.getMessage()); let control; switch (id) { - case "thumbnail": + case "avatar": control = new osparc.ui.basic.UserThumbnail(32).set({ marginTop: 4, alignY: "top", @@ -149,18 +149,18 @@ qx.Class.define("osparc.conversation.MessageUI", { const messageContent = this.getChildControl("message-content"); messageContent.setValue(message["content"]); - const thumbnail = this.getChildControl("thumbnail"); + const avatar = this.getChildControl("avatar"); const userName = this.getChildControl("user-name"); if (message["userGroupId"] === "system") { userName.setValue("Support"); } else { osparc.store.Users.getInstance().getUser(message["userGroupId"]) .then(user => { - thumbnail.setUser(user); + avatar.setUser(user); userName.setValue(user ? user.getLabel() : "Unknown user"); }) .catch(() => { - thumbnail.setSource(osparc.utils.Avatar.emailToThumbnail()); + avatar.setSource(osparc.utils.Avatar.emailToThumbnail()); userName.setValue("Unknown user"); }); } diff --git a/services/static-webserver/client/source/class/osparc/data/model/Conversation.js b/services/static-webserver/client/source/class/osparc/data/model/Conversation.js index 19d0372cc943..4c871a42e58c 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/Conversation.js +++ b/services/static-webserver/client/source/class/osparc/data/model/Conversation.js @@ -24,8 +24,9 @@ qx.Class.define("osparc.data.model.Conversation", { /** * @param conversationData {Object} Object containing the serialized Conversation Data + * @param studyId {String} ID of the Study * */ - construct: function(conversationData) { + construct: function(conversationData, studyId) { this.base(arguments); this.set({ @@ -37,6 +38,7 @@ qx.Class.define("osparc.data.model.Conversation", { modified: new Date(conversationData.modified), projectId: conversationData.projectUuid || null, extraContext: conversationData.extraContext || null, + studyId: studyId || null, }); this.__messages = []; @@ -134,6 +136,12 @@ qx.Class.define("osparc.data.model.Conversation", { event: "changeLastMessage", apply: "__applyLastMessage", }, + + studyId: { + check: "String", + nullable: true, + init: null, + }, }, events: { @@ -221,6 +229,10 @@ qx.Class.define("osparc.data.model.Conversation", { limit: 42 } }; + if (this.getStudyId()) { + params.url.studyId = this.getStudyId(); + } + const nextRequestParams = this.__nextRequestParams; if (nextRequestParams) { params.url.offset = nextRequestParams.offset; @@ -229,7 +241,10 @@ qx.Class.define("osparc.data.model.Conversation", { const options = { resolveWResponse: true }; - return osparc.data.Resources.fetch("conversationsSupport", "getMessagesPage", params, options) + const promise = this.getStudyId() ? + osparc.data.Resources.fetch("conversationsStudies", "getMessagesPage", params, options) : + osparc.data.Resources.fetch("conversationsSupport", "getMessagesPage", params, options); + return promise .then(resp => { const messages = resp["data"]; messages.forEach(message => this.addMessage(message)); diff --git a/services/static-webserver/client/source/class/osparc/desktop/StudyEditor.js b/services/static-webserver/client/source/class/osparc/desktop/StudyEditor.js index 24818effe361..773af71ce609 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/StudyEditor.js +++ b/services/static-webserver/client/source/class/osparc/desktop/StudyEditor.js @@ -313,7 +313,7 @@ qx.Class.define("osparc.desktop.StudyEditor", { this.__listenToNodeUpdated(); this.__listenToNodeProgress(); this.__listenToNoMoreCreditsEvents(); - this.__listenToEvent(); + this.__listenToServiceCustomEvents(); this.__listenToServiceStatus(); this.__listenToStatePorts(); @@ -537,7 +537,7 @@ qx.Class.define("osparc.desktop.StudyEditor", { } }, - __listenToEvent: function() { + __listenToServiceCustomEvents: function() { const socket = osparc.wrapper.WebSocket.getInstance(); // callback for events diff --git a/services/static-webserver/client/source/class/osparc/desktop/organizations/MembersList.js b/services/static-webserver/client/source/class/osparc/desktop/organizations/MembersList.js index c636ab36df21..4fb63a41a05c 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/organizations/MembersList.js +++ b/services/static-webserver/client/source/class/osparc/desktop/organizations/MembersList.js @@ -158,10 +158,7 @@ qx.Class.define("osparc.desktop.organizations.MembersList", { }, configureItem: item => { item.subscribeToFilterGroup("organizationMembersList"); - item.getChildControl("thumbnail").getContentElement() - .setStyles({ - "border-radius": "16px" - }); + item.getChildControl("thumbnail").setDecorator("circled"); item.addListener("promoteToMember", e => { const listedMember = e.getData(); this.__promoteToUser(listedMember); diff --git a/services/static-webserver/client/source/class/osparc/desktop/organizations/OrganizationsList.js b/services/static-webserver/client/source/class/osparc/desktop/organizations/OrganizationsList.js index 2a3c89f00684..82abb9e17120 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/organizations/OrganizationsList.js +++ b/services/static-webserver/client/source/class/osparc/desktop/organizations/OrganizationsList.js @@ -154,11 +154,7 @@ qx.Class.define("osparc.desktop.organizations.OrganizationsList", { configureItem: item => { item.subscribeToFilterGroup("organizationsList"); osparc.utils.Utils.setIdToWidget(item, "organizationListItem"); - const thumbnail = item.getChildControl("thumbnail"); - thumbnail.getContentElement() - .setStyles({ - "border-radius": "16px" - }); + item.getChildControl("thumbnail").setDecorator("circled"); item.addListener("openEditOrganization", e => { const orgKey = e.getData(); diff --git a/services/static-webserver/client/source/class/osparc/desktop/wallets/MembersList.js b/services/static-webserver/client/source/class/osparc/desktop/wallets/MembersList.js index a38b322b5a48..3cf57a6ce6aa 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/wallets/MembersList.js +++ b/services/static-webserver/client/source/class/osparc/desktop/wallets/MembersList.js @@ -146,10 +146,7 @@ qx.Class.define("osparc.desktop.wallets.MembersList", { }, configureItem: item => { item.subscribeToFilterGroup("walletMembersList"); - item.getChildControl("thumbnail").getContentElement() - .setStyles({ - "border-radius": "16px" - }); + item.getChildControl("thumbnail").setDecorator("circled"); item.addListener("promoteToAccountant", e => { const listedMember = e.getData(); this.__promoteToAccountant(listedMember); diff --git a/services/static-webserver/client/source/class/osparc/navigation/UserMenuButton.js b/services/static-webserver/client/source/class/osparc/navigation/UserMenuButton.js index 355b1c502d66..e0647ed985b8 100644 --- a/services/static-webserver/client/source/class/osparc/navigation/UserMenuButton.js +++ b/services/static-webserver/client/source/class/osparc/navigation/UserMenuButton.js @@ -39,9 +39,7 @@ qx.Class.define("osparc.navigation.UserMenuButton", { this.getContentElement().setStyles({ "border-radius": "20px" }); - this.getChildControl("icon").getContentElement().setStyles({ - "border-radius": "16px" - }); + this.getChildControl("icon").setDecorator("circled"); osparc.utils.Utils.setIdToWidget(this, "userMenuBtn"); const store = osparc.store.Store.getInstance(); diff --git a/services/static-webserver/client/source/class/osparc/share/Collaborators.js b/services/static-webserver/client/source/class/osparc/share/Collaborators.js index 3ade7f6e71a0..5ab5c84a5a13 100644 --- a/services/static-webserver/client/source/class/osparc/share/Collaborators.js +++ b/services/static-webserver/client/source/class/osparc/share/Collaborators.js @@ -356,10 +356,7 @@ qx.Class.define("osparc.share.Collaborators", { }, item, id); }, configureItem: item => { - item.getChildControl("thumbnail").getContentElement() - .setStyles({ - "border-radius": "16px" - }); + item.getChildControl("thumbnail").setDecorator("circled"); item.addListener("promoteToEditor", e => { const orgMember = e.getData(); this._promoteToEditor(orgMember, item); diff --git a/services/static-webserver/client/source/class/osparc/store/ConversationsSupport.js b/services/static-webserver/client/source/class/osparc/store/ConversationsSupport.js index 4aa7cd1361b4..53a2150bbbb2 100644 --- a/services/static-webserver/client/source/class/osparc/store/ConversationsSupport.js +++ b/services/static-webserver/client/source/class/osparc/store/ConversationsSupport.js @@ -158,7 +158,7 @@ qx.Class.define("osparc.store.ConversationsSupport", { .then(messagesData => { if (messagesData && messagesData.length) { const lastMessage = messagesData[0]; - this.__addMessageToCache(conversationId, lastMessage); + this.__addMessageToConversation(conversationId, lastMessage); return lastMessage; } return null; @@ -208,7 +208,7 @@ qx.Class.define("osparc.store.ConversationsSupport", { this.__conversationsCached[conversation.getConversationId()] = conversation; }, - __addMessageToCache: function(conversationId, messageData) { + __addMessageToConversation: function(conversationId, messageData) { if (conversationId in this.__conversationsCached) { this.__conversationsCached[conversationId].addMessage(messageData); } diff --git a/services/static-webserver/client/source/class/osparc/study/Conversation.js b/services/static-webserver/client/source/class/osparc/study/Conversation.js index a7f3c37eaf69..1535202eca84 100644 --- a/services/static-webserver/client/source/class/osparc/study/Conversation.js +++ b/services/static-webserver/client/source/class/osparc/study/Conversation.js @@ -17,185 +17,26 @@ qx.Class.define("osparc.study.Conversation", { - extend: qx.ui.tabview.Page, + extend: osparc.conversation.Conversation, /** * @param studyData {String} Study Data - * @param conversationData {Object} Conversation Data + * @param conversation {osparc.data.model.Conversation} Conversation */ - construct: function(studyData, conversationData) { - this.base(arguments); - + construct: function(studyData, conversation) { this.__studyData = studyData; - this.__messages = []; - - if (conversationData) { - const conversation = new osparc.data.model.Conversation(conversationData); - this.setConversation(conversation); - } - - this._setLayout(new qx.ui.layout.VBox(5)); - - this.set({ - padding: 10, - showCloseButton: false, - }); - - this.getChildControl("button").set({ - font: "text-13", - }); - this.__addConversationButtons(); - - this.__buildLayout(); - - this.__reloadMessages(); - }, - - properties: { - conversation: { - check: "osparc.data.model.Conversation", - init: null, - nullable: false, - event: "changeConversation", - apply: "__applyConversation", - }, + this.base(arguments, conversation); }, members: { __studyData: null, - __messages: null, - __nextRequestParams: null, - __messagesTitle: null, - __messageScroll: null, - __messagesList: null, - __loadMoreMessages: null, - - __applyConversation: function(conversation) { - conversation.addListener("messageAdded", e => { - const message = e.getData(); - this.addMessage(message); - }, this); - conversation.addListener("messageUpdated", e => { - const message = e.getData(); - this.updateMessage(message); - }, this); - conversation.addListener("messageDeleted", e => { - const message = e.getData(); - this.deleteMessage(message); - }, this); - }, - - getConversationId: function() { - if (this.getConversation()) { - return this.getConversation().getConversationId(); - } - return null; - }, - __addConversationButtons: function() { - const tabButton = this.getChildControl("button"); + _buildLayout: function() { + this.base(arguments); - const buttonsAesthetics = { - focusable: false, - keepActive: true, - padding: 0, - backgroundColor: "transparent", - }; - const renameButton = new qx.ui.form.Button(null, "@FontAwesome5Solid/pencil-alt/10").set({ - ...buttonsAesthetics, - visibility: osparc.data.model.Study.canIWrite(this.__studyData["accessRights"]) ? "visible" : "excluded", - }); - renameButton.addListener("execute", () => { - const titleEditor = new osparc.widget.Renamer(tabButton.getLabel()); - titleEditor.addListener("labelChanged", e => { - titleEditor.close(); - const newLabel = e.getData()["newLabel"]; - if (this.getConversationId()) { - osparc.store.ConversationsProject.getInstance().renameConversation(this.__studyData["uuid"], this.getConversationId(), newLabel) - .then(() => this.renameConversation(newLabel)); - } else { - // create new conversation first - osparc.store.ConversationsProject.getInstance().postConversation(this.__studyData["uuid"], newLabel) - .then(data => { - const conversation = new osparc.data.model.Conversation(data); - this.setConversation(conversation); - this.getChildControl("button").setLabel(newLabel); - }); - } - }, this); - titleEditor.center(); - titleEditor.open(); - }); - // eslint-disable-next-line no-underscore-dangle - tabButton._add(renameButton, { - row: 0, - column: 3 - }); - - const closeButton = new qx.ui.form.Button(null, "@FontAwesome5Solid/times/12").set({ - ...buttonsAesthetics, - paddingLeft: 4, // adds spacing between buttons - visibility: osparc.data.model.Study.canIWrite(this.__studyData["accessRights"]) ? "visible" : "excluded", - }); - closeButton.addListener("execute", () => { - if (this.__messagesList.getChildren().length === 0) { - osparc.store.ConversationsProject.getInstance().deleteConversation(this.__studyData["uuid"], this.getConversationId()); - } else { - const msg = this.tr("Are you sure you want to delete the conversation?"); - const confirmationWin = new osparc.ui.window.Confirmation(msg).set({ - caption: this.tr("Delete Conversation"), - confirmText: this.tr("Delete"), - confirmAction: "delete" - }); - confirmationWin.open(); - confirmationWin.addListener("close", () => { - if (confirmationWin.getConfirmed()) { - osparc.store.ConversationsProject.getInstance().deleteConversation(this.__studyData["uuid"], this.getConversationId()); - } - }, this); - } - }); - // eslint-disable-next-line no-underscore-dangle - tabButton._add(closeButton, { - row: 0, - column: 4 - }); - this.bind("conversation", closeButton, "visibility", { - converter: value => value ? "visible" : "excluded" - }); - }, - - renameConversation: function(newName) { - this.getChildControl("button").setLabel(newName); - }, - - __buildLayout: function() { - this.__messagesTitle = new qx.ui.basic.Label(); - this._add(this.__messagesTitle); - - // add spacer to keep the messages list at the bottom - this._add(new qx.ui.core.Spacer(), { - flex: 100 // high number to keep even a one message list at the bottom - }); - - this.__messagesList = new qx.ui.container.Composite(new qx.ui.layout.VBox(5)).set({ - alignY: "middle" - }); - const scrollView = this.__messageScroll = new qx.ui.container.Scroll(); - scrollView.add(this.__messagesList); - this._add(scrollView, { - flex: 1 - }); - - this.__loadMoreMessages = new osparc.ui.form.FetchButton(this.tr("Load more messages...")); - this.__loadMoreMessages.addListener("execute", () => this.__reloadMessages(false)); - this._add(this.__loadMoreMessages); - - const addMessage = new osparc.conversation.AddMessage().set({ + const addMessage = this.getChildControl("add-message").set({ studyData: this.__studyData, - conversationId: this.getConversationId(), enabled: osparc.data.model.Study.canIWrite(this.__studyData["accessRights"]), - paddingLeft: 10, }); addMessage.addListener("addMessage", e => { const content = e.getData(); @@ -206,7 +47,7 @@ qx.Class.define("osparc.study.Conversation", { // create new conversation first osparc.store.ConversationsProject.getInstance().postConversation(this.__studyData["uuid"]) .then(data => { - const newConversation = new osparc.data.model.Conversation(data); + const newConversation = new osparc.data.model.Conversation(data, this.__studyData["uuid"]); this.setConversation(newConversation); this.__postMessage(content); }); @@ -221,13 +62,20 @@ qx.Class.define("osparc.study.Conversation", { // create new conversation first osparc.store.ConversationsProject.getInstance().postConversation(this.__studyData["uuid"]) .then(data => { - const newConversation = new osparc.data.model.Conversation(data); + const newConversation = new osparc.data.model.Conversation(data, this.__studyData["uuid"]); this.setConversation(newConversation); this.__postNotify(userGid); }); } }); - this._add(addMessage); + }, + + _createMessageUI: function(message) { + const messageUI = new osparc.conversation.MessageUI(message, this.__studyData); + messageUI.getChildControl("message-content").set({ + measurerMaxWidth: 400, + }); + return messageUI; }, __postMessage: function(content) { @@ -251,159 +99,5 @@ qx.Class.define("osparc.study.Conversation", { } }); }, - - __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("conversationsStudies", "getMessagesPage", params, options) - .catch(err => osparc.FlashMessenger.logError(err)); - }, - - __reloadMessages: function(removeMessages = true) { - if (this.getConversationId() === null) { - // temporary conversation page - this.__messagesTitle.setValue(this.tr("No Messages yet")); - this.__messagesList.hide(); - this.__loadMoreMessages.hide(); - return; - } - - this.__messagesList.show(); - this.__loadMoreMessages.show(); - this.__loadMoreMessages.setFetching(true); - - if (removeMessages) { - this.__messages = []; - this.__messagesList.removeAll(); - } - - this.__getNextRequest() - .then(resp => { - const messages = resp["data"]; - messages.forEach(message => this.addMessage(message)); - this.__nextRequestParams = resp["_links"]["next"]; - if (this.__nextRequestParams === null && this.__loadMoreMessages) { - this.__loadMoreMessages.exclude(); - } - }) - .finally(() => this.__loadMoreMessages.setFetching(false)); - }, - - __updateMessagesNumber: function() { - if (!this.__messagesTitle) { - return; - } - const nMessages = this.__messages.filter(msg => msg["type"] === "MESSAGE").length; - if (nMessages === 0) { - this.__messagesTitle.setValue(this.tr("No Messages yet")); - } else if (nMessages === 1) { - this.__messagesTitle.setValue(this.tr("1 Message")); - } else if (nMessages > 1) { - this.__messagesTitle.setValue(nMessages + this.tr(" Messages")); - } - }, - - 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 latest‐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.getChildControl("message-content").set({ - measurerMaxWidth: 400, - }); - 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); - } - - // scroll to bottom - // add timeout to ensure the scroll happens after the UI is updated - setTimeout(() => { - this.__messageScroll.scrollToY(this.__messageScroll.getChildControl("pane").getScrollMaxY()); - }, 50); - - 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/study/ConversationPage.js b/services/static-webserver/client/source/class/osparc/study/ConversationPage.js new file mode 100644 index 000000000000..e4c0338314b3 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/study/ConversationPage.js @@ -0,0 +1,191 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2023 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + + +qx.Class.define("osparc.study.ConversationPage", { + extend: qx.ui.tabview.Page, + + /** + * @param studyData {String} Study Data + * @param conversationData {Object} Conversation Data + */ + construct: function(studyData, conversationData) { + this.base(arguments); + + this.__studyData = studyData; + this.__messages = []; + + this._setLayout(new qx.ui.layout.VBox(5)); + + this.set({ + padding: 10, + showCloseButton: false, + }); + + + this.bind("conversation", this.getChildControl("button"), "label", { + converter: conversation => conversation ? conversation.getName() : this.tr("new") + }); + this.getChildControl("button").set({ + font: "text-13", + }); + this.__addConversationButtons(); + + this.__buildLayout(); + + if (conversationData) { + const conversation = new osparc.data.model.Conversation(conversationData, this.__studyData["uuid"]); + this.setConversation(conversation); + } + + }, + + properties: { + conversation: { + check: "osparc.data.model.Conversation", + init: null, + nullable: true, + event: "changeConversation", + }, + }, + + members: { + __studyData: null, + + _createChildControlImpl: function(id) { + let control; + switch (id) { + case "n-messages": + control = new qx.ui.basic.Label(); + this._add(control); + break; + case "conversation": + control = new osparc.study.Conversation(this.__studyData); + this.bind("conversation", control, "conversation"); + control.addListener("messagesChanged", () => this.__updateMessagesNumber()); + this._add(control, { + flex: 1, + }); + break; + } + return control || this.base(arguments, id); + }, + + __buildLayout: function() { + this.getChildControl("n-messages"); + this.getChildControl("conversation"); + }, + + getConversationId: function() { + if (this.getConversation()) { + return this.getConversation().getConversationId(); + } + return null; + }, + + __addConversationButtons: function() { + const tabButton = this.getChildControl("button"); + + const buttonsAesthetics = { + focusable: false, + keepActive: true, + padding: 0, + backgroundColor: "transparent", + }; + const renameButton = new qx.ui.form.Button(null, "@FontAwesome5Solid/pencil-alt/10").set({ + ...buttonsAesthetics, + visibility: osparc.data.model.Study.canIWrite(this.__studyData["accessRights"]) ? "visible" : "excluded", + }); + renameButton.addListener("execute", () => { + const titleEditor = new osparc.widget.Renamer(tabButton.getLabel()); + titleEditor.addListener("labelChanged", e => { + titleEditor.close(); + const newLabel = e.getData()["newLabel"]; + if (this.getConversationId()) { + osparc.store.ConversationsProject.getInstance().renameConversation(this.__studyData["uuid"], this.getConversationId(), newLabel) + .then(() => this.renameConversation(newLabel)); + } else { + // create new conversation first + osparc.store.ConversationsProject.getInstance().postConversation(this.__studyData["uuid"], newLabel) + .then(data => { + const conversation = new osparc.data.model.Conversation(data, this.__studyData["uuid"]); + this.setConversation(conversation); + this.getChildControl("button").setLabel(newLabel); + }); + } + }, this); + titleEditor.center(); + titleEditor.open(); + }); + // eslint-disable-next-line no-underscore-dangle + tabButton._add(renameButton, { + row: 0, + column: 3 + }); + + const closeButton = new qx.ui.form.Button(null, "@FontAwesome5Solid/times/12").set({ + ...buttonsAesthetics, + paddingLeft: 4, // adds spacing between buttons + visibility: osparc.data.model.Study.canIWrite(this.__studyData["accessRights"]) ? "visible" : "excluded", + }); + closeButton.addListener("execute", () => { + const messages = this.getChildControl("conversation").getMessages(); + if (messages.length === 0) { + osparc.store.ConversationsProject.getInstance().deleteConversation(this.__studyData["uuid"], this.getConversationId()); + } else { + const msg = this.tr("Are you sure you want to delete the conversation?"); + const confirmationWin = new osparc.ui.window.Confirmation(msg).set({ + caption: this.tr("Delete Conversation"), + confirmText: this.tr("Delete"), + confirmAction: "delete" + }); + confirmationWin.open(); + confirmationWin.addListener("close", () => { + if (confirmationWin.getConfirmed()) { + osparc.store.ConversationsProject.getInstance().deleteConversation(this.__studyData["uuid"], this.getConversationId()); + } + }, this); + } + }); + // eslint-disable-next-line no-underscore-dangle + tabButton._add(closeButton, { + row: 0, + column: 4 + }); + this.bind("conversation", closeButton, "visibility", { + converter: value => value ? "visible" : "excluded" + }); + }, + + renameConversation: function(newName) { + this.getChildControl("button").setLabel(newName); + }, + + __updateMessagesNumber: function() { + const nMessagesLabel = this.getChildControl("n-messages"); + const messages = this.getChildControl("conversation").getMessages(); + const nMessages = messages.filter(msg => msg["type"] === "MESSAGE").length; + if (nMessages === 0) { + nMessagesLabel.setValue(this.tr("No Messages yet")); + } else if (nMessages === 1) { + nMessagesLabel.setValue(this.tr("1 Message")); + } else if (nMessages > 1) { + nMessagesLabel.setValue(nMessages + this.tr(" Messages")); + } + }, + } +}); 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 ec7e47675835..bf0d7c0e33a6 100644 --- a/services/static-webserver/client/source/class/osparc/study/Conversations.js +++ b/services/static-webserver/client/source/class/osparc/study/Conversations.js @@ -163,8 +163,7 @@ qx.Class.define("osparc.study.Conversations", { const studyData = this.getStudyData(); let conversationPage = null; if (conversationData) { - conversationPage = new osparc.study.Conversation(studyData, conversationData); - conversationPage.setLabel(conversationData["name"]); + conversationPage = new osparc.study.ConversationPage(studyData, conversationData); const conversationId = conversationData["conversationId"]; osparc.store.ConversationsProject.getInstance().addListener("conversationDeleted", e => { const data = e.getData(); @@ -173,9 +172,8 @@ qx.Class.define("osparc.study.Conversations", { } }); } else { - // create a temporary conversation - conversationPage = new osparc.study.Conversation(studyData); - conversationPage.setLabel(this.tr("new")); + // create a temporary conversation page + conversationPage = new osparc.study.ConversationPage(studyData); } return conversationPage; }, diff --git a/services/static-webserver/client/source/class/osparc/support/Conversation.js b/services/static-webserver/client/source/class/osparc/support/Conversation.js index 59f0bef60bd0..23588a4c1e26 100644 --- a/services/static-webserver/client/source/class/osparc/support/Conversation.js +++ b/services/static-webserver/client/source/class/osparc/support/Conversation.js @@ -17,41 +17,13 @@ qx.Class.define("osparc.support.Conversation", { - extend: qx.ui.core.Widget, + extend: osparc.conversation.Conversation, /** * @param conversation {osparc.data.model.Conversation} Conversation */ construct: function(conversation) { - this.base(arguments); - - this.__messages = []; - - this._setLayout(new qx.ui.layout.VBox(5)); - - this.__buildLayout(); - - if (conversation) { - this.setConversation(conversation); - } - }, - - properties: { - conversation: { - check: "osparc.data.model.Conversation", - init: null, - nullable: true, - event: "changeConversation", - apply: "__applyConversation", - }, - - studyId: { - check: "String", - init: null, - nullable: true, - event: "changeStudyId", - apply: "__applyStudyId", - }, + this.base(arguments, conversation); }, statics: { @@ -66,49 +38,9 @@ qx.Class.define("osparc.support.Conversation", { }, members: { - __messages: null, - _createChildControlImpl: function(id) { let control; switch (id) { - case "spacer-top": - control = new qx.ui.core.Spacer(); - this._addAt(control, 0, { - flex: 100 // high number to keep even a one message list at the bottom - }); - break; - case "messages-container-scroll": - control = new qx.ui.container.Scroll(); - this._addAt(control, 1, { - flex: 1 - }); - break; - case "messages-container": - control = new qx.ui.container.Composite(new qx.ui.layout.VBox(5)).set({ - alignY: "middle" - }); - this.getChildControl("messages-container-scroll").add(control); - break; - case "load-more-button": - control = new osparc.ui.form.FetchButton(this.tr("Load more messages...")); - control.addListener("execute", () => this.__reloadMessages()); - this._addAt(control, 2); - break; - case "support-suggestion": - control = new qx.ui.container.Composite(new qx.ui.layout.VBox(5)).set({ - alignY: "middle" - }); - this._addAt(control, 3); - break; - case "add-message": - control = new osparc.conversation.AddMessage().set({ - padding: 5, - }); - this.bind("conversation", control, "conversationId", { - converter: conversation => conversation ? conversation.getConversationId() : null - }); - this._addAt(control, 4); - break; case "share-project-layout": control = new qx.ui.container.Composite(new qx.ui.layout.HBox()).set({ backgroundColor: "strong-main", @@ -131,9 +63,9 @@ qx.Class.define("osparc.support.Conversation", { return control || this.base(arguments, id); }, - __buildLayout: function() { - this.getChildControl("spacer-top"); - this.getChildControl("messages-container"); + _buildLayout: function() { + this.base(arguments); + const addMessages = this.getChildControl("add-message"); addMessages.addListener("addMessage", e => { const content = e.getData(); @@ -153,9 +85,9 @@ qx.Class.define("osparc.support.Conversation", { let isBookACall = false; // make these checks first, setConversation will reload messages if ( - this.__messages.length === 1 && - this.__messages[0]["systemMessageType"] && - this.__messages[0]["systemMessageType"] === osparc.support.Conversation.SYSTEM_MESSAGE_TYPE.BOOK_A_CALL + this._messages.length === 1 && + this._messages[0]["systemMessageType"] && + this._messages[0]["systemMessageType"] === osparc.support.Conversation.SYSTEM_MESSAGE_TYPE.BOOK_A_CALL ) { isBookACall = true; } @@ -180,33 +112,17 @@ qx.Class.define("osparc.support.Conversation", { }); }, + _applyConversation: function(conversation) { + this.base(arguments, conversation); + + this.__populateShareProjectCheckbox(); + }, + __postMessage: function(content) { const conversationId = this.getConversation().getConversationId(); return osparc.store.ConversationsSupport.getInstance().postMessage(conversationId, content); }, - __applyConversation: function(conversation) { - this.clearAllMessages(); - this.__reloadMessages(); - - if (conversation) { - conversation.addListener("messageAdded", e => { - const data = e.getData(); - this.addMessage(data); - }); - conversation.addListener("messageUpdated", e => { - const data = e.getData(); - this.updateMessage(data); - }); - conversation.addListener("messageDeleted", e => { - const data = e.getData(); - this.deleteMessage(data); - }); - } - - this.__populateShareProjectCheckbox(); - }, - __populateShareProjectCheckbox: function() { const conversation = this.getConversation(); @@ -267,26 +183,6 @@ qx.Class.define("osparc.support.Conversation", { }); }, - __reloadMessages: function() { - const loadMoreMessages = this.getChildControl("load-more-button"); - if (this.getConversation() === null) { - loadMoreMessages.hide(); - return; - } - - loadMoreMessages.show(); - loadMoreMessages.setFetching(true); - this.getConversation().getNextMessages() - .then(resp => { - const messages = resp["data"]; - messages.forEach(message => this.addMessage(message)); - if (resp["_links"]["next"] === null && loadMoreMessages) { - loadMoreMessages.exclude(); - } - }) - .finally(() => loadMoreMessages.setFetching(false)); - }, - addSystemMessage: function(type) { type = type || osparc.support.Conversation.SYSTEM_MESSAGE_TYPE.ASK_A_QUESTION; @@ -321,91 +217,5 @@ qx.Class.define("osparc.support.Conversation", { this.addMessage(systemMessage); } }, - - addMessage: function(message) { - // 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 latest‐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); - 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 - const messagesContainer = this.getChildControl("messages-container"); - messagesContainer.addAt(control, insertAt); - } - - // scroll to bottom - // add timeout to ensure the scroll happens after the UI is updated - setTimeout(() => { - const messagesScroll = this.getChildControl("messages-container-scroll"); - messagesScroll.scrollToY(messagesScroll.getChildControl("pane").getScrollMaxY()); - }, 50); - }, - - clearAllMessages: function() { - this.__messages = []; - this.getChildControl("messages-container").removeAll(); - }, - - 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 messagesContainer = this.getChildControl("messages-container"); - const children = messagesContainer.getChildren(); - const controlIndex = children.findIndex( - ctrl => ("getMessage" in ctrl && ctrl.getMessage()["messageId"] === message["messageId"]) - ); - if (controlIndex > -1) { - messagesContainer.remove(children[controlIndex]); - } - }, - - updateMessage: function(message) { - // 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 - const messagesContainer = this.getChildControl("messages-container"); - const messageUI = messagesContainer.getChildren().find(control => { - return "getMessage" in control && control.getMessage()["messageId"] === message["messageId"]; - }); - if (messageUI) { - // Force a new reference - messageUI.setMessage(Object.assign({}, message)); - } - }, } }); diff --git a/services/static-webserver/client/source/class/osparc/support/ConversationListItem.js b/services/static-webserver/client/source/class/osparc/support/ConversationListItem.js index 727525770f61..ab88649bfad7 100644 --- a/services/static-webserver/client/source/class/osparc/support/ConversationListItem.js +++ b/services/static-webserver/client/source/class/osparc/support/ConversationListItem.js @@ -26,9 +26,7 @@ qx.Class.define("osparc.support.ConversationListItem", { layout.setSpacingY(0); // decorate - this.getChildControl("thumbnail").getContentElement().setStyles({ - "border-radius": "16px" - }); + this.getChildControl("thumbnail").setDecorator("circled"); this.getChildControl("subtitle").set({ textColor: "text-disabled", }); diff --git a/services/static-webserver/client/source/class/osparc/theme/Decoration.js b/services/static-webserver/client/source/class/osparc/theme/Decoration.js index 5d0fee783ee1..f9474e3dcb06 100644 --- a/services/static-webserver/client/source/class/osparc/theme/Decoration.js +++ b/services/static-webserver/client/source/class/osparc/theme/Decoration.js @@ -26,6 +26,12 @@ qx.Theme.define("osparc.theme.Decoration", { } }, + "circled": { + style: { + radius: 16 + } + }, + "chat-bubble": { style: { radius: 4, diff --git a/services/static-webserver/client/source/class/osparc/user/UserProfile.js b/services/static-webserver/client/source/class/osparc/user/UserProfile.js index 0aefd38b835d..8d3edc459e10 100644 --- a/services/static-webserver/client/source/class/osparc/user/UserProfile.js +++ b/services/static-webserver/client/source/class/osparc/user/UserProfile.js @@ -237,8 +237,6 @@ qx.Class.define("osparc.user.UserProfile", { this.getChildControl("user-id").setValue(String(user.getUserId())); this.getChildControl("group-id").setValue(String(user.getGroupId())); - // this.getChildControl("thumbnail").setSource(user.createThumbnail(this.self().THUMBNAIL_SIZE)); - // middle grid this.getChildControl("institution").setValue(user.getInstitution() || "-"); this.getChildControl("address").setValue(user.getAddress() || "-");