Skip to content

Commit b725092

Browse files
committed
Conversation
1 parent 9f2822c commit b725092

File tree

2 files changed

+352
-5
lines changed

2 files changed

+352
-5
lines changed
Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
/* ************************************************************************
2+
3+
osparc - the simcore frontend
4+
5+
https://osparc.io
6+
7+
Copyright:
8+
2023 IT'IS Foundation, https://itis.swiss
9+
10+
License:
11+
MIT: https://opensource.org/licenses/MIT
12+
13+
Authors:
14+
* Odei Maiz (odeimaiz)
15+
16+
************************************************************************ */
17+
18+
19+
qx.Class.define("osparc.support.Conversation", {
20+
extend: qx.ui.core.Widget,
21+
22+
/**
23+
* @param conversationId {String} Conversation Id
24+
*/
25+
construct: function(conversationId) {
26+
this.base(arguments);
27+
28+
this.__messages = [];
29+
30+
if (conversationId) {
31+
this.setConversationId(conversationId);
32+
}
33+
34+
this._setLayout(new qx.ui.layout.VBox(5));
35+
36+
this.set({
37+
padding: 10,
38+
showCloseButton: false,
39+
});
40+
41+
this.getChildControl("button").set({
42+
font: "text-13",
43+
});
44+
this.__addConversationButtons();
45+
46+
this.__buildLayout();
47+
48+
this.__reloadMessages();
49+
},
50+
51+
properties: {
52+
conversationId: {
53+
check: "String",
54+
init: null,
55+
nullable: false,
56+
event: "changeConversationId"
57+
},
58+
},
59+
60+
members: {
61+
__messages: null,
62+
__nextRequestParams: null,
63+
__messagesTitle: null,
64+
__messageScroll: null,
65+
__messagesList: null,
66+
__loadMoreMessages: null,
67+
68+
__addConversationButtons: function() {
69+
const tabButton = this.getChildControl("button");
70+
71+
const buttonsAesthetics = {
72+
focusable: false,
73+
keepActive: true,
74+
padding: 0,
75+
backgroundColor: "transparent",
76+
};
77+
const renameButton = new qx.ui.form.Button(null, "@FontAwesome5Solid/pencil-alt/10").set({
78+
...buttonsAesthetics,
79+
});
80+
renameButton.addListener("execute", () => {
81+
const titleEditor = new osparc.widget.Renamer(tabButton.getLabel());
82+
titleEditor.addListener("labelChanged", e => {
83+
titleEditor.close();
84+
const newLabel = e.getData()["newLabel"];
85+
if (this.getConversationId()) {
86+
osparc.store.ConversationsProject.getInstance().renameConversation(this.getConversationId(), newLabel)
87+
.then(() => this.renameConversation(newLabel));
88+
} else {
89+
// create new conversation first
90+
osparc.store.ConversationsProject.getInstance().addConversation(newLabel)
91+
.then(data => {
92+
this.setConversationId(data["conversationId"]);
93+
this.getChildControl("button").setLabel(newLabel);
94+
});
95+
}
96+
}, this);
97+
titleEditor.center();
98+
titleEditor.open();
99+
});
100+
// eslint-disable-next-line no-underscore-dangle
101+
tabButton._add(renameButton, {
102+
row: 0,
103+
column: 3
104+
});
105+
106+
const closeButton = new qx.ui.form.Button(null, "@FontAwesome5Solid/times/12").set({
107+
...buttonsAesthetics,
108+
paddingLeft: 4, // adds spacing between buttons
109+
});
110+
closeButton.addListener("execute", () => {
111+
if (this.__messagesList.getChildren().length === 0) {
112+
osparc.store.ConversationsProject.getInstance().deleteConversation(this.getConversationId());
113+
} else {
114+
const msg = this.tr("Are you sure you want to delete the conversation?");
115+
const confirmationWin = new osparc.ui.window.Confirmation(msg).set({
116+
caption: this.tr("Delete Conversation"),
117+
confirmText: this.tr("Delete"),
118+
confirmAction: "delete"
119+
});
120+
confirmationWin.open();
121+
confirmationWin.addListener("close", () => {
122+
if (confirmationWin.getConfirmed()) {
123+
osparc.store.ConversationsProject.getInstance().deleteConversation(this.getConversationId());
124+
}
125+
}, this);
126+
}
127+
});
128+
// eslint-disable-next-line no-underscore-dangle
129+
tabButton._add(closeButton, {
130+
row: 0,
131+
column: 4
132+
});
133+
this.bind("conversationId", closeButton, "visibility", {
134+
converter: value => value ? "visible" : "excluded"
135+
});
136+
},
137+
138+
renameConversation: function(newName) {
139+
this.getChildControl("button").setLabel(newName);
140+
},
141+
142+
__buildLayout: function() {
143+
this.__messagesTitle = new qx.ui.basic.Label();
144+
this._add(this.__messagesTitle);
145+
146+
// add spacer to keep the messages list at the bottom
147+
this._add(new qx.ui.core.Spacer(), {
148+
flex: 100 // high number to keep even a one message list at the bottom
149+
});
150+
151+
this.__messagesList = new qx.ui.container.Composite(new qx.ui.layout.VBox(5)).set({
152+
alignY: "middle"
153+
});
154+
const scrollView = this.__messageScroll = new qx.ui.container.Scroll();
155+
scrollView.add(this.__messagesList);
156+
this._add(scrollView, {
157+
flex: 1
158+
});
159+
160+
this.__loadMoreMessages = new osparc.ui.form.FetchButton(this.tr("Load more messages..."));
161+
this.__loadMoreMessages.addListener("execute", () => this.__reloadMessages(false));
162+
this._add(this.__loadMoreMessages);
163+
164+
const addMessages = new osparc.conversation.AddMessage(this.getConversationId()).set({
165+
paddingLeft: 10,
166+
});
167+
addMessages.addListener("messageAdded", e => {
168+
const data = e.getData();
169+
if (data["conversationId"]) {
170+
this.setConversationId(data["conversationId"]);
171+
this.addMessage(data);
172+
}
173+
});
174+
this._add(addMessages);
175+
},
176+
177+
__getNextRequest: function() {
178+
const params = {
179+
url: {
180+
conversationId: this.getConversationId(),
181+
offset: 0,
182+
limit: 42
183+
}
184+
};
185+
const nextRequestParams = this.__nextRequestParams;
186+
if (nextRequestParams) {
187+
params.url.offset = nextRequestParams.offset;
188+
params.url.limit = nextRequestParams.limit;
189+
}
190+
const options = {
191+
resolveWResponse: true
192+
};
193+
return osparc.data.Resources.fetch("conversationsStudies", "getMessagesPage", params, options)
194+
.catch(err => osparc.FlashMessenger.logError(err));
195+
},
196+
197+
__reloadMessages: function(removeMessages = true) {
198+
if (this.getConversationId() === null) {
199+
// temporary conversation page
200+
this.__messagesTitle.setValue(this.tr("No Messages yet"));
201+
this.__messagesList.hide();
202+
this.__loadMoreMessages.hide();
203+
return;
204+
}
205+
206+
this.__messagesList.show();
207+
this.__loadMoreMessages.show();
208+
this.__loadMoreMessages.setFetching(true);
209+
210+
if (removeMessages) {
211+
this.__messages = [];
212+
this.__messagesList.removeAll();
213+
}
214+
215+
this.__getNextRequest()
216+
.then(resp => {
217+
const messages = resp["data"];
218+
messages.forEach(message => this.addMessage(message));
219+
this.__nextRequestParams = resp["_links"]["next"];
220+
if (this.__nextRequestParams === null && this.__loadMoreMessages) {
221+
this.__loadMoreMessages.exclude();
222+
}
223+
})
224+
.finally(() => this.__loadMoreMessages.setFetching(false));
225+
},
226+
227+
__updateMessagesNumber: function() {
228+
const nMessages = this.__messages.filter(msg => msg["type"] === "MESSAGE").length;
229+
if (nMessages === 0) {
230+
this.__messagesTitle.setValue(this.tr("No Messages yet"));
231+
} else if (nMessages === 1) {
232+
this.__messagesTitle.setValue(this.tr("1 Message"));
233+
} else if (nMessages > 1) {
234+
this.__messagesTitle.setValue(nMessages + this.tr(" Messages"));
235+
}
236+
},
237+
238+
addMessage: function(message) {
239+
// ignore it if it was already there
240+
const messageIndex = this.__messages.findIndex(msg => msg["messageId"] === message["messageId"]);
241+
if (messageIndex !== -1) {
242+
return;
243+
}
244+
245+
// determine insertion index for latest‐first order
246+
const newTime = new Date(message["created"]);
247+
let insertAt = this.__messages.findIndex(m => new Date(m["created"]) > newTime);
248+
if (insertAt === -1) {
249+
insertAt = this.__messages.length;
250+
}
251+
252+
// Insert the message in the messages array
253+
this.__messages.splice(insertAt, 0, message);
254+
255+
// Add the UI element to the messages list
256+
let control = null;
257+
switch (message["type"]) {
258+
case "MESSAGE":
259+
control = new osparc.conversation.MessageUI(message);
260+
control.addListener("messageUpdated", e => this.updateMessage(e.getData()));
261+
control.addListener("messageDeleted", e => this.deleteMessage(e.getData()));
262+
break;
263+
case "NOTIFICATION":
264+
control = new osparc.conversation.NotificationUI(message);
265+
break;
266+
}
267+
if (control) {
268+
// insert into the UI at the same position
269+
this.__messagesList.addAt(control, insertAt);
270+
}
271+
272+
// scroll to bottom
273+
// add timeout to ensure the scroll happens after the UI is updated
274+
setTimeout(() => {
275+
this.__messageScroll.scrollToY(this.__messageScroll.getChildControl("pane").getScrollMaxY());
276+
}, 50);
277+
278+
this.__updateMessagesNumber();
279+
},
280+
281+
deleteMessage: function(message) {
282+
// remove it from the messages array
283+
const messageIndex = this.__messages.findIndex(msg => msg["messageId"] === message["messageId"]);
284+
if (messageIndex === -1) {
285+
return;
286+
}
287+
this.__messages.splice(messageIndex, 1);
288+
289+
// Remove the UI element from the messages list
290+
const children = this.__messagesList.getChildren();
291+
const controlIndex = children.findIndex(
292+
ctrl => ("getMessage" in ctrl && ctrl.getMessage()["messageId"] === message["messageId"])
293+
);
294+
if (controlIndex > -1) {
295+
this.__messagesList.remove(children[controlIndex]);
296+
}
297+
298+
this.__updateMessagesNumber();
299+
},
300+
301+
updateMessage: function(message) {
302+
// Replace the message in the messages array
303+
const messageIndex = this.__messages.findIndex(msg => msg["messageId"] === message["messageId"]);
304+
if (messageIndex === -1) {
305+
return;
306+
}
307+
this.__messages[messageIndex] = message;
308+
309+
// Update the UI element from the messages list
310+
this.__messagesList.getChildren().forEach(control => {
311+
if ("getMessage" in control && control.getMessage()["messageId"] === message["messageId"]) {
312+
control.setMessage(message);
313+
return;
314+
}
315+
});
316+
},
317+
}
318+
});

