diff --git a/services/static-webserver/client/Manifest.json b/services/static-webserver/client/Manifest.json
index 64089c078666..743a52c5c2fb 100644
--- a/services/static-webserver/client/Manifest.json
+++ b/services/static-webserver/client/Manifest.json
@@ -35,6 +35,7 @@
"css": [
"jsontreeviewer/jsonTree.css",
"hint/hint.css",
+ "marked/markdown.css",
"common/common.css"
]
},
diff --git a/services/static-webserver/client/source/class/osparc/conversation/AddMessage.js b/services/static-webserver/client/source/class/osparc/conversation/AddMessage.js
index 0d37b8ae6e31..a3b6e7a2b6cd 100644
--- a/services/static-webserver/client/source/class/osparc/conversation/AddMessage.js
+++ b/services/static-webserver/client/source/class/osparc/conversation/AddMessage.js
@@ -83,17 +83,12 @@ qx.Class.define("osparc.conversation.AddMessage", {
}
case "comment-field":
control = new osparc.editor.MarkdownEditor();
- control.addListener("keydown", e => {
- if (e.isCtrlPressed() && e.getKeyIdentifier() === "Enter") {
- this.addComment();
- e.stopPropagation();
- e.preventDefault();
- }
- }, this);
- control.getChildControl("buttons").exclude();
+ control.addListener("textChanged", () => this.__addCommentPressed(), this);
+ control.setCompact(true);
control.getChildControl("text-area").getContentElement().setStyles({
"border-top-right-radius": "0px", // no roundness there to match the arrow button
});
+ // make it more compact
this.getChildControl("add-comment-layout").add(control, {
flex: 1
});
@@ -102,7 +97,8 @@ qx.Class.define("osparc.conversation.AddMessage", {
control = new qx.ui.form.Button(null, "@FontAwesome5Solid/arrow-up/16").set({
backgroundColor: "input_background",
allowGrowX: false,
- alignX: "right"
+ alignX: "right",
+ alignY: "middle",
});
control.getContentElement().setStyles({
"border-bottom": "1px solid " + qx.theme.manager.Color.getInstance().resolve("default-button-active"),
@@ -161,9 +157,6 @@ qx.Class.define("osparc.conversation.AddMessage", {
// edit mode
const commentField = this.getChildControl("comment-field");
commentField.setText(message["content"]);
-
- const addMessageButton = this.getChildControl("add-comment-button");
- addMessageButton.setLabel(this.tr("Edit message"));
}
},
diff --git a/services/static-webserver/client/source/class/osparc/conversation/MessageUI.js b/services/static-webserver/client/source/class/osparc/conversation/MessageUI.js
index fc3d5ea21c96..be5c1129e1d3 100644
--- a/services/static-webserver/client/source/class/osparc/conversation/MessageUI.js
+++ b/services/static-webserver/client/source/class/osparc/conversation/MessageUI.js
@@ -28,9 +28,7 @@ qx.Class.define("osparc.conversation.MessageUI", {
this.__studyData = studyData;
- const layout = new qx.ui.layout.Grid(12, 2);
- layout.setColumnFlex(1, 1); // content
- this._setLayout(layout);
+ this._setLayout(new qx.ui.layout.HBox(10));
this.setPadding(5);
this.set({
@@ -69,22 +67,22 @@ qx.Class.define("osparc.conversation.MessageUI", {
case "thumbnail":
control = new osparc.ui.basic.UserThumbnail(32).set({
marginTop: 4,
+ alignY: "top",
});
- this._add(control, {
- row: 0,
- column: isMyMessage ? 2 : 0,
- rowSpan: 2,
- });
+ this._addAt(control, isMyMessage ? 1 : 0);
+ break;
+ case "main-layout":
+ control = new qx.ui.container.Composite(new qx.ui.layout.VBox(2).set({
+ alignX: isMyMessage ? "right" : "left"
+ }));
+ this._addAt(control, isMyMessage ? 0 : 1, { flex: 1});
break;
case "header-layout":
control = new qx.ui.container.Composite(new qx.ui.layout.HBox(5).set({
alignX: isMyMessage ? "right" : "left"
}));
control.addAt(new qx.ui.basic.Label("-"), 1);
- this._add(control, {
- row: 0,
- column: 1
- });
+ this.getChildControl("main-layout").addAt(control, 0);
break;
case "user-name":
control = new qx.ui.basic.Label().set({
@@ -100,25 +98,21 @@ qx.Class.define("osparc.conversation.MessageUI", {
});
this.getChildControl("header-layout").addAt(control, isMyMessage ? 0 : 2);
break;
- case "message-content":
- control = new osparc.ui.markdown.Markdown().set({
- noMargin: true,
- allowGrowX: true,
- });
- control.getContentElement().setStyles({
- "text-align": isMyMessage ? "right" : "left",
- });
- this._add(control, {
- row: 1,
- column: 1,
+ case "message-bubble":
+ control = new qx.ui.container.Composite(new qx.ui.layout.VBox().set({
+ alignX: isMyMessage ? "right" : "left"
+ })).set({
+ decorator: "chat-bubble",
+ allowGrowX: false,
+ padding: 8,
});
+ const bubbleStyle = isMyMessage ? { "border-top-right-radius": "0px" } : { "border-top-left-radius": "0px" };
+ control.getContentElement().setStyles(bubbleStyle);
+ this.getChildControl("main-layout").addAt(control, 1);
break;
- case "spacer":
- control = new qx.ui.core.Spacer();
- this._add(control, {
- row: 1,
- column: isMyMessage ? 0 : 2,
- });
+ case "message-content":
+ control = new osparc.ui.markdown.MarkdownChat();
+ this.getChildControl("message-bubble").add(control);
break;
case "menu-button": {
const buttonSize = 22;
@@ -129,14 +123,10 @@ qx.Class.define("osparc.conversation.MessageUI", {
allowGrowY: false,
marginTop: 4,
alignY: "top",
- icon: "@FontAwesome5Solid/ellipsis-v/14",
+ icon: "@FontAwesome5Solid/ellipsis-v/12",
focusable: false
});
- this._add(control, {
- row: 0,
- column: 3,
- rowSpan: 2,
- });
+ this._addAt(control, 2);
break;
}
}
@@ -145,13 +135,6 @@ qx.Class.define("osparc.conversation.MessageUI", {
},
__applyMessage: function(message) {
- const isMyMessage = this.self().isMyMessage(message);
- this._getLayout().setColumnFlex(isMyMessage ? 0 : 2, 3); // spacer
-
- const thumbnail = this.getChildControl("thumbnail");
-
- const userName = this.getChildControl("user-name");
-
const createdDateData = new Date(message["created"]);
const createdDate = osparc.utils.Utils.formatDateAndTime(createdDateData);
const lastUpdate = this.getChildControl("last-updated");
@@ -166,6 +149,8 @@ qx.Class.define("osparc.conversation.MessageUI", {
const messageContent = this.getChildControl("message-content");
messageContent.setValue(message["content"]);
+ const thumbnail = this.getChildControl("thumbnail");
+ const userName = this.getChildControl("user-name");
if (message["userGroupId"] === "system") {
userName.setValue("Support");
} else {
@@ -180,8 +165,6 @@ qx.Class.define("osparc.conversation.MessageUI", {
});
}
- this.getChildControl("spacer");
-
if (this.self().isMyMessage(message)) {
const menuButton = this.getChildControl("menu-button");
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 a28bd4d4b699..19d0372cc943 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
@@ -293,6 +293,13 @@ qx.Class.define("osparc.data.model.Conversation", {
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"];
diff --git a/services/static-webserver/client/source/class/osparc/editor/MarkdownEditor.js b/services/static-webserver/client/source/class/osparc/editor/MarkdownEditor.js
index 79c8e94f5e51..8b12fa66a815 100644
--- a/services/static-webserver/client/source/class/osparc/editor/MarkdownEditor.js
+++ b/services/static-webserver/client/source/class/osparc/editor/MarkdownEditor.js
@@ -42,6 +42,14 @@ qx.Class.define("osparc.editor.MarkdownEditor", {
});
},
+ properties: {
+ compact: {
+ check: "Boolean",
+ init: null,
+ apply: "__applyCompact",
+ }
+ },
+
members: {
_createChildControlImpl: function(id) {
let control;
@@ -69,6 +77,12 @@ qx.Class.define("osparc.editor.MarkdownEditor", {
}
}
return control || this.base(arguments, id);
- }
+ },
+
+ __applyCompact: function(value) {
+ this.getChildControl("buttons").setVisibility(value ? "excluded" : "visible");
+ this.getChildControl("tabs").getChildControl("bar").setVisibility(value ? "excluded" : "visible");
+ this.getChildControl("subtitle").setVisibility(value ? "excluded" : "visible");
+ },
}
});
diff --git a/services/static-webserver/client/source/class/osparc/editor/TextEditor.js b/services/static-webserver/client/source/class/osparc/editor/TextEditor.js
index 1acf44f50b52..277ea3786b42 100644
--- a/services/static-webserver/client/source/class/osparc/editor/TextEditor.js
+++ b/services/static-webserver/client/source/class/osparc/editor/TextEditor.js
@@ -32,6 +32,15 @@ qx.Class.define("osparc.editor.TextEditor", {
}
this.__addButtons();
+
+ this.addListener("keydown", e => {
+ if (e.isCtrlPressed() && e.getKeyIdentifier() === "Enter") {
+ const text = this.getChildControl("text-area").getValue();
+ this.fireDataEvent("textChanged", text);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ }, this);
},
events: {
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 3907e5930a86..a7f3c37eaf69 100644
--- a/services/static-webserver/client/source/class/osparc/study/Conversation.js
+++ b/services/static-webserver/client/source/class/osparc/study/Conversation.js
@@ -342,6 +342,9 @@ qx.Class.define("osparc.study.Conversation", {
switch (message["type"]) {
case "MESSAGE":
control = new osparc.conversation.MessageUI(message, this.__studyData);
+ control.getChildControl("message-content").set({
+ measurerMaxWidth: 400,
+ });
control.addListener("messageUpdated", e => this.updateMessage(e.getData()));
control.addListener("messageDeleted", e => this.deleteMessage(e.getData()));
break;
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 93031b0063f3..81fb93a6c79f 100644
--- a/services/static-webserver/client/source/class/osparc/support/Conversation.js
+++ b/services/static-webserver/client/source/class/osparc/support/Conversation.js
@@ -105,9 +105,6 @@ qx.Class.define("osparc.support.Conversation", {
this.bind("conversation", control, "conversationId", {
converter: conversation => conversation ? conversation.getConversationId() : null
});
- // make it more compact
- control.getChildControl("comment-field").getChildControl("tabs").getChildControl("bar").exclude();
- control.getChildControl("comment-field").getChildControl("subtitle").exclude();
this._addAt(control, 4);
break;
case "share-project-layout":
@@ -155,6 +152,7 @@ qx.Class.define("osparc.support.Conversation", {
// make these checks first, setConversation will reload messages
if (
this.__messages.length === 1 &&
+ this.__messages[0]["systemMessageType"] &&
this.__messages[0]["systemMessageType"] === osparc.support.Conversation.SYSTEM_MESSAGE_TYPE.BOOK_A_CALL
) {
isBookACall = true;
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 c6382edfa733..429c3702a505 100644
--- a/services/static-webserver/client/source/class/osparc/support/ConversationPage.js
+++ b/services/static-webserver/client/source/class/osparc/support/ConversationPage.js
@@ -196,7 +196,6 @@ qx.Class.define("osparc.support.ConversationPage", {
return new qx.ui.basic.Label(text).set({
font: "text-12",
textColor: "text-disabled",
- rich: true,
allowGrowX: true,
selectable: true,
});
@@ -207,31 +206,24 @@ qx.Class.define("osparc.support.ConversationPage", {
if (extraContext && Object.keys(extraContext).length) {
const ticketIdLabel = createExtraContextLabel(`Ticket ID: ${osparc.utils.Utils.uuidToShort(conversation.getConversationId())}`);
extraContextLayout.add(ticketIdLabel);
- const contextProjectId = conversation.getContextProjectId();
- if (contextProjectId && amISupporter) {
- const projectIdLabel = createExtraContextLabel(`Project ID: ${osparc.utils.Utils.uuidToShort(contextProjectId)}`);
- extraContextLayout.add(projectIdLabel);
- }
- /*
- const appointment = conversation.getAppointment();
- if (appointment) {
- const appointmentLabel = createExtraContextLabel();
- let appointmentText = "Appointment: ";
- if (appointment === "requested") {
- // still pending
- appointmentText += appointment;
- } else {
- // already set
- appointmentText += osparc.utils.Utils.formatDateAndTime(new Date(appointment));
- appointmentLabel.set({
- cursor: "pointer",
- toolTipText: osparc.utils.Utils.formatDateWithCityAndTZ(new Date(appointment)),
+ if (amISupporter) {
+ const fogbugzLink = conversation.getFogbugzLink();
+ if (fogbugzLink) {
+ const text = "Fogbugz Case: " + fogbugzLink.split("/").pop();
+ const fogbugzLabel = new osparc.ui.basic.LinkLabel(text, fogbugzLink).set({
+ font: "link-label-12",
+ textColor: "text-disabled",
+ allowGrowX: true,
});
+ extraContextLayout.add(fogbugzLabel);
+ }
+ const contextProjectId = conversation.getContextProjectId();
+ if (contextProjectId) {
+ const projectIdLabel = createExtraContextLabel(`Project ID: ${osparc.utils.Utils.uuidToShort(contextProjectId)}`);
+ extraContextLayout.add(projectIdLabel);
}
- appointmentLabel.setValue(appointmentText);
- extraContextLayout.add(appointmentLabel);
+
}
- */
}
};
updateExtraContext();
diff --git a/services/static-webserver/client/source/class/osparc/theme/Decoration.js b/services/static-webserver/client/source/class/osparc/theme/Decoration.js
index 0e295ac3c168..5d0fee783ee1 100644
--- a/services/static-webserver/client/source/class/osparc/theme/Decoration.js
+++ b/services/static-webserver/client/source/class/osparc/theme/Decoration.js
@@ -29,8 +29,9 @@ qx.Theme.define("osparc.theme.Decoration", {
"chat-bubble": {
style: {
radius: 4,
- width: 1,
- color: "text-disabled"
+ // width: 1,
+ // color: "text-disabled",
+ backgroundColor: "background-main-2",
}
},
diff --git a/services/static-webserver/client/source/class/osparc/ui/markdown/Markdown.js b/services/static-webserver/client/source/class/osparc/ui/markdown/Markdown.js
index c7b944ee3f06..765f6287c47e 100644
--- a/services/static-webserver/client/source/class/osparc/ui/markdown/Markdown.js
+++ b/services/static-webserver/client/source/class/osparc/ui/markdown/Markdown.js
@@ -92,7 +92,7 @@ qx.Class.define("osparc.ui.markdown.Markdown", {
if (linkRepresentation.type === "text") {
linkHtml += linkRepresentation.text;
} else if (linkRepresentation.type === "image") {
- linkHtml += ``;
+ linkHtml += `
`;
}
}
linkHtml += ``;
diff --git a/services/static-webserver/client/source/class/osparc/ui/markdown/MarkdownChat.js b/services/static-webserver/client/source/class/osparc/ui/markdown/MarkdownChat.js
new file mode 100644
index 000000000000..a27463a05d35
--- /dev/null
+++ b/services/static-webserver/client/source/class/osparc/ui/markdown/MarkdownChat.js
@@ -0,0 +1,208 @@
+/*
+ * 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)
+ */
+
+/**
+ * @asset(marked/marked.min.js)
+ * @asset(marked/markdown.css)
+ * @ignore(marked)
+ */
+
+/* global marked */
+
+qx.Class.define("osparc.ui.markdown.MarkdownChat", {
+ extend: qx.ui.embed.Html,
+
+ /**
+ * @param {String} markdown Plain text accepting markdown syntax
+ */
+ construct: function(markdown) {
+ this.base(arguments);
+
+ this.set({
+ allowGrowX: false,
+ allowGrowY: true,
+ overflowX: "hidden", // hide scrollbars
+ overflowY: "hidden", // hide scrollbars
+ });
+
+ const markdownCssUri = qx.util.ResourceManager.getInstance().toUri("marked/markdown.css");
+ qx.module.Css.includeStylesheet(markdownCssUri);
+
+ this.__loadMarked = new Promise((resolve, reject) => {
+ if (typeof marked === "function") {
+ resolve(marked);
+ } else {
+ const loader = new qx.util.DynamicScriptLoader([
+ "marked/marked.min.js"
+ ]);
+ loader.addListenerOnce("ready", () => resolve(marked), this);
+ loader.addListenerOnce("failed", e =>
+ reject(Error(`Failed to load ${e.getData()}`))
+ );
+ loader.start();
+ }
+ });
+
+ if (markdown) {
+ this.setValue(markdown);
+ }
+
+ this.addListenerOnce("appear", () => {
+ this.getContentElement().addClass("osparc-markdown");
+ this.__scheduleResize(); // first paint sizing
+ });
+ },
+
+ properties: {
+ /**
+ * Holds the raw markdown text and updates the label's {@link #value} whenever new markdown arrives.
+ */
+ value: {
+ check: "String",
+ apply: "__applyMarkdown"
+ },
+
+ measurerMaxWidth: {
+ check: "Integer",
+ init: 220,
+ nullable: true,
+ },
+ },
+
+ events: {
+ "resized": "qx.event.type.Event",
+ },
+
+ statics: {
+ MD_ROOT: "osparc-md-root",
+ MD_MEASURE: "osparc-md-measure",
+ },
+
+ members: {
+ __loadMarked: null,
+
+ /**
+ * Apply function for the markdown property. Compiles the markdown text to HTML and applies it to the value property of the label.
+ * @param {String} value Plain text accepting markdown syntax.
+ */
+ __applyMarkdown: function(value = "") {
+ this.__loadMarked.then(() => {
+ const renderer = {
+ link(link) {
+ const linkColor = qx.theme.manager.Color.getInstance().resolve("link");
+ let linkHtml = ``
+ if (link.tokens && link.tokens.length) {
+ const linkRepresentation = link.tokens[0];
+ if (linkRepresentation.type === "text") {
+ linkHtml += linkRepresentation.text;
+ } else if (linkRepresentation.type === "image") {
+ linkHtml += `
`;
+ }
+ }
+ linkHtml += ``;
+ return linkHtml;
+ }
+ };
+ marked.use({ renderer });
+ // By default, Markdown requires two spaces at the end of a line or a blank line between paragraphs to produce a line break.
+ // With this, a single line break (Enter) in your Markdown input will render as a
in HTML.
+ marked.setOptions({ breaks: true });
+
+ const html = marked.parse(value);
+
+ const safeHtml = osparc.wrapper.DOMPurify.getInstance().sanitize(html);
+
+ // flow-root prevents margin collapsing; inline style avoids extra stylesheet juggling
+ const max = this.getMeasurerMaxWidth() || 220;
+ const mdRoot = `
+