diff --git a/services/static-webserver/client/source/class/osparc/conversation/MessageList.js b/services/static-webserver/client/source/class/osparc/conversation/MessageList.js index 7750a90b6068..324d6a6fc981 100644 --- a/services/static-webserver/client/source/class/osparc/conversation/MessageList.js +++ b/services/static-webserver/client/source/class/osparc/conversation/MessageList.js @@ -100,7 +100,7 @@ qx.Class.define("osparc.conversation.MessageList", { if (conversation) { conversation.addListener("messageAdded", e => { const data = e.getData(); - this.__messageAdded(data); + this._messageAdded(data); }); conversation.addListener("messageDeleted", e => { const data = e.getData(); @@ -120,7 +120,7 @@ qx.Class.define("osparc.conversation.MessageList", { return; } - this.getConversation().getMessages().forEach(message => this.__messageAdded(message)); + this.getConversation().getMessages().forEach(message => this._messageAdded(message)); loadMoreMessages.show(); loadMoreMessages.setFetching(true); @@ -153,7 +153,7 @@ qx.Class.define("osparc.conversation.MessageList", { ); }, - __messageAdded: function(message) { + _messageAdded: function(message) { // ignore it if it was already there const existingMessageUI = this.__getMessageUI(message.getMessageId()); if (existingMessageUI) { @@ -171,7 +171,7 @@ qx.Class.define("osparc.conversation.MessageList", { control = new osparc.conversation.NotificationUI(message); break; } - if (control) { + if (control && this.getConversation()) { // insert into the UI at the same position const insertAt = this.getConversation().getMessageIndex(message.getMessageId()); const messagesContainer = this.getChildControl("messages-container"); 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 cb57114265eb..8a9c852f9eed 100644 --- a/services/static-webserver/client/source/class/osparc/conversation/MessageUI.js +++ b/services/static-webserver/client/source/class/osparc/conversation/MessageUI.js @@ -36,15 +36,6 @@ qx.Class.define("osparc.conversation.MessageUI", { }); }, - statics: { - isMyMessage: function(message) { - if (message.getUserGroupId() === osparc.data.model.Message.SYSTEM_MESSAGE_ID) { - return false; - } - return message && osparc.auth.Data.getInstance().getGroupId() === message.getUserGroupId(); - } - }, - events: { "messageUpdated": "qx.event.type.Data", "messageDeleted": "qx.event.type.Data", @@ -61,7 +52,7 @@ qx.Class.define("osparc.conversation.MessageUI", { members: { _createChildControlImpl: function(id) { - const isMyMessage = this.self().isMyMessage(this.getMessage()); + const isMyMessage = osparc.data.model.Message.isMyMessage(this.getMessage()); let control; switch (id) { case "avatar": @@ -154,7 +145,7 @@ qx.Class.define("osparc.conversation.MessageUI", { const avatar = this.getChildControl("avatar"); const userName = this.getChildControl("user-name"); - if (message.getUserGroupId() === osparc.data.model.Message.SYSTEM_MESSAGE_ID) { + if (osparc.data.model.Message.isSupportMessage(message)) { userName.setValue("Support"); } else { osparc.store.Users.getInstance().getUser(message.getUserGroupId()) @@ -168,7 +159,7 @@ qx.Class.define("osparc.conversation.MessageUI", { }); } - if (this.self().isMyMessage(message)) { + if (osparc.data.model.Message.isMyMessage(message)) { const menuButton = this.getChildControl("menu-button"); const menu = new qx.ui.menu.Menu().set({ 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 cdb58a0b79ac..52a3a3ff123b 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 @@ -20,13 +20,13 @@ */ qx.Class.define("osparc.data.model.Conversation", { + type: "abstract", extend: qx.core.Object, /** * @param conversationData {Object} Object containing the serialized Conversation Data - * @param studyId {String} ID of the Study * */ - construct: function(conversationData, studyId) { + construct: function(conversationData) { this.base(arguments); this.set({ @@ -36,17 +36,11 @@ qx.Class.define("osparc.data.model.Conversation", { type: conversationData.type, created: new Date(conversationData.created), modified: new Date(conversationData.modified), - projectId: conversationData.projectUuid || null, - extraContext: conversationData.extraContext || null, - studyId: studyId || null, + lastMessageCreatedAt: conversationData.lastMessageCreatedAt ? new Date(conversationData.lastMessageCreatedAt) : null }); this.__messages = []; this.__listenToConversationMessageWS(); - - if (conversationData.type === "SUPPORT") { - this.__fetchFirstAndLastMessages(); - } }, statics: { @@ -76,7 +70,7 @@ qx.Class.define("osparc.data.model.Conversation", { nullable: false, init: null, event: "changeName", - apply: "__applyName", + apply: "_applyName", }, userGroupId: { @@ -111,46 +105,11 @@ qx.Class.define("osparc.data.model.Conversation", { event: "changeModified", }, - projectId: { - check: "String", - nullable: true, - init: null, - event: "changeProjectId", - }, - - extraContext: { - check: "Object", - nullable: true, - init: null, - event: "changeExtraContext", - }, - - nameAlias: { - check: "String", - nullable: false, - init: "", - event: "changeNameAlias", - }, - - firstMessage: { - check: "osparc.data.model.Message", - nullable: true, - init: null, - event: "changeFirstMessage", - }, - - lastMessage: { - check: "osparc.data.model.Message", - nullable: true, - init: null, - event: "changeLastMessage", - apply: "__applyLastMessage", - }, - - studyId: { - check: "String", + lastMessageCreatedAt: { + check: "Date", nullable: true, init: null, + event: "changeLastMessageCreatedAt", }, }, @@ -161,21 +120,11 @@ qx.Class.define("osparc.data.model.Conversation", { }, members: { - __fetchingFirstAndLastMessage: null, __nextRequestParams: null, __messages: null, - __applyName: function(name) { - if (name && name !== "null") { - this.setNameAlias(name); - } - }, - - __applyLastMessage: function(lastMessage) { - const name = this.getName(); - if (!name || name === "null") { - this.setNameAlias(lastMessage ? lastMessage.getContent() : ""); - } + _applyName: function(name) { + return; }, __listenToConversationMessageWS: function() { @@ -191,13 +140,14 @@ qx.Class.define("osparc.data.model.Conversation", { if (conversationId === this.getConversationId()) { switch (eventName) { case this.self().CHANNELS.CONVERSATION_MESSAGE_CREATED: - this.addMessage(messageData); + this._addMessage(messageData); + this.setLastMessageCreatedAt(new Date(messageData.created)); break; case this.self().CHANNELS.CONVERSATION_MESSAGE_UPDATED: - this.updateMessage(messageData); + this._updateMessage(messageData); break; case this.self().CHANNELS.CONVERSATION_MESSAGE_DELETED: - this.deleteMessage(messageData); + this._deleteMessage(messageData); break; } } @@ -207,38 +157,6 @@ qx.Class.define("osparc.data.model.Conversation", { }); }, - __fetchFirstAndLastMessages: function() { - if (this.__fetchingFirstAndLastMessage) { - return this.__fetchingFirstAndLastMessage; - } - - this.__fetchingFirstAndLastMessage = true; - osparc.store.ConversationsSupport.getInstance().fetchLastMessage(this.getConversationId()) - .then(resp => { - const messages = resp["data"]; - if (messages.length) { - const lastMessage = new osparc.data.model.Message(messages[0]); - this.setLastMessage(lastMessage); - } - // fetch first message only if there is more than one message - if (resp["_meta"]["total"] === 1) { - const firstMessage = new osparc.data.model.Message(messages[0]); - this.setFirstMessage(firstMessage); - } else if (resp["_meta"]["total"] > 1) { - osparc.store.ConversationsSupport.getInstance().fetchFirstMessage(this.getConversationId(), resp["_meta"]) - .then(firstMessages => { - if (firstMessages.length) { - const firstMessage = new osparc.data.model.Message(firstMessages[0]); - this.setFirstMessage(firstMessage); - } - }); - } - return null; - }) - .catch(err => osparc.FlashMessenger.logError(err)) - .finally(() => this.__fetchingFirstAndLastMessage = null); - }, - amIOwner: function() { return this.getUserGroupId() === osparc.auth.Data.getInstance().getGroupId(); }, @@ -251,7 +169,8 @@ qx.Class.define("osparc.data.model.Conversation", { limit: 42 } }; - if (this.getStudyId()) { + const isProjectConversation = this instanceof osparc.data.model.ConversationProject; + if (isProjectConversation) { params.url.studyId = this.getStudyId(); } @@ -263,13 +182,13 @@ qx.Class.define("osparc.data.model.Conversation", { const options = { resolveWResponse: true }; - const promise = this.getStudyId() ? + const promise = isProjectConversation ? osparc.data.Resources.fetch("conversationsStudies", "getMessagesPage", params, options) : osparc.data.Resources.fetch("conversationsSupport", "getMessagesPage", params, options); return promise .then(resp => { const messagesData = resp["data"]; - messagesData.forEach(messageData => this.addMessage(messageData)); + messagesData.forEach(messageData => this._addMessage(messageData)); this.__nextRequestParams = resp["_links"]["next"]; return resp; }) @@ -282,13 +201,6 @@ qx.Class.define("osparc.data.model.Conversation", { .catch(err => osparc.FlashMessenger.logError(err)); }, - patchExtraContext: function(extraContext) { - osparc.store.ConversationsSupport.getInstance().patchExtraContext(this.getConversationId(), extraContext) - .then(() => { - this.setExtraContext(extraContext); - }); - }, - getMessages: function() { return this.__messages; }, @@ -301,81 +213,35 @@ qx.Class.define("osparc.data.model.Conversation", { return this.__messages.some(msg => msg.getMessageId() === messageId); }, - addMessage: function(messageData) { + _addMessage: function(messageData) { let message = this.__messages.find(msg => msg.getMessageId() === messageData["messageId"]); if (!message) { message = new osparc.data.model.Message(messageData); this.__messages.push(message); osparc.data.model.Message.sortMessagesByDate(this.__messages); this.fireDataEvent("messageAdded", message); - this.__evalFirstAndLastMessage(); } return message; }, - updateMessage: function(messageData) { + _updateMessage: function(messageData) { if (messageData) { const found = this.__messages.find(msg => msg.getMessageId() === messageData["messageId"]); if (found) { found.setData(messageData); this.fireDataEvent("messageUpdated", found); - this.__evalFirstAndLastMessage(); } } }, - deleteMessage: function(messageData) { + _deleteMessage: function(messageData) { if (messageData) { const found = this.__messages.find(msg => msg.getMessageId() === messageData["messageId"]); if (found) { this.__messages.splice(this.__messages.indexOf(found), 1); this.fireDataEvent("messageDeleted", found); - this.__evalFirstAndLastMessage(); } } }, - - __evalFirstAndLastMessage: function() { - if (this.__messages && this.__messages.length) { - // newest first - this.setFirstMessage(this.__messages[this.__messages.length - 1]); - this.setLastMessage(this.__messages[0]); - } - }, - - getContextProjectId: function() { - if (this.getExtraContext() && "projectId" in this.getExtraContext()) { - return this.getExtraContext()["projectId"]; - } - return null; - }, - - getFogbugzLink: function() { - if (this.getExtraContext() && "fogbugz_case_url" in this.getExtraContext()) { - return this.getExtraContext()["fogbugz_case_url"]; - } - return null; - }, - - getAppointment: function() { - if (this.getExtraContext() && "appointment" in this.getExtraContext()) { - return this.getExtraContext()["appointment"]; - } - return null; - }, - - setAppointment: function(appointment) { - const extraContext = this.getExtraContext() || {}; - extraContext["appointment"] = appointment ? appointment.toISOString() : null; - // OM: Supporters are not allowed to patch the conversation metadata yet - const backendAllowsPatch = osparc.store.Groups.getInstance().amIASupportUser() ? false : true; - if (backendAllowsPatch) { - return osparc.store.ConversationsSupport.getInstance().patchExtraContext(this.getConversationId(), extraContext) - .then(() => { - this.setExtraContext(Object.assign({}, extraContext)); - }); - } - return Promise.resolve(this.setExtraContext(Object.assign({}, extraContext))); - }, }, }); diff --git a/services/static-webserver/client/source/class/osparc/data/model/ConversationProject.js b/services/static-webserver/client/source/class/osparc/data/model/ConversationProject.js new file mode 100644 index 000000000000..9ea31ac40da1 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/data/model/ConversationProject.js @@ -0,0 +1,44 @@ +/* ************************************************************************ + + 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) + +************************************************************************ */ + +/** + * Class that stores Project Conversation data. + */ + +qx.Class.define("osparc.data.model.ConversationProject", { + extend: osparc.data.model.Conversation, + + /** + * @param conversationData {Object} Object containing the serialized Conversation Data + * @param studyId {String} ID of the Study + * */ + construct: function(conversationData, studyId) { + this.base(arguments, conversationData); + + this.set({ + studyId: studyId || null, + }); + }, + + properties: { + studyId: { + check: "String", + nullable: true, + init: null, + }, + }, +}); diff --git a/services/static-webserver/client/source/class/osparc/data/model/ConversationSupport.js b/services/static-webserver/client/source/class/osparc/data/model/ConversationSupport.js new file mode 100644 index 000000000000..0004ae7fdd47 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/data/model/ConversationSupport.js @@ -0,0 +1,237 @@ +/* ************************************************************************ + + 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) + +************************************************************************ */ + +/** + * Class that stores Support Conversation data. + */ + +qx.Class.define("osparc.data.model.ConversationSupport", { + extend: osparc.data.model.Conversation, + + /** + * @param conversationData {Object} Object containing the serialized Conversation Data + * */ + construct: function(conversationData) { + this.base(arguments, conversationData); + + this.set({ + projectId: conversationData.projectUuid || null, + extraContext: conversationData.extraContext || null, + fogbugzCaseId: conversationData.fogbugz_case_id || null, + readByUser: Boolean(conversationData.isReadByUser), + readBySupport: Boolean(conversationData.isReadBySupport), + resolved: null, + }); + + this.__fetchFirstAndLastMessages(); + }, + + properties: { + nameAlias: { + check: "String", + nullable: false, + init: "", + event: "changeNameAlias", + }, + + projectId: { + check: "String", + nullable: true, + init: null, + event: "changeProjectId", + }, + + extraContext: { + check: "Object", + nullable: true, + init: null, + event: "changeExtraContext", + }, + + fogbugzCaseId: { + check: "String", + nullable: true, + init: null, + event: "changeFogbugzCaseId", + }, + + firstMessage: { + check: "osparc.data.model.Message", + nullable: true, + init: null, + event: "changeFirstMessage", + }, + + lastMessage: { + check: "osparc.data.model.Message", + nullable: true, + init: null, + event: "changeLastMessage", + apply: "__applyLastMessage", + }, + + readByUser: { + check: "Boolean", + nullable: false, + init: null, + event: "changeReadByUser", + }, + + readBySupport: { + check: "Boolean", + nullable: false, + init: null, + event: "changeReadBySupport", + }, + + resolved: { + check: "Boolean", + nullable: true, + init: null, + event: "changeResolved", + }, + }, + + members: { + __fetchingFirstAndLastMessage: null, + + _applyName: function(name) { + if (name && name !== "null") { + this.setNameAlias(name); + } + }, + + __applyLastMessage: function(lastMessage) { + const name = this.getName(); + if (!name || name === "null") { + this.setNameAlias(lastMessage ? lastMessage.getContent() : ""); + } + }, + + __fetchFirstAndLastMessages: function() { + if (this.__fetchingFirstAndLastMessage) { + return this.__fetchingFirstAndLastMessage; + } + + this.__fetchingFirstAndLastMessage = true; + osparc.store.ConversationsSupport.getInstance().fetchLastMessage(this.getConversationId()) + .then(resp => { + const messages = resp["data"]; + if (messages.length) { + const lastMessage = new osparc.data.model.Message(messages[0]); + this.setLastMessage(lastMessage); + } + // fetch first message only if there is more than one message + if (resp["_meta"]["total"] === 1) { + const firstMessage = new osparc.data.model.Message(messages[0]); + this.setFirstMessage(firstMessage); + } else if (resp["_meta"]["total"] > 1) { + osparc.store.ConversationsSupport.getInstance().fetchFirstMessage(this.getConversationId(), resp["_meta"]) + .then(firstMessages => { + if (firstMessages.length) { + const firstMessage = new osparc.data.model.Message(firstMessages[0]); + this.setFirstMessage(firstMessage); + } + }); + } + return null; + }) + .catch(err => osparc.FlashMessenger.logError(err)) + .finally(() => this.__fetchingFirstAndLastMessage = null); + }, + + patchExtraContext: function(extraContext) { + osparc.store.ConversationsSupport.getInstance().patchExtraContext(this.getConversationId(), extraContext) + .then(() => { + this.setExtraContext(extraContext); + }); + }, + + setReadBy: function(isRead) { + osparc.store.Groups.getInstance().amIASupportUser() ? this.setReadBySupport(isRead) : this.setReadByUser(isRead); + }, + + getReadBy: function() { + return osparc.store.Groups.getInstance().amIASupportUser() ? this.getReadBySupport() : this.getReadByUser(); + }, + + markAsRead: function() { + osparc.store.ConversationsSupport.getInstance().markAsRead(this.getConversationId()) + .then(() => this.setReadBy(true)); + }, + + markAsResolved: function() { + osparc.store.ConversationsSupport.getInstance().markAsResolved(this.getConversationId()) + .then(() => { + this.setResolved(true); + osparc.FlashMessenger.logAs(qx.locale.Manager.tr("The case has been marked as resolved"), "INFO"); + }) + .catch(err => osparc.FlashMessenger.logError(err)); + }, + + // overriden + _addMessage: function(messageData) { + const message = this.base(arguments, messageData); + this.__evalFirstAndLastMessage(); + + // mark conversation as unread if the message is from the other party + if (!osparc.data.model.Message.isMyMessage(message)) { + this.setReadBy(false); + } + return message; + }, + + // overriden + _updateMessage: function(messageData) { + this.base(arguments, messageData); + this.__evalFirstAndLastMessage(); + }, + + // overriden + _deleteMessage: function(messageData) { + this.base(arguments, messageData); + this.__evalFirstAndLastMessage(); + }, + + __evalFirstAndLastMessage: function() { + if (this.__messages && this.__messages.length) { + // sort messages by date just in case + osparc.data.model.Message.sortMessagesByDate(this.__messages); + // newest first + this.setFirstMessage(this.__messages[0]); + this.setLastMessage(this.__messages[this.__messages.length - 1]); + } + }, + + getContextProjectId: function() { + if (this.getExtraContext() && "projectId" in this.getExtraContext()) { + return this.getExtraContext()["projectId"]; + } + return null; + }, + + getFogbugzLink: function() { + if (this.getFogbugzCaseId()) { + return this.getFogbugzCaseId(); + } + if (this.getExtraContext() && "fogbugz_case_url" in this.getExtraContext()) { + return this.getExtraContext()["fogbugz_case_url"]; + } + return null; + }, + }, +}); diff --git a/services/static-webserver/client/source/class/osparc/data/model/Message.js b/services/static-webserver/client/source/class/osparc/data/model/Message.js index 4b775d18f5a3..d95ce668b583 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/Message.js +++ b/services/static-webserver/client/source/class/osparc/data/model/Message.js @@ -92,6 +92,17 @@ qx.Class.define("osparc.data.model.Message", { // oldest first: higher in the list. Latest at the bottom messages.sort((a, b) => a.getCreated() - b.getCreated()); }, + + isSupportMessage: function(message) { + return message.getUserGroupId() === osparc.data.model.Message.SYSTEM_MESSAGE_ID; + }, + + isMyMessage: function(message) { + if (osparc.data.model.Message.isSupportMessage(message)) { + return false; + } + return message && osparc.auth.Data.getInstance().getGroupId() === message.getUserGroupId(); + }, }, members: { diff --git a/services/static-webserver/client/source/class/osparc/desktop/MainPage.js b/services/static-webserver/client/source/class/osparc/desktop/MainPage.js index 706d99032d2f..fd9045df2e55 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/MainPage.js +++ b/services/static-webserver/client/source/class/osparc/desktop/MainPage.js @@ -47,11 +47,17 @@ qx.Class.define("osparc.desktop.MainPage", { this._add(osparc.notification.RibbonNotifications.getInstance()); + const navBarPromises = []; const navBar = this.__navBar = new osparc.navigation.NavigationBar(); - navBar.populateLayout(); navBar.addListener("backToDashboardPressed", () => this.__backToDashboardPressed(), this); navBar.addListener("openLogger", () => this.__openLogger(), this); this._add(navBar); + navBarPromises.push(osparc.store.Groups.getInstance().fetchGroups()); + navBarPromises.push(osparc.store.PollTasks.getInstance().fetchTasks()); + navBarPromises.push(osparc.store.Jobs.getInstance().fetchJobsLatest()); + navBarPromises.push(osparc.store.ConversationsSupport.getInstance().fetchConversations()); + Promise.all(navBarPromises) + .finally(() => navBar.populateLayout()); // Some resources request before building the main stack osparc.MaintenanceTracker.getInstance().startTracker(); @@ -68,8 +74,6 @@ qx.Class.define("osparc.desktop.MainPage", { preloadPromises.push(store.getAllClassifiers(true)); preloadPromises.push(osparc.store.Tags.getInstance().fetchTags()); preloadPromises.push(osparc.store.Products.getInstance().fetchUiConfig()); - preloadPromises.push(osparc.store.PollTasks.getInstance().fetchTasks()); - preloadPromises.push(osparc.store.Jobs.getInstance().fetchJobsLatest()); preloadPromises.push(osparc.data.Permissions.getInstance().fetchPermissions()); preloadPromises.push(osparc.data.Permissions.getInstance().fetchFunctionPermissions()); Promise.all(preloadPromises) diff --git a/services/static-webserver/client/source/class/osparc/jobs/JobsButton.js b/services/static-webserver/client/source/class/osparc/jobs/JobsButton.js index ce969b847001..7196bb60bc6a 100644 --- a/services/static-webserver/client/source/class/osparc/jobs/JobsButton.js +++ b/services/static-webserver/client/source/class/osparc/jobs/JobsButton.js @@ -53,7 +53,7 @@ qx.Class.define("osparc.jobs.JobsButton", { const logoContainer = new qx.ui.container.Composite(new qx.ui.layout.HBox().set({ alignY: "middle" })).set({ - paddingLeft: 5, + paddingLeft: 4, }); logoContainer.add(control); this._add(logoContainer, { diff --git a/services/static-webserver/client/source/class/osparc/navigation/NavigationBar.js b/services/static-webserver/client/source/class/osparc/navigation/NavigationBar.js index 23207efc46b6..75657b36fe1f 100644 --- a/services/static-webserver/client/source/class/osparc/navigation/NavigationBar.js +++ b/services/static-webserver/client/source/class/osparc/navigation/NavigationBar.js @@ -138,13 +138,13 @@ qx.Class.define("osparc.navigation.NavigationBar", { if (osparc.utils.DisabledPlugins.isRTCEnabled()) { this.getChildControl("avatar-group"); } + this.getChildControl("expiration-icon"); this.getChildControl("tasks-button"); if (osparc.product.Utils.showComputationalActivity()) { this.getChildControl("jobs-button"); } this.getChildControl("notifications-button"); - this.getChildControl("expiration-icon"); - this.getChildControl("help"); + this.getChildControl("help-button"); if (osparc.desktop.credits.Utils.areWalletsEnabled()) { this.getChildControl("credits-button"); } @@ -262,24 +262,6 @@ qx.Class.define("osparc.navigation.NavigationBar", { this.getChildControl("right-items").add(control); break; } - case "tasks-button": - control = new osparc.task.TasksButton().set({ - ...this.self().RIGHT_BUTTON_OPTS - }); - this.getChildControl("right-items").add(control); - break; - case "jobs-button": - control = new osparc.jobs.JobsButton().set({ - ...this.self().RIGHT_BUTTON_OPTS - }); - this.getChildControl("right-items").add(control); - break; - case "notifications-button": - control = new osparc.notification.NotificationsButton().set({ - ...this.self().RIGHT_BUTTON_OPTS - }); - this.getChildControl("right-items").add(control); - break; case "expiration-icon": { control = new qx.ui.basic.Image("@FontAwesome5Solid/hourglass-end/22").set({ visibility: "excluded", @@ -306,11 +288,28 @@ qx.Class.define("osparc.navigation.NavigationBar", { this.getChildControl("right-items").add(control); break; } - case "help": - control = this.__createHelpBtn().set({ + case "tasks-button": + control = new osparc.task.TasksButton().set({ + ...this.self().RIGHT_BUTTON_OPTS + }); + this.getChildControl("right-items").add(control); + break; + case "jobs-button": + control = new osparc.jobs.JobsButton().set({ + ...this.self().RIGHT_BUTTON_OPTS + }); + this.getChildControl("right-items").add(control); + break; + case "notifications-button": + control = new osparc.notification.NotificationsButton().set({ + ...this.self().RIGHT_BUTTON_OPTS + }); + this.getChildControl("right-items").add(control); + break; + case "help-button": + control = new osparc.support.SupportButton().set({ ...this.self().RIGHT_BUTTON_OPTS }); - osparc.utils.Utils.setIdToWidget(control, "helpNavigationBtn"); this.getChildControl("right-items").add(control); break; case "credits-button": @@ -361,14 +360,6 @@ qx.Class.define("osparc.navigation.NavigationBar", { }, this); }, - __createHelpBtn: function() { - const helpButton = new qx.ui.form.Button(null, "@FontAwesome5Regular/question-circle/24").set({ - backgroundColor: "transparent" - }); - helpButton.addListener("execute", () => osparc.support.SupportCenter.openWindow()); - return helpButton; - }, - __createLoginBtn: function() { const registerButton = new qx.ui.form.Button(this.tr("Log in"), "@FontAwesome5Solid/edit/14"); registerButton.addListener("execute", () => window.open(window.location.href, "_blank")); @@ -435,7 +426,7 @@ qx.Class.define("osparc.navigation.NavigationBar", { // right-items this.getChildControl("user-menu").exclude(); - this.getChildControl("help").exclude(); + this.getChildControl("help-button").exclude(); this.getChildControl("user-menu-compact").show(); } else { // left-items @@ -456,7 +447,7 @@ qx.Class.define("osparc.navigation.NavigationBar", { // right-items this.getChildControl("user-menu-compact").exclude(); - this.getChildControl("help").show(); + this.getChildControl("help-button").show(); this.getChildControl("user-menu").show(); } } diff --git a/services/static-webserver/client/source/class/osparc/notification/NotificationsButton.js b/services/static-webserver/client/source/class/osparc/notification/NotificationsButton.js index fa08ea2d66bf..0d7496327adf 100644 --- a/services/static-webserver/client/source/class/osparc/notification/NotificationsButton.js +++ b/services/static-webserver/client/source/class/osparc/notification/NotificationsButton.js @@ -49,7 +49,7 @@ qx.Class.define("osparc.notification.NotificationsButton", { const iconContainer = new qx.ui.container.Composite(new qx.ui.layout.HBox().set({ alignY: "middle", })).set({ - paddingLeft: 5, + paddingLeft: 4, }); iconContainer.add(control); this._add(iconContainer, { 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 6201f104db21..ccdf406228e5 100644 --- a/services/static-webserver/client/source/class/osparc/store/ConversationsSupport.js +++ b/services/static-webserver/client/source/class/osparc/store/ConversationsSupport.js @@ -26,6 +26,7 @@ qx.Class.define("osparc.store.ConversationsSupport", { }, events: { + "conversationAdded": "qx.event.type.Data", "conversationCreated": "qx.event.type.Data", "conversationDeleted": "qx.event.type.Data", }, @@ -37,6 +38,10 @@ qx.Class.define("osparc.store.ConversationsSupport", { }, members: { + getConversations: function() { + return Object.values(this.__conversationsCached); + }, + fetchConversations: function() { const params = { url: { @@ -52,8 +57,7 @@ qx.Class.define("osparc.store.ConversationsSupport", { conversationsData.sort((a, b) => new Date(b["created"]) - new Date(a["created"])); } conversationsData.forEach(conversationData => { - const conversation = new osparc.data.model.Conversation(conversationData); - this.__addToCache(conversation); + const conversation = this.__addToCache(conversationData); conversations.push(conversation); }); return conversations; @@ -73,8 +77,7 @@ qx.Class.define("osparc.store.ConversationsSupport", { }; return osparc.data.Resources.fetch("conversationsSupport", "getConversation", params) .then(conversationData => { - const conversation = new osparc.data.model.Conversation(conversationData); - this.__addToCache(conversation); + const conversation = this.__addToCache(conversationData); return conversation; }); }, @@ -92,8 +95,7 @@ qx.Class.define("osparc.store.ConversationsSupport", { }; return osparc.data.Resources.fetch("conversationsSupport", "postConversation", params) .then(conversationData => { - const conversation = new osparc.data.model.Conversation(conversationData); - this.__addToCache(conversation); + const conversation = this.__addToCache(conversationData); this.fireDataEvent("conversationCreated", conversation); return conversationData; }) @@ -115,28 +117,45 @@ qx.Class.define("osparc.store.ConversationsSupport", { .catch(err => osparc.FlashMessenger.logError(err)); }, - renameConversation: function(conversationId, name) { + __patchConversation: function(conversationId, data) { const params = { url: { conversationId, }, - data: { - name, - } + data, }; return osparc.data.Resources.fetch("conversationsSupport", "patchConversation", params); }, + renameConversation: function(conversationId, name) { + const patchData = { + name, + }; + return this.__patchConversation(conversationId, patchData); + }, + patchExtraContext: function(conversationId, extraContext) { - const params = { - url: { - conversationId, - }, - data: { - extraContext, - } + const patchData = { + extraContext, }; - return osparc.data.Resources.fetch("conversationsSupport", "patchConversation", params); + return this.__patchConversation(conversationId, patchData); + }, + + markAsRead: function(conversationId) { + const patchData = {}; + if (osparc.store.Groups.getInstance().amIASupportUser()) { + patchData["isReadBySupport"] = true; + } else { + patchData["isReadByUser"] = true; + } + return this.__patchConversation(conversationId, patchData); + }, + + markAsResolved: function(conversationId) { + const patchData = { + resolved: true, + }; + return this.__patchConversation(conversationId, patchData); }, fetchLastMessage: function(conversationId) { @@ -207,8 +226,16 @@ qx.Class.define("osparc.store.ConversationsSupport", { .catch(err => osparc.FlashMessenger.logError(err)); }, - __addToCache: function(conversation) { + __addToCache: function(conversationData) { + // check if already cached + if (conversationData["conversationId"] in this.__conversationsCached) { + return this.__conversationsCached[conversationData["conversationId"]]; + } + // add to cache + const conversation = new osparc.data.model.ConversationSupport(conversationData); this.__conversationsCached[conversation.getConversationId()] = conversation; + this.fireDataEvent("conversationAdded", conversation); + return conversation; }, } }); diff --git a/services/static-webserver/client/source/class/osparc/store/Groups.js b/services/static-webserver/client/source/class/osparc/store/Groups.js index a91598e6378d..39cc85927a34 100644 --- a/services/static-webserver/client/source/class/osparc/store/Groups.js +++ b/services/static-webserver/client/source/class/osparc/store/Groups.js @@ -22,7 +22,7 @@ qx.Class.define("osparc.store.Groups", { construct: function() { this.base(arguments); - this.groupsCached = []; + this.__groupsCached = []; }, properties: { @@ -72,16 +72,28 @@ qx.Class.define("osparc.store.Groups", { }, members: { - groupsCached: null, + __groupsCached: null, + __groupsPromiseCached: null, - __fetchGroups: function() { + fetchGroups: function() { if (osparc.auth.Data.getInstance().isGuest()) { return new Promise(resolve => { resolve([]); }); } + + if (this.__groupsPromiseCached) { + return this.__groupsPromiseCached; + } + + if (this.__groupsCached && this.__groupsCached.length) { + return new Promise(resolve => { + resolve(this.getOrganizations()); + }); + } + const useCache = false; - return osparc.data.Resources.get("organizations", {}, useCache) + return this.__groupsPromiseCached = osparc.data.Resources.get("organizations", {}, useCache) .then(resp => { const everyoneGroup = this.__addToGroupsCache(resp["all"], "everyone"); const productEveryoneGroup = this.__addToGroupsCache(resp["product"], "productEveryone"); @@ -118,6 +130,9 @@ qx.Class.define("osparc.store.Groups", { thumbnail: myAuthData.getAvatar(32), }) return orgs; + }) + .finally(() => { + this.__groupsPromiseCached = null; }); }, @@ -142,7 +157,7 @@ qx.Class.define("osparc.store.Groups", { fetchGroupsAndMembers: function() { return new Promise(resolve => { - this.__fetchGroups() + this.fetchGroups() .then(orgs => { // reset Users const usersStore = osparc.store.Users.getInstance(); @@ -495,12 +510,12 @@ qx.Class.define("osparc.store.Groups", { // CRUD GROUP MEMBERS __addToGroupsCache: function(groupData, groupType) { - let group = this.groupsCached.find(f => f.getGroupId() === groupData["gid"]); + let group = this.__groupsCached.find(f => f.getGroupId() === groupData["gid"]); if (!group) { group = new osparc.data.model.Group(groupData).set({ groupType }); - this.groupsCached.unshift(group); + this.__groupsCached.unshift(group); } return group; }, 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 a72ad5ab4833..c81e267b2779 100644 --- a/services/static-webserver/client/source/class/osparc/study/Conversation.js +++ b/services/static-webserver/client/source/class/osparc/study/Conversation.js @@ -47,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, this.__studyData["uuid"]); + const newConversation = new osparc.data.model.ConversationProject(data, this.__studyData["uuid"]); this.setConversation(newConversation); this.__postMessage(content); }); @@ -62,7 +62,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, this.__studyData["uuid"]); + const newConversation = new osparc.data.model.ConversationProject(data, this.__studyData["uuid"]); this.setConversation(newConversation); this.__postNotify(userGid); }); diff --git a/services/static-webserver/client/source/class/osparc/study/ConversationPage.js b/services/static-webserver/client/source/class/osparc/study/ConversationPage.js index 64c76471f07d..d311bfc0671c 100644 --- a/services/static-webserver/client/source/class/osparc/study/ConversationPage.js +++ b/services/static-webserver/client/source/class/osparc/study/ConversationPage.js @@ -48,7 +48,7 @@ qx.Class.define("osparc.study.ConversationPage", { this.__buildLayout(); if (conversationData) { - const conversation = new osparc.data.model.Conversation(conversationData, this.__studyData["uuid"]); + const conversation = new osparc.data.model.ConversationProject(conversationData, this.__studyData["uuid"]); this.setConversation(conversation); } @@ -124,7 +124,7 @@ qx.Class.define("osparc.study.ConversationPage", { // 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"]); + const conversation = new osparc.data.model.ConversationProject(data, this.__studyData["uuid"]); this.setConversation(conversation); this.getChildControl("button").setLabel(newLabel); }); @@ -178,15 +178,17 @@ qx.Class.define("osparc.study.ConversationPage", { }, __updateMessagesNumber: function() { - const nMessagesLabel = this.getChildControl("n-messages"); - const messages = this.getConversation().getMessages(); - const nMessages = messages.filter(msg => msg.getType() === "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")); + if (this.getConversation()) { + const nMessagesLabel = this.getChildControl("n-messages"); + const messages = this.getConversation().getMessages(); + const nMessages = messages.filter(msg => msg.getType() === "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/support/Conversation.js b/services/static-webserver/client/source/class/osparc/support/Conversation.js index 550c3af11555..b997a217ef93 100644 --- a/services/static-webserver/client/source/class/osparc/support/Conversation.js +++ b/services/static-webserver/client/source/class/osparc/support/Conversation.js @@ -86,7 +86,7 @@ qx.Class.define("osparc.support.Conversation", { .then(data => { // clone first, it will be reset when setting the conversation const bookACallInfo = this.__bookACallInfo ? Object.assign({}, this.__bookACallInfo) : null; - const newConversation = new osparc.data.model.Conversation(data); + const newConversation = new osparc.data.model.ConversationSupport(data); this.setConversation(newConversation); let prePostMessagePromise = new Promise((resolve) => resolve()); if (bookACallInfo) { @@ -133,6 +133,17 @@ qx.Class.define("osparc.support.Conversation", { this.__evaluateShareProject(); }, + // overridden + _messageAdded: function(message) { + this.base(arguments, message); + + // keep conversation read + const conversation = this.getConversation(); + if (conversation) { + conversation.setReadBy(true); + } + }, + __postMessage: function(content) { const conversationId = this.getConversation().getConversationId(); return osparc.store.ConversationsSupport.getInstance().postMessage(conversationId, content); 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 60e9015d6d25..0341065894a8 100644 --- a/services/static-webserver/client/source/class/osparc/support/ConversationListItem.js +++ b/services/static-webserver/client/source/class/osparc/support/ConversationListItem.js @@ -46,27 +46,107 @@ qx.Class.define("osparc.support.ConversationListItem", { event: "changeConversation", apply: "__applyConversation", }, + + subSubtitle: { + check : "String", + apply : "__applySubSubtitle", + nullable : true + }, }, members: { + _createChildControlImpl: function(id, hash) { + let control; + switch(id) { + case "sub-subtitle": + control = new qx.ui.basic.Label().set({ + font: "text-12", + selectable: true, + rich: true, + }); + this._add(control, { + row: 2, + column: 1 + }); + break; + case "unread-badge": + control = new osparc.ui.basic.Chip(this.tr("Unread")).set({ + statusColor: "success", + font: "text-12", + allowGrowY: false, + alignX: "right", + }); + this.getChildControl("third-column-layout").addAt(control, 1, { + flex: 1 + }); + break; + case "resolved-badge": + control = new osparc.ui.basic.Chip().set({ + font: "text-12", + allowGrowY: false, + alignX: "right", + }); + this.getChildControl("third-column-layout").addAt(control, 1, { + flex: 1 + }); + break; + } + return control || this.base(arguments, id); + }, + __applyConversation: function(conversation) { conversation.bind("nameAlias", this, "title"); - this.__populateWithLastMessage(); - conversation.addListener("changeLastMessage", this.__populateWithLastMessage, this); - - this.__populateWithFirstMessage(); - conversation.addListener("changeFirstMessage", this.__populateWithFirstMessage, this); + this.__lastMessageChanged(); + conversation.addListener("changeLastMessage", this.__lastMessageChanged, this); + + this.__firstMessageChanged(); + conversation.addListener("changeFirstMessage", this.__firstMessageChanged, this); + + conversation.bind("lastMessageCreatedAt", this, "role", { + converter: val => { + return osparc.utils.Utils.formatDateAndTime(val); + }, + }); + + const unreadBadge = this.getChildControl("unread-badge"); + const propName = osparc.store.Groups.getInstance().amIASupportUser() ? "readBySupport" : "readByUser"; + conversation.bind(propName, unreadBadge, "visibility", { + converter: val => val === false ? "visible" : "excluded" + }); + + /* + const resolvedBadge = this.getChildControl("resolved-badge"); + resolvedBadge.set({ + visibility: osparc.store.Groups.getInstance().amIASupportUser() ? "visible" : "excluded", + }); + conversation.bind("resolved", resolvedBadge, "label", { + converter: val => { + if (val === true) { + return this.tr("Resolved"); + } else if (val === false) { + return this.tr("Open"); + } + return ""; + } + }); + conversation.bind("resolved", resolvedBadge, "statusColor", { + converter: val => { + if (val === true) { + return "success"; + } else if (val === false) { + return "warning"; + } + return null; + } + }); + */ }, - __populateWithLastMessage: function() { + __lastMessageChanged: function() { const conversation = this.getConversation(); const lastMessage = conversation.getLastMessage(); if (lastMessage) { - const date = osparc.utils.Utils.formatDateAndTime(lastMessage.getCreated()); - this.set({ - role: date, - }); const userGroupId = lastMessage.getUserGroupId(); osparc.store.Users.getInstance().getUser(userGroupId) .then(user => { @@ -80,7 +160,7 @@ qx.Class.define("osparc.support.ConversationListItem", { } }, - __populateWithFirstMessage: function() { + __firstMessageChanged: function() { const conversation = this.getConversation(); const firstMessage = conversation.getFirstMessage(); if (firstMessage) { @@ -102,5 +182,13 @@ qx.Class.define("osparc.support.ConversationListItem", { }); } }, + + __applySubSubtitle: function(value) { + if (value === null) { + return; + } + const label = this.getChildControl("sub-subtitle"); + label.setValue(value); + }, }, }); diff --git a/services/static-webserver/client/source/class/osparc/support/ConversationPage.js b/services/static-webserver/client/source/class/osparc/support/ConversationPage.js index 6d190dfa9cbd..48474a66c84b 100644 --- a/services/static-webserver/client/source/class/osparc/support/ConversationPage.js +++ b/services/static-webserver/client/source/class/osparc/support/ConversationPage.js @@ -44,7 +44,7 @@ qx.Class.define("osparc.support.ConversationPage", { }, events: { - "showConversations": "qx.event.type.Event", + "backToConversations": "qx.event.type.Event", }, members: { @@ -63,11 +63,15 @@ qx.Class.define("osparc.support.ConversationPage", { } case "back-button": control = new qx.ui.form.Button().set({ - toolTipText: this.tr("Return to Messages"), + toolTipText: this.tr("Return to Conversations"), icon: "@FontAwesome5Solid/arrow-left/16", backgroundColor: "transparent" }); - control.addListener("execute", () => this.fireEvent("showConversations")); + control.addListener("execute", () => { + this.getConversation().setReadBy(true); + this.setConversation(null); + this.fireEvent("backToConversations"); + }); this.getChildControl("conversation-header-layout").addAt(control, 0); break; case "conversation-header-center-layout": @@ -127,27 +131,18 @@ qx.Class.define("osparc.support.ConversationPage", { this.getChildControl("buttons-layout").addAt(control, 2); break; } - case "open-ticket-link-button": { + case "resolve-case-button": { control = new qx.ui.form.Button().set({ - icon: "@FontAwesome5Solid/link/12", - toolTipText: this.tr("Open Ticket"), + icon: "@FontAwesome5Solid/check/12", + toolTipText: this.tr("Resolve Case"), + appearance: "strong-button", alignX: "center", alignY: "middle", }); + control.addListener("execute", () => this.__resolveCase()); this.getChildControl("buttons-layout").addAt(control, 3); break; } - case "set-appointment-button": { - control = new qx.ui.form.Button().set({ - icon: "@FontAwesome5Solid/clock/12", - toolTipText: this.tr("Set Appointment"), - alignX: "center", - alignY: "middle", - }); - control.addListener("execute", () => this.__openAppointmentDetails()); - this.getChildControl("buttons-layout").addAt(control, 4); - break; - } case "main-stack": control = new qx.ui.container.Stack(); this._add(control, { @@ -225,16 +220,15 @@ qx.Class.define("osparc.support.ConversationPage", { }, __applyConversation: function(conversation) { - const title = this.getChildControl("conversation-title"); - if (conversation) { - conversation.bind("nameAlias", title, "value"); - } - const extraContextLayout = this.getChildControl("conversation-extra-layout"); extraContextLayout.removeAll(); + if (conversation) { const amISupporter = osparc.store.Groups.getInstance().amIASupportUser(); + const title = this.getChildControl("conversation-title"); + conversation.bind("nameAlias", title, "value"); + const createExtraContextLabel = text => { return new qx.ui.basic.Label(text).set({ font: "text-12", @@ -270,14 +264,30 @@ qx.Class.define("osparc.support.ConversationPage", { }; updateExtraContext(); conversation.addListener("changeExtraContext", () => updateExtraContext(), this); + + this.getChildControl("rename-conversation-button"); + + const openProjectButton = this.getChildControl("open-project-button"); + if (conversation && conversation.getContextProjectId()) { + openProjectButton.setVisibility("visible"); + osparc.store.Study.getInstance().getOne(conversation.getContextProjectId()) + .then(() => openProjectButton.setEnabled(true)) + .catch(() => openProjectButton.setEnabled(false)); + } else { + openProjectButton.setVisibility("excluded"); + } + + this.getChildControl("copy-ticket-id-button"); + + if (amISupporter) { + const resolveCaseButton = this.getChildControl("resolve-case-button"); + conversation.bind("resolved", resolveCaseButton, "visibility", { + converter: val => val === false ? "visible" : "excluded" + }); + } } this.getChildControl("buttons-layout").setVisibility(conversation ? "visible" : "excluded"); - - this.getChildControl("rename-conversation-button"); - const openProjectButton = this.getChildControl("open-project-button"); - openProjectButton.setVisibility(conversation && conversation.getContextProjectId() ? "visible" : "excluded"); - this.getChildControl("copy-ticket-id-button"); }, __openProjectDetails: function() { @@ -302,15 +312,10 @@ qx.Class.define("osparc.support.ConversationPage", { } }, - __openAppointmentDetails: function() { - const win = new osparc.widget.DateTimeChooser(); - win.addListener("dateChanged", e => { - const newValue = e.getData()["newValue"]; - this.getConversation().setAppointment(newValue) - .catch(err => console.error(err)); - win.close(); - }, this); - win.open(); + __resolveCase: function() { + if (this.getConversation()) { + this.getConversation().markAsResolved(); + } }, __renameConversation: function() { diff --git a/services/static-webserver/client/source/class/osparc/support/Conversations.js b/services/static-webserver/client/source/class/osparc/support/Conversations.js index 0d4c83417e35..977796e90279 100644 --- a/services/static-webserver/client/source/class/osparc/support/Conversations.js +++ b/services/static-webserver/client/source/class/osparc/support/Conversations.js @@ -25,28 +25,106 @@ qx.Class.define("osparc.support.Conversations", { this._setLayout(new qx.ui.layout.VBox(10)); this.__conversationListItems = []; + this.__filterButtons = []; + this.__filterButtons.push(this.getChildControl("filter-all-button")); + this.__filterButtons.push(this.getChildControl("filter-unread-button")); + /* + if (osparc.store.Groups.getInstance().amIASupportUser()) { + this.__filterButtons.push(this.getChildControl("filter-open-button")); + } + */ + this.__fetchConversations(); this.__listenToNewConversations(); }, + properties: { + currentFilter: { + check: [ + "all", + "unread", + "open", + ], + init: "all", + event: "changeCurrentFilter", + }, + }, + events: { "openConversation": "qx.event.type.Data", }, + statics: { + FILTER_BUTTON_AESTHETIC: { + appearance: "filter-toggle-button", + allowGrowX: false, + paddingTop: 4, + paddingBottom: 4, + }, + }, + members: { __conversationListItems: null, _createChildControlImpl: function(id) { let control; switch (id) { + case "filters-layout": + control = new qx.ui.container.Composite(new qx.ui.layout.HBox(4)); + this._addAt(control, 0); + break; + case "filter-all-button": + control = new qx.ui.form.ToggleButton(this.tr("All")); + control.set({ + value: true, + toolTipText: this.tr("Show all conversations"), + ...this.self().FILTER_BUTTON_AESTHETIC, + }); + control.addListener("execute", () => { + this.setCurrentFilter("all"); + this.__applyCurrentFilter("all"); + }); + this.getChildControl("filters-layout").add(control); + break; + case "filter-unread-button": + control = new qx.ui.form.ToggleButton(this.tr("Unread")); + control.set({ + toolTipText: this.tr("Show only unread conversations"), + ...this.self().FILTER_BUTTON_AESTHETIC, + }); + control.addListener("execute", () => { + this.setCurrentFilter("unread"); + this.__applyCurrentFilter("unread"); + }); + this.getChildControl("filters-layout").add(control); + break; + case "filter-open-button": + control = new qx.ui.form.ToggleButton(this.tr("Open")); + control.set({ + toolTipText: this.tr("Show only open conversations"), + ...this.self().FILTER_BUTTON_AESTHETIC, + }); + control.addListener("execute", () => { + this.setCurrentFilter("open"); + this.__applyCurrentFilter("open"); + }); + this.getChildControl("filters-layout").add(control); + break; case "loading-button": control = new osparc.ui.form.FetchButton(); - this._add(control); + this._addAt(control, 1); + break; + case "no-messages-label": + control = new qx.ui.basic.Label().set({ + alignX: "center", + visibility: "excluded", + }); + this._addAt(control, 2); break; case "conversations-layout": control = new qx.ui.container.Composite(new qx.ui.layout.VBox(5)); - this._add(control, { + this._addAt(control, 3, { flex: 1 }); break; @@ -55,6 +133,64 @@ qx.Class.define("osparc.support.Conversations", { return control || this.base(arguments, id); }, + __applyCurrentFilter: function(filter) { + this.getChildControl("no-messages-label").exclude(); + + this.__filterButtons.forEach(button => { + button.setValue(false); + }); + switch (filter) { + case "all": + this.getChildControl("filter-all-button").setValue(true); + break; + case "unread": + this.getChildControl("filter-unread-button").setValue(true); + break; + case "open": + this.getChildControl("filter-open-button").setValue(true); + break; + } + + this.__conversationListItems.forEach(conversationItem => { + const conversation = conversationItem.getConversation(); + switch (filter) { + case "all": + conversationItem.show(); + break; + case "unread": + conversation.getReadBy() ? conversationItem.exclude() : conversationItem.show(); + break; + case "open": + if (conversation.getResolved() === false) { + conversationItem.show(); + } else { + conversationItem.exclude(); + } + break; + } + }); + + const hasVisibleConversations = this.__conversationListItems.some(conversationItem => conversationItem.isVisible()); + if (!hasVisibleConversations) { + let msg = ""; + switch (filter) { + case "all": + msg = this.tr("No conversations yet"); + break; + case "unread": + msg = this.tr("No unread conversations"); + break; + case "open": + msg = this.tr("No open conversations"); + break; + } + this.getChildControl("no-messages-label").set({ + value: msg, + visibility: "visible", + }); + } + }, + __getConversationItem: function(conversationId) { return this.__conversationListItems.find(conversation => conversation.getConversation().getConversationId() === conversationId); }, @@ -68,21 +204,24 @@ qx.Class.define("osparc.support.Conversations", { if (conversations.length) { conversations.forEach(conversation => this.__addConversation(conversation)); } + this.__sortConversations(); }) .finally(() => { loadMoreButton.setFetching(false); loadMoreButton.exclude(); + this.__applyCurrentFilter(this.getCurrentFilter()); }); }, __listenToNewConversations: function() { osparc.store.ConversationsSupport.getInstance().addListener("conversationCreated", e => { const conversation = e.getData(); - this.__addConversation(conversation, 0); + this.__addConversation(conversation); + this.__sortConversations(); }); }, - __addConversation: function(conversation, position) { + __addConversation: function(conversation) { // ignore it if it was already there const conversationId = conversation.getConversationId(); const conversationItemFound = this.__getConversationItem(conversationId); @@ -93,15 +232,26 @@ qx.Class.define("osparc.support.Conversations", { const conversationListItem = new osparc.support.ConversationListItem(); conversationListItem.setConversation(conversation); conversationListItem.addListener("tap", () => this.fireDataEvent("openConversation", conversationId, this)); - const conversationsLayout = this.getChildControl("conversations-layout"); - if (position !== undefined) { - conversationsLayout.addAt(conversationListItem, position); - } else { - conversationsLayout.add(conversationListItem); - } + conversation.addListener("changeLastMessageCreatedAt", () => this.__sortConversations(), this); + const eventName = osparc.store.Groups.getInstance().amIASupportUser() ? "changeReadBySupport" : "changeReadByUser"; + conversation.addListener(eventName, () => this.__applyCurrentFilter(this.getCurrentFilter()), this); + conversation.addListener("changeResolved", () => this.__applyCurrentFilter(this.getCurrentFilter()), this); this.__conversationListItems.push(conversationListItem); - return conversationListItem; }, + + __sortConversations: function() { + const conversationsLayout = this.getChildControl("conversations-layout"); + conversationsLayout.removeAll(); + // sort them by modified date (newest first) + this.__conversationListItems.sort((a, b) => { + const aConversation = a.getConversation(); + const bConversation = b.getConversation(); + const aDate = aConversation.getLastMessageCreatedAt() || aConversation.getModified(); + const bDate = bConversation.getLastMessageCreatedAt() || bConversation.getModified(); + return bDate - aDate; + }); + this.__conversationListItems.forEach(item => conversationsLayout.add(item)); + }, }, }); diff --git a/services/static-webserver/client/source/class/osparc/support/SupportButton.js b/services/static-webserver/client/source/class/osparc/support/SupportButton.js new file mode 100644 index 000000000000..2c09a2a54acf --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/support/SupportButton.js @@ -0,0 +1,129 @@ +/* ************************************************************************ + + 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.support.SupportButton", { + extend: qx.ui.core.Widget, + + construct: function() { + this.base(arguments); + + this._setLayout(new qx.ui.layout.Canvas()); + + this.set({ + toolTipText: this.tr("Help & Support"), + }); + + osparc.utils.Utils.setIdToWidget(this, "helpNavigationBtn"); + + this.getChildControl("icon"); + + this.__listenToStore(); + this.__updateButton(); + + this.addListener("tap", () => { + const supportCenter = osparc.support.SupportCenter.openWindow(); + if (this.isUnreadMessages()) { + supportCenter.showConversations(); + } + }); + }, + + properties: { + unreadMessages: { + check: "Boolean", + init: null, + nullable: false, + event: "changeUnreadMessages", + apply: "__applyUnreadMessages", + }, + }, + + members: { + __notificationsContainer: null, + + _createChildControlImpl: function(id) { + let control; + switch (id) { + case "icon": { + control = new qx.ui.basic.Image("@FontAwesome5Regular/question-circle/24"); + const iconContainer = new qx.ui.container.Composite(new qx.ui.layout.HBox().set({ + alignY: "middle", + })).set({ + paddingLeft: 4, + }); + iconContainer.add(control); + this._add(iconContainer, { + height: "100%" + }); + break; + } + case "is-active-icon-outline": + control = new qx.ui.basic.Image("@FontAwesome5Solid/circle/12").set({ + textColor: osparc.navigation.NavigationBar.BG_COLOR, + }); + this._add(control, { + bottom: -4, + right: -4, + }); + break; + case "is-active-icon": + control = new qx.ui.basic.Image("@FontAwesome5Solid/circle/8").set({ + textColor: "strong-main", + }); + this._add(control, { + bottom: -2, + right: -2, + }); + break; + } + return control || this.base(arguments, id); + }, + + __listenToStore: function() { + const eventName = osparc.store.Groups.getInstance().amIASupportUser() ? "changeReadBySupport" : "changeReadByUser"; + const conversationsStore = osparc.store.ConversationsSupport.getInstance(); + const cachedConversations = conversationsStore.getConversations(); + cachedConversations.forEach(conversation => conversation.addListener(eventName, () => this.__updateButton(), this)); + conversationsStore.addListener("conversationAdded", e => { + const conversation = e.getData(); + conversation.addListener(eventName, e => { + this.__updateButton(); + }, this); + this.__updateButton(); + }, this); + }, + + __updateButton: function() { + const propName = osparc.store.Groups.getInstance().amIASupportUser() ? "readBySupport" : "readByUser"; + const conversationsStore = osparc.store.ConversationsSupport.getInstance(); + const cachedConversations = conversationsStore.getConversations(); + const unread = cachedConversations.some(conversation => Boolean(conversation.get(propName) === false)); + this.setUnreadMessages(unread); + }, + + __applyUnreadMessages: function(unread) { + [ + this.getChildControl("is-active-icon-outline"), + this.getChildControl("is-active-icon"), + ].forEach(control => { + control.set({ + visibility: unread ? "visible" : "excluded" + }); + }); + }, + } +}); diff --git a/services/static-webserver/client/source/class/osparc/support/SupportCenter.js b/services/static-webserver/client/source/class/osparc/support/SupportCenter.js index 76bde6c257ed..b065de83b729 100644 --- a/services/static-webserver/client/source/class/osparc/support/SupportCenter.js +++ b/services/static-webserver/client/source/class/osparc/support/SupportCenter.js @@ -146,7 +146,7 @@ qx.Class.define("osparc.support.SupportCenter", { break; case "conversation-page": control = new osparc.support.ConversationPage(); - control.addListener("showConversations", () => this.showConversations(), this); + control.addListener("backToConversations", () => this.showConversations(), this); this.getChildControl("conversations-stack").add(control); break; } @@ -182,7 +182,17 @@ qx.Class.define("osparc.support.SupportCenter", { __showConversation: function() { this.__selectConversationsStackPage(); - this.getChildControl("conversations-stack").setSelection([this.getChildControl("conversation-page")]); + const conversationPage = this.getChildControl("conversation-page"); + this.getChildControl("conversations-stack").setSelection([conversationPage]); + + const conversation = conversationPage.getConversation(); + if (conversation) { + if (osparc.store.Groups.getInstance().amIASupportUser() && conversation.isReadBySupport() === false) { + conversation.markAsRead(); + } else if (!osparc.store.Groups.getInstance().amIASupportUser() && conversation.isReadByUser() === false) { + conversation.markAsRead(); + } + } }, openConversation: function(conversationId) { diff --git a/services/static-webserver/client/source/class/osparc/ui/list/ListItem.js b/services/static-webserver/client/source/class/osparc/ui/list/ListItem.js index 19bb4d2473a0..2c8f661ef6b0 100644 --- a/services/static-webserver/client/source/class/osparc/ui/list/ListItem.js +++ b/services/static-webserver/client/source/class/osparc/ui/list/ListItem.js @@ -108,12 +108,6 @@ qx.Class.define("osparc.ui.list.ListItem", { nullable : true }, - subSubtitle: { - check : "String", - apply : "__applySubSubtitle", - nullable : true - }, - role: { check : "String", apply : "__applyRole", @@ -121,6 +115,10 @@ qx.Class.define("osparc.ui.list.ListItem", { } }, + statics: { + MAX_ROWS: 3, + }, + members: { // eslint-disable-line qx-rules/no-refs-in-members // overridden _forwardStates: { @@ -152,7 +150,7 @@ qx.Class.define("osparc.ui.list.ListItem", { this._add(control, { row: 0, column: 0, - rowSpan: 3 + rowSpan: this.self().MAX_ROWS, }); break; case "title": @@ -189,26 +187,22 @@ qx.Class.define("osparc.ui.list.ListItem", { column: 1 }); break; - case "sub-subtitle": - control = new qx.ui.basic.Label().set({ - font: "text-12", - selectable: true, - rich: true, - }); + case "third-column-layout": + control = new qx.ui.container.Composite(new qx.ui.layout.VBox(5).set({ + alignY: "middle" + })); this._add(control, { - row: 2, - column: 1 + row: 0, + column: 2, + rowSpan: this.self().MAX_ROWS, }); break; case "role": control = new qx.ui.basic.Label().set({ font: "text-13", - alignY: "middle" }); - this._add(control, { - row: 0, - column: 2, - rowSpan: 3 + this.getChildControl("third-column-layout").addAt(control, 0, { + flex: 1 }); break; } @@ -264,14 +258,6 @@ qx.Class.define("osparc.ui.list.ListItem", { label.setValue(value); }, - __applySubSubtitle: function(value) { - if (value === null) { - return; - } - const label = this.getChildControl("sub-subtitle"); - label.setValue(value); - }, - __applyRole: function(value) { if (value === null) { return; diff --git a/services/static-webserver/client/source/class/osparc/viewer/NavigationBar.js b/services/static-webserver/client/source/class/osparc/viewer/NavigationBar.js index a6083d60bcda..e7243ae44249 100644 --- a/services/static-webserver/client/source/class/osparc/viewer/NavigationBar.js +++ b/services/static-webserver/client/source/class/osparc/viewer/NavigationBar.js @@ -26,7 +26,7 @@ qx.Class.define("osparc.viewer.NavigationBar", { flex: 1 }); - this.getChildControl("help"); + this.getChildControl("help-button"); this.getChildControl("theme-switch"); } }