services/static-webserver/client/source/class/osparc/support/SupportCenter.js

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,11 @@ qx.Class.define("osparc.support.SupportCenter", {
3131
showClose: true,
3232
});
3333

34-
this.getChildControl("intro-text");
34+
this.getChildControl("conversations-intro-text");
3535
this.getChildControl("conversations-list");
36+
// if (!osparc.store.Products.getInstance().amIASupportUser()) {
37+
this.getChildControl("ask-a-question-button")
38+
// }
3639
},
3740

3841
statics: {
@@ -81,23 +84,49 @@ qx.Class.define("osparc.support.SupportCenter", {
8184
control = new qx.ui.container.Composite(new qx.ui.layout.VBox(5));
8285
this.getChildControl("stack-layout").add(control);
8386
break;
84-
case "intro-text":
87+
case "conversations-intro-text":
8588
control = new qx.ui.basic.Label(this.tr("Welcome to the Support Center"));
8689
this.getChildControl("conversations-layout").add(control);
8790
break;
8891
case "conversations-list": {
89-
control = new osparc.support.Conversations().set({
90-
minHeight: 300,
91-
});
92+
control = new osparc.support.Conversations();
9293
const scroll = new qx.ui.container.Scroll();
9394
scroll.add(control);
9495
this.getChildControl("conversations-layout").add(scroll, {
9596
flex: 1,
9697
});
9798
break;
9899
}
100+
case "ask-a-question-button":
101+
control = new qx.ui.form.Button(this.tr("Ask a Question")).set({
102+
appearance: "strong-button",
103+
center: true,
104+
});
105+
control.addListener("execute", () => {
106+
this.__newConversation();
107+
});
108+
this.getChildControl("conversations-layout").add(control);
109+
break;
110+
case "conversation-layout":
111+
control = new qx.ui.container.Composite(new qx.ui.layout.VBox(5));
112+
this.getChildControl("stack-layout").add(control);
113+
break;
114+
case "conversation-intro-text":
115+
control = new qx.ui.basic.Label(this.tr("One conversation"));
116+
this.getChildControl("conversation-layout").add(control);
117+
break;
118+
case "conversation-content":
119+
control = new osparc.support.Conversation();
120+
this.getChildControl("conversation-layout").add(control);
121+
break;
99122
}
100123
return control || this.base(arguments, id);
101124
},
125+
126+
__newConversation: function() {
127+
this.getChildControl("conversation-intro-text").setValue(this.tr("New conversation"));
128+
const conversation = this.getChildControl("conversation-content");
129+
this.getChildControl("stack-layout").setSelection([this.getChildControl("conversation-layout")]);
130+
},
102131
}
103132
});

0 commit comments

Comments
 (0)