diff --git a/services/static-webserver/client/source/class/osparc/conversation/AddMessage.js b/services/static-webserver/client/source/class/osparc/conversation/AddMessage.js new file mode 100644 index 00000000000..7a0cf26f871 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/conversation/AddMessage.js @@ -0,0 +1,243 @@ +/* ************************************************************************ + + 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.conversation.AddMessage", { + extend: qx.ui.core.Widget, + + /** + * @param studyData {Object} serialized Study Data + * @param conversationId {String} Conversation Id + */ + construct: function(studyData, conversationId = null) { + this.base(arguments); + + this.__studyData = studyData; + this.__conversationId = conversationId; + + this._setLayout(new qx.ui.layout.VBox(5)); + + this.__buildLayout(); + }, + + events: { + "commentAdded": "qx.event.type.Event" + }, + + members: { + __studyData: null, + __conversationId: null, + + _createChildControlImpl: function(id) { + let control; + switch (id) { + case "add-comment-label": + control = new qx.ui.basic.Label().set({ + value: this.tr("Add comment") + }); + this._add(control); + break; + case "add-comment-layout": { + const grid = new qx.ui.layout.Grid(8, 5); + grid.setColumnWidth(0, 32); + grid.setColumnFlex(1, 1); + control = new qx.ui.container.Composite(grid); + this._add(control, { + flex: 1 + }); + break; + } + case "thumbnail": { + control = new qx.ui.basic.Image().set({ + alignY: "middle", + scale: true, + allowGrowX: true, + allowGrowY: true, + allowShrinkX: true, + allowShrinkY: true, + maxWidth: 32, + maxHeight: 32, + decorator: "rounded", + }); + const authData = osparc.auth.Data.getInstance(); + const myUsername = authData.getUsername(); + const myEmail = authData.getEmail(); + control.set({ + source: osparc.utils.Avatar.emailToThumbnail(myEmail, myUsername, 32) + }); + const layout = this.getChildControl("add-comment-layout"); + layout.add(control, { + row: 0, + column: 0 + }); + break; + } + case "comment-field": + control = new osparc.editor.MarkdownEditor(); + control.getChildControl("buttons").exclude(); + const layout = this.getChildControl("add-comment-layout"); + layout.add(control, { + row: 0, + column: 1 + }); + break; + case "add-comment-button": + control = new qx.ui.form.Button(this.tr("Add message")).set({ + appearance: "form-button", + allowGrowX: false, + alignX: "right" + }); + control.setEnabled(osparc.data.model.Study.canIWrite(this.__studyData["accessRights"])); + this._add(control); + break; + case "notify-user-button": + control = new qx.ui.form.Button("🔔 " + this.tr("Notify user")).set({ + appearance: "form-button", + allowGrowX: false, + alignX: "right" + }); + control.setEnabled(osparc.data.model.Study.canIWrite(this.__studyData["accessRights"])); + this._add(control); + break; + } + + return control || this.base(arguments, id); + }, + + __buildLayout: function() { + this.getChildControl("thumbnail"); + this.getChildControl("comment-field"); + + const addMessageButton = this.getChildControl("add-comment-button"); + addMessageButton.addListener("execute", () => this.__addComment()); + + const notifyUserButton = this.getChildControl("notify-user-button"); + notifyUserButton.addListener("execute", () => this.__notifyUserTapped()); + }, + + __addComment: function() { + if (this.__conversationId) { + this.__postMessage(); + } else { + // create new conversation first + osparc.study.Conversations.addConversation(this.__studyData["uuid"]) + .then(data => { + this.__conversationId = data["conversationId"]; + this.__postMessage(); + }) + } + }, + + __notifyUserTapped: function() { + const showOrganizations = false; + const showAccessRights = false; + const userManager = new osparc.share.NewCollaboratorsManager(this.__studyData, showOrganizations, showAccessRights).set({ + acceptOnlyOne: true, + }); + userManager.setCaption(this.tr("Notify user")); + userManager.getActionButton().setLabel(this.tr("Notify")); + userManager.addListener("addCollaborators", e => { + userManager.close(); + const data = e.getData(); + const userGids = data["selectedGids"]; + if (userGids && userGids.length) { + const userGid = parseInt(userGids[0]); + this.__notifyUser(userGid); + } + }); + }, + + __notifyUser: function(userGid) { + // Note! + // This check only works if the project is directly shared with the user. + // If it's shared through a group, it might be a bit confusing + if (userGid in this.__studyData["accessRights"]) { + this.__addNotify(userGid); + } else { + const msg = this.tr("This user has no access to the project. Do you want to share it?"); + const win = new osparc.ui.window.Confirmation(msg).set({ + caption: this.tr("Share"), + confirmText: this.tr("Share"), + confirmAction: "create" + }); + win.center(); + win.open(); + win.addListener("close", () => { + if (win.getConfirmed()) { + const newCollaborators = { + [userGid]: osparc.data.Roles.STUDY["write"].accessRights + }; + osparc.store.Study.addCollaborators(this.__studyData, newCollaborators) + .then(() => { + this.__addNotify(userGid); + const potentialCollaborators = osparc.store.Groups.getInstance().getPotentialCollaborators() + if (userGid in potentialCollaborators && "getUserId" in potentialCollaborators[userGid]) { + const uid = potentialCollaborators[userGid].getUserId(); + osparc.notification.Notifications.pushStudyShared(uid, this.__studyData["uuid"]); + } + }) + .catch(err => osparc.FlashMessenger.logError(err)); + } + }, this); + } + }, + + __addNotify: function(userGid) { + if (this.__conversationId) { + this.__postNotify(userGid); + } else { + // create new conversation first + osparc.study.Conversations.addConversation(this.__studyData["uuid"]) + .then(data => { + this.__conversationId = data["conversationId"]; + this.__postNotify(userGid); + }); + } + }, + + __postMessage: function() { + const commentField = this.getChildControl("comment-field"); + const comment = commentField.getChildControl("text-area").getValue(); + if (comment) { + osparc.study.Conversations.addMessage(this.__studyData["uuid"], this.__conversationId, comment) + .then(data => { + this.fireDataEvent("commentAdded", data); + commentField.getChildControl("text-area").setValue(""); + osparc.FlashMessenger.logAs(this.tr("Message added"), "INFO"); + }); + } + }, + + __postNotify: function(userGid) { + if (userGid) { + osparc.study.Conversations.notifyUser(this.__studyData["uuid"], this.__conversationId, userGid) + .then(data => { + this.fireDataEvent("commentAdded", data); + const potentialCollaborators = osparc.store.Groups.getInstance().getPotentialCollaborators(); + if (userGid in potentialCollaborators) { + if ("getUserId" in potentialCollaborators[userGid]) { + const uid = potentialCollaborators[userGid].getUserId(); + osparc.notification.Notifications.pushConversationNotification(uid, this.__studyData["uuid"]); + } + const msg = "getLabel" in potentialCollaborators[userGid] ? potentialCollaborators[userGid].getLabel() + this.tr(" was notified") : this.tr("Notification sent"); + osparc.FlashMessenger.logAs(msg, "INFO"); + } + }); + } + }, + } +}); diff --git a/services/static-webserver/client/source/class/osparc/info/Conversation.js b/services/static-webserver/client/source/class/osparc/conversation/Conversation.js similarity index 87% rename from services/static-webserver/client/source/class/osparc/info/Conversation.js rename to services/static-webserver/client/source/class/osparc/conversation/Conversation.js index d63d9b7732d..d61f5620098 100644 --- a/services/static-webserver/client/source/class/osparc/info/Conversation.js +++ b/services/static-webserver/client/source/class/osparc/conversation/Conversation.js @@ -16,7 +16,7 @@ ************************************************************************ */ -qx.Class.define("osparc.info.Conversation", { +qx.Class.define("osparc.conversation.Conversation", { extend: qx.ui.tabview.Page, /** @@ -32,7 +32,7 @@ qx.Class.define("osparc.info.Conversation", { this.setConversationId(conversationId); } - this._setLayout(new qx.ui.layout.VBox(10)); + this._setLayout(new qx.ui.layout.VBox(5)); this.set({ padding: 10, @@ -152,7 +152,9 @@ qx.Class.define("osparc.info.Conversation", { this.__messagesList = new qx.ui.container.Composite(new qx.ui.layout.VBox(5)).set({ alignY: "middle" }); - this._add(this.__messagesList, { + const scrollView = new qx.ui.container.Scroll(); + scrollView.add(this.__messagesList); + this._add(scrollView, { flex: 1 }); @@ -161,7 +163,7 @@ qx.Class.define("osparc.info.Conversation", { this._add(this.__loadMoreMessages); if (osparc.data.model.Study.canIWrite(this.__studyData["accessRights"])) { - const addMessages = new osparc.info.CommentAdd(this.__studyData["uuid"], this.getConversationId()); + const addMessages = new osparc.conversation.AddMessage(this.__studyData, this.getConversationId()); addMessages.setPaddingLeft(10); addMessages.addListener("commentAdded", e => { const data = e.getData(); @@ -223,15 +225,26 @@ qx.Class.define("osparc.info.Conversation", { }, __addMessages: function(messages) { - if (messages.length === 1) { + const nMessages = messages.filter(msg => msg["type"] === "MESSAGE").length; + if (nMessages === 1) { this.__messagesTitle.setValue(this.tr("1 Message")); - } else if (messages.length > 1) { - this.__messagesTitle.setValue(messages.length + this.tr(" Messages")); + } else if (nMessages > 1) { + this.__messagesTitle.setValue(nMessages + this.tr(" Messages")); } messages.forEach(message => { - const messageUi = new osparc.info.CommentUI(message); - this.__messagesList.add(messageUi); + let control = null; + switch (message["type"]) { + case "MESSAGE": + control = new osparc.conversation.MessageUI(message); + break; + case "NOTIFICATION": + control = new osparc.conversation.NotificationUI(message); + break; + } + if (control) { + this.__messagesList.add(control); + } }); }, } diff --git a/services/static-webserver/client/source/class/osparc/info/CommentUI.js b/services/static-webserver/client/source/class/osparc/conversation/MessageUI.js similarity index 71% rename from services/static-webserver/client/source/class/osparc/info/CommentUI.js rename to services/static-webserver/client/source/class/osparc/conversation/MessageUI.js index 22263198564..55a5cbec23d 100644 --- a/services/static-webserver/client/source/class/osparc/info/CommentUI.js +++ b/services/static-webserver/client/source/class/osparc/conversation/MessageUI.js @@ -16,35 +16,38 @@ ************************************************************************ */ -qx.Class.define("osparc.info.CommentUI", { +qx.Class.define("osparc.conversation.MessageUI", { extend: qx.ui.core.Widget, /** - * @param comment {Object} comment + * @param message {Object} message */ - construct: function(comment) { + construct: function(message) { this.base(arguments); - this.__comment = comment; + this.__message = message; - const isMyComment = this.__isMyComment(); + const isMyMessage = this.self().isMyMessage(this.__message); const layout = new qx.ui.layout.Grid(12, 4); - layout.setColumnFlex(1, 1); // comment - layout.setColumnFlex(isMyComment ? 0 : 2, 3); // spacer + layout.setColumnFlex(1, 1); // content + layout.setColumnFlex(isMyMessage ? 0 : 2, 3); // spacer this._setLayout(layout); this.setPadding(5); this.__buildLayout(); }, - members: { - __comment: null, + statics: { + isMyMessage: function(message) { + return message && osparc.auth.Data.getInstance().getGroupId() === message["userGroupId"]; + } + }, - __isMyComment: function() { - return this.__comment && osparc.auth.Data.getInstance().getGroupId() === this.__comment["userGroupId"]; - }, + members: { + __message: null, _createChildControlImpl: function(id) { + const isMyMessage = this.self().isMyMessage(this.__message); let control; switch (id) { case "thumbnail": @@ -53,17 +56,17 @@ qx.Class.define("osparc.info.CommentUI", { maxWidth: 32, maxHeight: 32, decorator: "rounded", - marginTop: 2, + marginTop: 4, }); this._add(control, { row: 0, - column: this.__isMyComment() ? 2 : 0, + column: isMyMessage ? 2 : 0, rowSpan: 2, }); break; case "header-layout": control = new qx.ui.container.Composite(new qx.ui.layout.HBox(5).set({ - alignX: this.__isMyComment() ? "right" : "left" + alignX: isMyMessage ? "right" : "left" })); control.addAt(new qx.ui.basic.Label("-"), 1); this._add(control, { @@ -75,15 +78,15 @@ qx.Class.define("osparc.info.CommentUI", { control = new qx.ui.basic.Label().set({ font: "text-12" }); - this.getChildControl("header-layout").addAt(control, this.__isMyComment() ? 2 : 0); + this.getChildControl("header-layout").addAt(control, isMyMessage ? 2 : 0); break; case "last-updated": control = new qx.ui.basic.Label().set({ font: "text-12" }); - this.getChildControl("header-layout").addAt(control, this.__isMyComment() ? 0 : 2); + this.getChildControl("header-layout").addAt(control, isMyMessage ? 0 : 2); break; - case "comment-content": + case "message-content": control = new osparc.ui.markdown.Markdown().set({ decorator: "rounded", noMargin: true, @@ -92,7 +95,7 @@ qx.Class.define("osparc.info.CommentUI", { allowGrowX: true, }); control.getContentElement().setStyles({ - "text-align": this.__isMyComment() ? "right" : "left", + "text-align": isMyMessage ? "right" : "left", }); this._add(control, { row: 1, @@ -103,7 +106,7 @@ qx.Class.define("osparc.info.CommentUI", { control = new qx.ui.core.Spacer(); this._add(control, { row: 1, - column: this.__isMyComment() ? 0 : 2, + column: isMyMessage ? 0 : 2, }); break; } @@ -116,15 +119,15 @@ qx.Class.define("osparc.info.CommentUI", { const userName = this.getChildControl("user-name"); - const date = new Date(this.__comment["modified"]); + const date = new Date(this.__message["modified"]); const date2 = osparc.utils.Utils.formatDateAndTime(date); const lastUpdate = this.getChildControl("last-updated"); lastUpdate.setValue(date2); - const commentContent = this.getChildControl("comment-content"); - commentContent.setValue(this.__comment["content"]); + const messageContent = this.getChildControl("message-content"); + messageContent.setValue(this.__message["content"]); - osparc.store.Users.getInstance().getUser(this.__comment["userGroupId"]) + osparc.store.Users.getInstance().getUser(this.__message["userGroupId"]) .then(user => { if (user) { thumbnail.setSource(user.getThumbnail()); diff --git a/services/static-webserver/client/source/class/osparc/conversation/NotificationUI.js b/services/static-webserver/client/source/class/osparc/conversation/NotificationUI.js new file mode 100644 index 00000000000..670e3437e3d --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/conversation/NotificationUI.js @@ -0,0 +1,139 @@ +/* ************************************************************************ + + 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.conversation.NotificationUI", { + extend: qx.ui.core.Widget, + + /** + * @param message {Object} message + */ + construct: function(message) { + this.base(arguments); + + this.__message = message; + + const isMyMessage = osparc.conversation.MessageUI.isMyMessage(this.__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(); + }, + + members: { + __message: null, + + // spacer - date - content - (thumbnail-spacer) + // (thumbnail-spacer) - content - date - spacer + _createChildControlImpl: function(id) { + const isMyMessage = osparc.conversation.MessageUI.isMyMessage(this.__message); + let control; + switch (id) { + case "thumbnail-spacer": + control = new qx.ui.core.Spacer().set({ + width: 32, + }); + this._add(control, { + row: 0, + column: isMyMessage ? 3 : 0, + }); + break; + case "message-content": + control = new qx.ui.basic.Label().set({ + }); + control.getContentElement().setStyles({ + "text-align": isMyMessage ? "right" : "left", + }); + this._add(control, { + row: 0, + column: isMyMessage ? 2 : 1, + }); + break; + case "last-updated": + control = new qx.ui.basic.Label().set({ + font: "text-12" + }); + this._add(control, { + row: 0, + column: isMyMessage ? 1 : 2, + }); + break; + case "spacer": + control = new qx.ui.core.Spacer(); + this._add(control, { + row: 0, + column: isMyMessage ? 0 : 3, + }); + break; + } + + return control || this.base(arguments, id); + }, + + __buildLayout: function() { + this.getChildControl("thumbnail-spacer"); + + const isMyMessage = osparc.conversation.MessageUI.isMyMessage(this.__message); + + const modifiedDate = new Date(this.__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"]); + let msgContent = "🔔 "; + Promise.all([ + osparc.store.Users.getInstance().getUser(notifierUserGroupId), + osparc.store.Users.getInstance().getUser(notifiedUserGroupId), + ]) + .then(values => { + const notifierUser = values[0]; + if (isMyMessage) { + msgContent += "You"; + } else if (notifierUser) { + msgContent += notifierUser.getLabel(); + } else { + msgContent += "unknown user"; + } + + msgContent += " notified "; + + const notifiedUser = values[1]; + if (osparc.auth.Data.getInstance().getGroupId() === notifiedUserGroupId) { + msgContent += "You"; + } else if (notifiedUser) { + msgContent += notifiedUser.getLabel(); + } else { + msgContent += "unknown user"; + } + }) + .catch(() => { + msgContent += "unknown user notified"; + }) + .finally(() => { + messageContent.setValue(msgContent); + }); + + this.getChildControl("spacer"); + } + } +}); diff --git a/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js b/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js index 0026d83b81d..d517f80bdd1 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js @@ -144,6 +144,7 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { __resourceModel: null, __infoPage: null, __servicesUpdatePage: null, + __conversationsPage: null, __permissionsPage: null, __tagsPage: null, __billingSettings: null, @@ -294,6 +295,10 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { this._openPage(this.__servicesUpdatePage); }, + openConversations: function() { + this._openPage(this.__conversationsPage); + }, + openAccessRights: function() { this._openPage(this.__permissionsPage); }, @@ -556,7 +561,7 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { const id = "Conversations"; const title = this.tr("Conversations"); const iconSrc = "@FontAwesome5Solid/comments/22"; - const page = new osparc.dashboard.resources.pages.BasePage(title, iconSrc, id); + const page = this.__conversationsPage = new osparc.dashboard.resources.pages.BasePage(title, iconSrc, id); this.__addToolbarButtons(page); const lazyLoadContent = () => { diff --git a/services/static-webserver/client/source/class/osparc/editor/AnnotationNoteCreator.js b/services/static-webserver/client/source/class/osparc/editor/AnnotationNoteCreator.js index 4d3f31c15ee..a1d69c4b2f0 100644 --- a/services/static-webserver/client/source/class/osparc/editor/AnnotationNoteCreator.js +++ b/services/static-webserver/client/source/class/osparc/editor/AnnotationNoteCreator.js @@ -85,54 +85,7 @@ qx.Class.define("osparc.editor.AnnotationNoteCreator", { control = new qx.ui.form.Button(this.tr("Select recipient")).set({ allowGrowX: false }); - control.addListener("execute", () => { - const currentStudy = osparc.store.Store.getInstance().getCurrentStudy().serialize(); - currentStudy["resourceType"] = "study"; - const recipientsManager = new osparc.share.NewCollaboratorsManager(currentStudy, false, false); - recipientsManager.setCaption("Recipient"); - recipientsManager.getActionButton().setLabel(this.tr("Add")); - recipientsManager.addListener("addCollaborators", e => { - const data = e.getData(); - const recipientGids = data["selectedGids"]; - - if (recipientGids && recipientGids.length) { - const recipientGid = parseInt(recipientGids[0]); - this.__setRecipientGid(recipientGid); - recipientsManager.close(); - - const currentAccessRights = this.__study.getAccessRights(); - const proposeSharing = []; - if (!(parseInt(recipientGid) in currentAccessRights)) { - proposeSharing.push(recipientGid); - } - if (proposeSharing.length) { - const collaboratorsManager = new osparc.share.NewCollaboratorsManager(currentStudy, false, true, proposeSharing); - collaboratorsManager.addListener("addCollaborators", ev => { - const { - selectedGids, - newAccessRights, - } = ev.getData(); - const newCollaborators = {}; - selectedGids.forEach(gid => { - newCollaborators[gid] = newAccessRights; - }); - const studyData = this.__study.serialize(); - osparc.store.Study.addCollaborators(studyData, newCollaborators) - .then(() => { - const potentialCollaborators = osparc.store.Groups.getInstance().getPotentialCollaborators() - selectedGids.forEach(gid => { - if (gid in potentialCollaborators && "getUserId" in potentialCollaborators[gid]) { - const uid = potentialCollaborators[gid].getUserId(); - osparc.notification.Notifications.postNewStudy(uid, studyData["uuid"]); - } - }); - }) - .finally(() => collaboratorsManager.close()); - }); - } - } - }, this); - }, this); + control.addListener("execute", () => this.__selectRecipientTapped(), this); this.getChildControl("recipient-layout").add(control); break; case "selected-recipient": @@ -178,6 +131,59 @@ qx.Class.define("osparc.editor.AnnotationNoteCreator", { return control || this.base(arguments, id); }, + __selectRecipientTapped: function() { + const currentStudyData = osparc.store.Store.getInstance().getCurrentStudy().serialize(); + currentStudyData["resourceType"] = "study"; + const usersManager = new osparc.share.NewCollaboratorsManager(currentStudyData, false, false).set({ + acceptOnlyOne: true, + }); + usersManager.setCaption("Recipient"); + usersManager.getActionButton().setLabel(this.tr("Add")); + usersManager.addListener("addCollaborators", e => { + usersManager.close(); + const data = e.getData(); + const userGids = data["selectedGids"]; + if (userGids && userGids.length) { + const userGid = parseInt(userGids[0]); + this.__recipientSelected(userGid); + } + }, this); + }, + + __recipientSelected: function(userGid) { + const currentAccessRights = this.__study.getAccessRights(); + if (userGid in currentAccessRights) { + this.__setRecipientGid(userGid); + } else { + const msg = this.tr("This user has no access to the project. Do you want to share it?"); + const win = new osparc.ui.window.Confirmation(msg).set({ + caption: this.tr("Share"), + confirmText: this.tr("Share"), + confirmAction: "create" + }); + win.center(); + win.open(); + win.addListener("close", () => { + if (win.getConfirmed()) { + const newCollaborators = { + [userGid]: osparc.data.Roles.STUDY["write"].accessRights + }; + const currentStudyData = osparc.store.Store.getInstance().getCurrentStudy().serialize(); + osparc.store.Study.addCollaborators(currentStudyData, newCollaborators) + .then(() => { + this.__setRecipientGid(userGid); + const potentialCollaborators = osparc.store.Groups.getInstance().getPotentialCollaborators() + if (userGid in potentialCollaborators && "getUserId" in potentialCollaborators[userGid]) { + const uid = potentialCollaborators[userGid].getUserId(); + osparc.notification.Notifications.pushStudyShared(uid, currentStudyData["uuid"]); + } + }) + .finally(() => collaboratorsManager.close()); + } + }); + } + }, + __setRecipientGid: function(gid) { this.setRecipientGid(gid); // only users were proposed diff --git a/services/static-webserver/client/source/class/osparc/info/CommentAdd.js b/services/static-webserver/client/source/class/osparc/info/CommentAdd.js deleted file mode 100644 index 8bfb2a2c330..00000000000 --- a/services/static-webserver/client/source/class/osparc/info/CommentAdd.js +++ /dev/null @@ -1,149 +0,0 @@ -/* ************************************************************************ - - 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.info.CommentAdd", { - extend: qx.ui.core.Widget, - - /** - * @param studyId {String} Study Id - * @param conversationId {String} Conversation Id - */ - construct: function(studyId, conversationId = null) { - this.base(arguments); - - this.__studyId = studyId; - this.__conversationId = conversationId; - - this._setLayout(new qx.ui.layout.VBox(5)); - - this.__buildLayout(); - }, - - events: { - "commentAdded": "qx.event.type.Event" - }, - - members: { - __studyId: null, - __conversationId: null, - - _createChildControlImpl: function(id) { - let control; - switch (id) { - case "add-comment-label": - control = new qx.ui.basic.Label().set({ - value: this.tr("Add comment") - }); - this._add(control); - break; - case "add-comment-layout": { - const grid = new qx.ui.layout.Grid(8, 5); - grid.setColumnWidth(0, 32); - grid.setColumnFlex(1, 1); - control = new qx.ui.container.Composite(grid); - this._add(control, { - flex: 1 - }); - break; - } - case "thumbnail": { - control = new qx.ui.basic.Image().set({ - alignY: "middle", - scale: true, - allowGrowX: true, - allowGrowY: true, - allowShrinkX: true, - allowShrinkY: true, - maxWidth: 32, - maxHeight: 32, - decorator: "rounded", - }); - const authData = osparc.auth.Data.getInstance(); - const myUsername = authData.getUsername(); - const myEmail = authData.getEmail(); - control.set({ - source: osparc.utils.Avatar.emailToThumbnail(myEmail, myUsername, 32) - }); - const layout = this.getChildControl("add-comment-layout"); - layout.add(control, { - row: 0, - column: 0 - }); - break; - } - case "comment-field": { - control = new osparc.editor.MarkdownEditor(); - control.getChildControl("buttons").exclude(); - const layout = this.getChildControl("add-comment-layout"); - layout.add(control, { - row: 0, - column: 1 - }); - break; - } - case "buttons-layout": { - control = new qx.ui.container.Composite(new qx.ui.layout.HBox(5).set({ - alignX: "right" - })); - this._add(control); - break; - } - case "add-comment-button": { - control = new qx.ui.form.Button(this.tr("Add message")).set({ - appearance: "form-button", - allowGrowX: false, - }); - this.getChildControl("buttons-layout").add(control); - break; - } - } - - return control || this.base(arguments, id); - }, - - __buildLayout: function() { - this.getChildControl("thumbnail"); - this.getChildControl("comment-field"); - const addButton = this.getChildControl("add-comment-button"); - addButton.addListener("execute", () => { - if (this.__conversationId) { - this.__addComment(); - } else { - // create new conversation first - osparc.study.Conversations.addConversation(this.__studyId) - .then(data => { - this.__conversationId = data["conversationId"]; - this.__addComment(); - }) - } - }); - }, - - __addComment: function() { - const commentField = this.getChildControl("comment-field"); - const comment = commentField.getChildControl("text-area").getValue(); - if (comment) { - osparc.study.Conversations.addMessage(this.__studyId, this.__conversationId, comment) - .then(data => { - this.fireDataEvent("commentAdded", data); - commentField.getChildControl("text-area").setValue(""); - }); - } - }, - } -}); diff --git a/services/static-webserver/client/source/class/osparc/notification/Notification.js b/services/static-webserver/client/source/class/osparc/notification/Notification.js index af219894c0f..7318bd995d7 100644 --- a/services/static-webserver/client/source/class/osparc/notification/Notification.js +++ b/services/static-webserver/client/source/class/osparc/notification/Notification.js @@ -54,6 +54,7 @@ qx.Class.define("osparc.notification.Notification", { "NEW_ORGANIZATION", "STUDY_SHARED", "TEMPLATE_SHARED", + "CONVERSATION_NOTIFICATION", "ANNOTATION_NOTE", "WALLET_SHARED" ], diff --git a/services/static-webserver/client/source/class/osparc/notification/NotificationUI.js b/services/static-webserver/client/source/class/osparc/notification/NotificationUI.js index 98bf24ebb7d..7cd46cbae75 100644 --- a/services/static-webserver/client/source/class/osparc/notification/NotificationUI.js +++ b/services/static-webserver/client/source/class/osparc/notification/NotificationUI.js @@ -114,6 +114,60 @@ qx.Class.define("osparc.notification.NotificationUI", { }, __applyNotification: function(notification) { + const icon = this.getChildControl("icon"); + switch (notification.getCategory()) { + case "NEW_ORGANIZATION": + icon.setSource("@FontAwesome5Solid/users/14"); + break; + case "STUDY_SHARED": + icon.setSource("@FontAwesome5Solid/file/14"); + break; + case "TEMPLATE_SHARED": + icon.setSource("@FontAwesome5Solid/copy/14"); + break; + case "CONVERSATION_NOTIFICATION": + icon.setSource("@FontAwesome5Solid/bell/14"); + break; + case "ANNOTATION_NOTE": + icon.setSource("@FontAwesome5Solid/file/14"); + break; + case "WALLET_SHARED": + icon.setSource("@MaterialIcons/account_balance_wallet/14"); + break; + } + + const titleLabel = this.getChildControl("title"); + titleLabel.setValue(notification.getTitle()); + + const descriptionLabel = this.getChildControl("text"); + descriptionLabel.setValue(notification.getText()); + + const date = this.getChildControl("date"); + notification.bind("date", date, "value", { + converter: value => { + if (value) { + return osparc.utils.Utils.formatDateAndTime(new Date(value)); + } + return ""; + } + }); + + const highlight = mouseOn => { + this.set({ + backgroundColor: mouseOn ? "strong-main" : "transparent" + }) + }; + this.addListener("mouseover", () => highlight(true)); + this.addListener("mouseout", () => highlight(false)); + highlight(false); + + // this will trigger calls to the backend, so only make them if necessary + this.addListenerOnce("appear", () => this.__enrichTexts()); + }, + + __enrichTexts: function() { + const notification = this.getNotification(); + let resourceId = null; if (notification.getResourceId()) { resourceId = notification.getResourceId(); @@ -122,28 +176,23 @@ qx.Class.define("osparc.notification.NotificationUI", { const actionablePath = notification.getActionablePath(); resourceId = actionablePath.split("/")[1]; } - const userFromId = notification.getUserFromId(); - const icon = this.getChildControl("icon"); + const userFromId = notification.getUserFromId(); const titleLabel = this.getChildControl("title"); - titleLabel.setValue(notification.getTitle()); const descriptionLabel = this.getChildControl("text"); - descriptionLabel.setValue(notification.getText()); switch (notification.getCategory()) { case "NEW_ORGANIZATION": - icon.setSource("@FontAwesome5Solid/users/14"); if (resourceId) { const org = osparc.store.Groups.getInstance().getOrganization(resourceId); if (org) { - descriptionLabel.setValue("You're now member of '" + org.getLabel() + "'") + descriptionLabel.setValue("You're now member of '" + org.getLabel() + "'"); } else { this.setEnabled(false); } } break; case "STUDY_SHARED": - icon.setSource("@FontAwesome5Solid/file/14"); if (resourceId) { const params = { url: { @@ -167,7 +216,6 @@ qx.Class.define("osparc.notification.NotificationUI", { } break; case "TEMPLATE_SHARED": - icon.setSource("@FontAwesome5Solid/copy/14"); if (resourceId) { osparc.store.Templates.fetchTemplate(resourceId) .then(templateData => { @@ -184,8 +232,25 @@ qx.Class.define("osparc.notification.NotificationUI", { } } break; + case "CONVERSATION_NOTIFICATION": + if (resourceId) { + const params = { + url: { + "studyId": resourceId + } + }; + osparc.data.Resources.fetch("studies", "getOne", params) + .then(study => titleLabel.setValue(`You were notified in '${study["name"]}'`)) + .catch(() => this.setEnabled(false)); + } + if (userFromId) { + const user = osparc.store.Groups.getInstance().getUserByUserId(userFromId); + if (user) { + descriptionLabel.setValue(user.getLabel() + " wants you to check the conversation"); + } + } + break; case "ANNOTATION_NOTE": - icon.setSource("@FontAwesome5Solid/file/14"); if (resourceId) { const params = { url: { @@ -204,28 +269,8 @@ qx.Class.define("osparc.notification.NotificationUI", { } break; case "WALLET_SHARED": - icon.setSource("@MaterialIcons/account_balance_wallet/14"); break; } - - const date = this.getChildControl("date"); - notification.bind("date", date, "value", { - converter: value => { - if (value) { - return osparc.utils.Utils.formatDateAndTime(new Date(value)); - } - return ""; - } - }); - - const highlight = mouseOn => { - this.set({ - backgroundColor: mouseOn ? "strong-main" : "transparent" - }) - }; - this.addListener("mouseover", () => highlight(true)); - this.addListener("mouseout", () => highlight(false)); - highlight(false); }, __notificationTapped: function() { @@ -250,6 +295,7 @@ qx.Class.define("osparc.notification.NotificationUI", { break; case "TEMPLATE_SHARED": case "STUDY_SHARED": + case "CONVERSATION_NOTIFICATION": case "ANNOTATION_NOTE": this.__openStudyDetails(resourceId, notification); break; @@ -294,6 +340,9 @@ qx.Class.define("osparc.notification.NotificationUI", { osparc.dashboard.ResourceBrowserBase.startStudyById(studyId, openCB); } }); + if (notification.getCategory() === "CONVERSATION_NOTIFICATION") { + resourceDetails.addListener("pagesAdded", () => resourceDetails.openConversations()); + } } }) .catch(err => { diff --git a/services/static-webserver/client/source/class/osparc/notification/Notifications.js b/services/static-webserver/client/source/class/osparc/notification/Notifications.js index 2d5a2de9318..8ee9273958a 100644 --- a/services/static-webserver/client/source/class/osparc/notification/Notifications.js +++ b/services/static-webserver/client/source/class/osparc/notification/Notifications.js @@ -85,6 +85,21 @@ qx.Class.define("osparc.notification.Notifications", { }; }, + __newConversationNotificationObj: function(userId, studyId) { + const baseNotification = this.__newNotificationBase(userId); + const specNotification = { + "category": "CONVERSATION_NOTIFICATION", + "actionable_path": "study/"+studyId, + "resource_id": studyId, + "title": "New notification", + "text": "You were notified in a conversation" + }; + return { + ...baseNotification, + ...specNotification + }; + }, + __newAnnotationNoteObj: function(userId, studyId) { const baseNotification = this.__newNotificationBase(userId); const specNotification = { @@ -122,7 +137,7 @@ qx.Class.define("osparc.notification.Notifications", { return osparc.data.Resources.fetch("notifications", "post", params); }, - postNewStudy: function(userId, studyId) { + pushStudyShared: function(userId, studyId) { const params = { data: this.__newStudyObj(userId, studyId) }; @@ -136,7 +151,14 @@ qx.Class.define("osparc.notification.Notifications", { return osparc.data.Resources.fetch("notifications", "post", params); }, - postNewAnnotationNote: function(userId, studyId) { + pushConversationNotification: function(userId, studyId) { + const params = { + data: this.__newConversationNotificationObj(userId, studyId) + }; + return osparc.data.Resources.fetch("notifications", "post", params); + }, + + pushNewAnnotationNote: function(userId, studyId) { const params = { data: this.__newAnnotationNoteObj(userId, studyId) }; diff --git a/services/static-webserver/client/source/class/osparc/share/CollaboratorsStudy.js b/services/static-webserver/client/source/class/osparc/share/CollaboratorsStudy.js index 124d275ae76..6be3a6b9884 100644 --- a/services/static-webserver/client/source/class/osparc/share/CollaboratorsStudy.js +++ b/services/static-webserver/client/source/class/osparc/share/CollaboratorsStudy.js @@ -217,7 +217,7 @@ qx.Class.define("osparc.share.CollaboratorsStudy", { const uid = potentialCollaborators[gid].getUserId(); switch (this._resourceType) { case "study": - osparc.notification.Notifications.postNewStudy(uid, this._serializedDataCopy["uuid"]); + osparc.notification.Notifications.pushStudyShared(uid, this._serializedDataCopy["uuid"]); break; case "template": case "tutorial": diff --git a/services/static-webserver/client/source/class/osparc/share/NewCollaboratorsManager.js b/services/static-webserver/client/source/class/osparc/share/NewCollaboratorsManager.js index c8c21429d73..057cdfc1222 100644 --- a/services/static-webserver/client/source/class/osparc/share/NewCollaboratorsManager.js +++ b/services/static-webserver/client/source/class/osparc/share/NewCollaboratorsManager.js @@ -55,6 +55,14 @@ qx.Class.define("osparc.share.NewCollaboratorsManager", { "shareWithEmails": "qx.event.type.Data", }, + properties: { + acceptOnlyOne: { + check: "Boolean", + init: false, + event: "changeAcceptOnlyOne" + } + }, + members: { __resourceData: null, __showOrganizations: null, @@ -276,6 +284,7 @@ qx.Class.define("osparc.share.NewCollaboratorsManager", { __collaboratorButton: function(collaborator) { const collaboratorButton = new osparc.filter.CollaboratorToggleButton(collaborator); + collaborator.button = collaboratorButton; collaboratorButton.groupId = collaborator.getGroupId(); collaboratorButton.subscribeToFilterGroup("collaboratorsManager"); @@ -298,6 +307,7 @@ qx.Class.define("osparc.share.NewCollaboratorsManager", { }; const collaborator = qx.data.marshal.Json.createModel(collaboratorData); const collaboratorButton = new osparc.filter.CollaboratorToggleButton(collaborator); + collaborator.button = collaboratorButton; collaboratorButton.setIconSrc("@FontAwesome5Solid/envelope/14"); collaboratorButton.addListener("changeValue", e => { @@ -309,6 +319,11 @@ qx.Class.define("osparc.share.NewCollaboratorsManager", { __collaboratorSelected: function(selected, collaboratorGidOrEmail, collaborator, collaboratorButton) { if (selected) { + if (this.isAcceptOnlyOne() && Object.keys(this.__selectedCollaborators).length) { + // unselect the previous collaborator + const id = Object.keys(this.__selectedCollaborators)[0]; + this.__selectedCollaborators[id].button.setValue(false); + } this.__selectedCollaborators[collaboratorGidOrEmail] = collaborator; collaboratorButton.unsubscribeToFilterGroup("collaboratorsManager"); } else if (collaborator.getGroupId() in this.__selectedCollaborators) { 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 d43a945cf0c..720b00f7c99 100644 --- a/services/static-webserver/client/source/class/osparc/study/Conversations.js +++ b/services/static-webserver/client/source/class/osparc/study/Conversations.js @@ -93,6 +93,21 @@ qx.Class.define("osparc.study.Conversations", { return osparc.data.Resources.fetch("conversations", "addMessage", params) .catch(err => osparc.FlashMessenger.logError(err)); }, + + notifyUser: function(studyId, conversationId, userGroupId) { + const params = { + url: { + studyId, + conversationId, + }, + data: { + "content": userGroupId.toString(), // eventually the backend will accept integers + "type": "NOTIFICATION", + } + }; + return osparc.data.Resources.fetch("conversations", "addMessage", params) + .catch(err => osparc.FlashMessenger.logError(err)); + }, }, members: { @@ -151,7 +166,7 @@ qx.Class.define("osparc.study.Conversations", { }; if (conversations.length === 0) { - const noConversationTab = new osparc.info.Conversation(studyData); + const noConversationTab = new osparc.conversation.Conversation(studyData); conversationPages.push(noConversationTab); noConversationTab.setLabel(this.tr("new")); noConversationTab.addListener("conversationDeleted", () => reloadConversations()); @@ -159,7 +174,7 @@ qx.Class.define("osparc.study.Conversations", { } else { conversations.forEach(conversation => { const conversationId = conversation["conversationId"]; - const conversationTab = new osparc.info.Conversation(studyData, conversationId); + const conversationTab = new osparc.conversation.Conversation(studyData, conversationId); conversationPages.push(conversationTab); conversationTab.setLabel(conversation["name"]); conversationTab.addListener("conversationDeleted", () => reloadConversations()); diff --git a/services/static-webserver/client/source/class/osparc/ui/window/Confirmation.js b/services/static-webserver/client/source/class/osparc/ui/window/Confirmation.js index aafb1aac589..05cc501562f 100644 --- a/services/static-webserver/client/source/class/osparc/ui/window/Confirmation.js +++ b/services/static-webserver/client/source/class/osparc/ui/window/Confirmation.js @@ -40,7 +40,7 @@ qx.Class.define("osparc.ui.window.Confirmation", { check: [null, "create", "warning", "delete"], init: null, nullable: true, - apply: "__applyConfirmAppearance" + event: "changeConfirmAction", }, confirmed: { @@ -50,7 +50,6 @@ qx.Class.define("osparc.ui.window.Confirmation", { }, members: { - _createChildControlImpl: function(id) { let control; switch (id) { @@ -64,6 +63,20 @@ qx.Class.define("osparc.ui.window.Confirmation", { this.setConfirmed(true); this.close(1); }, this); + this.bind("confirmAction", control, "appearance", { + converter: value => { + switch (value) { + case "create": + return "strong-button"; + case "warning": + return "warning-button"; + case "delete": + return "danger-button"; + default: + return "strong-button"; + } + } + }); const command = new qx.ui.command.Command("Enter"); control.setCommand(command); const btnsLayout = this.getChildControl("buttons-layout"); @@ -81,23 +94,5 @@ qx.Class.define("osparc.ui.window.Confirmation", { getCancelButton: function() { return this.getChildControl("cancel-button"); }, - - __applyConfirmAppearance: function(confirmationAction) { - const confirmButton = this.getChildControl("confirm-button"); - switch (confirmationAction) { - case "create": - confirmButton.setAppearance("strong-button"); - break; - case "warning": - confirmButton.setAppearance("warning-button"); - break; - case "delete": - confirmButton.setAppearance("danger-button"); - break; - default: - confirmButton.resetAppearance(); - break; - } - } } }); diff --git a/services/static-webserver/client/source/class/osparc/workbench/WorkbenchUI.js b/services/static-webserver/client/source/class/osparc/workbench/WorkbenchUI.js index a9b633ed008..a4380c73195 100644 --- a/services/static-webserver/client/source/class/osparc/workbench/WorkbenchUI.js +++ b/services/static-webserver/client/source/class/osparc/workbench/WorkbenchUI.js @@ -1935,7 +1935,7 @@ qx.Class.define("osparc.workbench.WorkbenchUI", { serializeData.attributes.text = noteEditor.getNote(); const user = osparc.store.Groups.getInstance().getUserByGroupId(gid) if (user) { - osparc.notification.Notifications.postNewAnnotationNote(user.getUserId(), this.getStudy().getUuid()); + osparc.notification.Notifications.pushNewAnnotationNote(user.getUserId(), this.getStudy().getUuid()); } this.__addAnnotation(serializeData); win.close(); diff --git a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml index d8e49bb3adf..db70d6dab52 100644 --- a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml +++ b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml @@ -13519,6 +13519,7 @@ components: - NEW_ORGANIZATION - STUDY_SHARED - TEMPLATE_SHARED + - CONVERSATION_NOTIFICATION - ANNOTATION_NOTE - WALLET_SHARED title: NotificationCategory diff --git a/services/web/server/src/simcore_service_webserver/users/_notifications.py b/services/web/server/src/simcore_service_webserver/users/_notifications.py index 68b322dc29c..37f902b0950 100644 --- a/services/web/server/src/simcore_service_webserver/users/_notifications.py +++ b/services/web/server/src/simcore_service_webserver/users/_notifications.py @@ -20,6 +20,7 @@ class NotificationCategory(StrAutoEnum): NEW_ORGANIZATION = auto() STUDY_SHARED = auto() TEMPLATE_SHARED = auto() + CONVERSATION_NOTIFICATION = auto() ANNOTATION_NOTE = auto() WALLET_SHARED = auto() @@ -41,8 +42,7 @@ def category_to_upper(cls, value: str) -> str: return value.upper() -class UserNotificationCreate(BaseUserNotification): - ... +class UserNotificationCreate(BaseUserNotification): ... class UserNotificationPatch(BaseModel): @@ -100,6 +100,19 @@ def create_from_request_data( "product": "osparc", "read": False, }, + { + "id": "390053c9-3931-40e1-839f-585268f6fd3d", + "user_id": "1", + "category": "CONVERSATION_NOTIFICATION", + "actionable_path": "study/27edd65c-b360-11ed-93d7-02420a000014", + "title": "New notification", + "text": "You were notified in a conversation", + "date": "2023-02-23T16:28:13.122Z", + "product": "s4l", + "read": False, + "resource_id": "3fb96d89-ff5d-4d27-b5aa-d20d46e20e12", + "user_from_id": "2", + }, { "id": "390053c9-3931-40e1-839f-585268f6fd3d", "user_id": "1",