Skip to content

Commit 67d0e3c

Browse files
authored
♻️🎨 [Frontend] Refactor conversations (#8404)
1 parent fba8218 commit 67d0e3c

File tree

18 files changed

+509
-569
lines changed

18 files changed

+509
-569
lines changed

services/static-webserver/client/source/class/osparc/conversation/AddMessage.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ qx.Class.define("osparc.conversation.AddMessage", {
6969
});
7070
break;
7171
}
72-
case "thumbnail": {
72+
case "avatar": {
7373
control = osparc.utils.Utils.createThumbnail(32);
7474
const authStore = osparc.auth.Data.getInstance();
7575
control.set({
@@ -95,6 +95,7 @@ qx.Class.define("osparc.conversation.AddMessage", {
9595
break;
9696
case "add-comment-button":
9797
control = new qx.ui.form.Button(null, "@FontAwesome5Solid/arrow-up/16").set({
98+
toolTipText: this.tr("Ctrl+Enter"),
9899
backgroundColor: "input_background",
99100
allowGrowX: false,
100101
alignX: "right",
@@ -135,7 +136,7 @@ qx.Class.define("osparc.conversation.AddMessage", {
135136
},
136137

137138
__buildLayout: function() {
138-
this.getChildControl("thumbnail");
139+
this.getChildControl("avatar");
139140
this.getChildControl("comment-field");
140141
this.getChildControl("add-comment-button");
141142
},
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
/* ************************************************************************
2+
3+
osparc - the simcore frontend
4+
5+
https://osparc.io
6+
7+
Copyright:
8+
2025 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.conversation.Conversation", {
20+
extend: qx.ui.core.Widget,
21+
22+
/**
23+
* @param conversation {osparc.data.model.Conversation} Conversation
24+
*/
25+
construct: function(conversation) {
26+
this.base(arguments);
27+
28+
this._messages = [];
29+
30+
this._setLayout(new qx.ui.layout.VBox(5));
31+
32+
this._buildLayout();
33+
34+
if (conversation) {
35+
this.setConversation(conversation);
36+
}
37+
},
38+
39+
properties: {
40+
conversation: {
41+
check: "osparc.data.model.Conversation",
42+
init: null,
43+
nullable: true,
44+
event: "changeConversation",
45+
apply: "_applyConversation",
46+
},
47+
},
48+
49+
events: {
50+
"messagesChanged": "qx.event.type.Event",
51+
},
52+
53+
members: {
54+
_messages: null,
55+
56+
_createChildControlImpl: function(id) {
57+
let control;
58+
switch (id) {
59+
case "spacer-top":
60+
control = new qx.ui.core.Spacer();
61+
this._addAt(control, 0, {
62+
flex: 100 // high number to keep even a one message list at the bottom
63+
});
64+
break;
65+
case "messages-container-scroll":
66+
control = new qx.ui.container.Scroll();
67+
this._addAt(control, 1, {
68+
flex: 1
69+
});
70+
break;
71+
case "messages-container":
72+
control = new qx.ui.container.Composite(new qx.ui.layout.VBox(5)).set({
73+
alignY: "middle"
74+
});
75+
this.getChildControl("messages-container-scroll").add(control);
76+
break;
77+
case "load-more-button":
78+
control = new osparc.ui.form.FetchButton(this.tr("Load more messages..."));
79+
control.addListener("execute", () => this.__reloadMessages(false));
80+
this._addAt(control, 2);
81+
break;
82+
case "add-message":
83+
control = new osparc.conversation.AddMessage().set({
84+
padding: 5,
85+
});
86+
this.bind("conversation", control, "conversationId", {
87+
converter: conversation => conversation ? conversation.getConversationId() : null
88+
});
89+
this._addAt(control, 3);
90+
break;
91+
}
92+
return control || this.base(arguments, id);
93+
},
94+
95+
_buildLayout: function() {
96+
this.getChildControl("spacer-top");
97+
this.getChildControl("messages-container");
98+
this.getChildControl("add-message");
99+
},
100+
101+
_applyConversation: function(conversation) {
102+
this.__reloadMessages(true);
103+
104+
if (conversation) {
105+
conversation.addListener("messageAdded", e => {
106+
const data = e.getData();
107+
this.addMessage(data);
108+
});
109+
conversation.addListener("messageUpdated", e => {
110+
const data = e.getData();
111+
this.updateMessage(data);
112+
});
113+
conversation.addListener("messageDeleted", e => {
114+
const data = e.getData();
115+
this.deleteMessage(data);
116+
});
117+
}
118+
},
119+
120+
__reloadMessages: function(removeMessages = true) {
121+
if (removeMessages) {
122+
this.clearAllMessages();
123+
}
124+
125+
const loadMoreMessages = this.getChildControl("load-more-button");
126+
if (this.getConversation() === null) {
127+
loadMoreMessages.hide();
128+
return;
129+
}
130+
131+
loadMoreMessages.show();
132+
loadMoreMessages.setFetching(true);
133+
this.getConversation().getNextMessages()
134+
.then(resp => {
135+
const messages = resp["data"];
136+
messages.forEach(message => this.addMessage(message));
137+
if (resp["_links"]["next"] === null && loadMoreMessages) {
138+
loadMoreMessages.exclude();
139+
}
140+
})
141+
.finally(() => loadMoreMessages.setFetching(false));
142+
},
143+
144+
_createMessageUI: function(message) {
145+
return new osparc.conversation.MessageUI(message);
146+
},
147+
148+
getMessages: function() {
149+
return this._messages;
150+
},
151+
152+
clearAllMessages: function() {
153+
this._messages = [];
154+
this.getChildControl("messages-container").removeAll();
155+
156+
this.fireEvent("messagesChanged");
157+
},
158+
159+
addMessage: function(message) {
160+
// ignore it if it was already there
161+
const messageIndex = this._messages.findIndex(msg => msg["messageId"] === message["messageId"]);
162+
if (messageIndex !== -1) {
163+
return;
164+
}
165+
166+
// determine insertion index for latest‐first order
167+
const newTime = new Date(message["created"]);
168+
let insertAt = this._messages.findIndex(m => new Date(m["created"]) > newTime);
169+
if (insertAt === -1) {
170+
insertAt = this._messages.length;
171+
}
172+
173+
// Insert the message in the messages array
174+
this._messages.splice(insertAt, 0, message);
175+
176+
// Add the UI element to the messages list
177+
let control = null;
178+
switch (message["type"]) {
179+
case "MESSAGE":
180+
control = this._createMessageUI(message);
181+
control.addListener("messageUpdated", e => this.updateMessage(e.getData()));
182+
control.addListener("messageDeleted", e => this.deleteMessage(e.getData()));
183+
break;
184+
case "NOTIFICATION":
185+
control = new osparc.conversation.NotificationUI(message);
186+
break;
187+
}
188+
if (control) {
189+
// insert into the UI at the same position
190+
const messagesContainer = this.getChildControl("messages-container");
191+
messagesContainer.addAt(control, insertAt);
192+
}
193+
194+
// scroll to bottom
195+
// add timeout to ensure the scroll happens after the UI is updated
196+
setTimeout(() => {
197+
const messagesScroll = this.getChildControl("messages-container-scroll");
198+
messagesScroll.scrollToY(messagesScroll.getChildControl("pane").getScrollMaxY());
199+
}, 50);
200+
201+
this.fireEvent("messagesChanged");
202+
},
203+
204+
deleteMessage: function(message) {
205+
// remove it from the messages array
206+
const messageIndex = this._messages.findIndex(msg => msg["messageId"] === message["messageId"]);
207+
if (messageIndex === -1) {
208+
return;
209+
}
210+
this._messages.splice(messageIndex, 1);
211+
212+
// Remove the UI element from the messages list
213+
const messagesContainer = this.getChildControl("messages-container");
214+
const children = messagesContainer.getChildren();
215+
const controlIndex = children.findIndex(
216+
ctrl => ("getMessage" in ctrl && ctrl.getMessage()["messageId"] === message["messageId"])
217+
);
218+
if (controlIndex > -1) {
219+
messagesContainer.remove(children[controlIndex]);
220+
}
221+
222+
this.fireEvent("messagesChanged");
223+
},
224+
225+
updateMessage: function(message) {
226+
// Replace the message in the messages array
227+
const messageIndex = this._messages.findIndex(msg => msg["messageId"] === message["messageId"]);
228+
if (messageIndex === -1) {
229+
return;
230+
}
231+
this._messages[messageIndex] = message;
232+
233+
// Update the UI element from the messages list
234+
const messagesContainer = this.getChildControl("messages-container");
235+
const messageUI = messagesContainer.getChildren().find(control => {
236+
return "getMessage" in control && control.getMessage()["messageId"] === message["messageId"];
237+
});
238+
if (messageUI) {
239+
// Force a new reference
240+
messageUI.setMessage(Object.assign({}, message));
241+
}
242+
},
243+
}
244+
});

services/static-webserver/client/source/class/osparc/conversation/MessageUI.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ qx.Class.define("osparc.conversation.MessageUI", {
6464
const isMyMessage = this.self().isMyMessage(this.getMessage());
6565
let control;
6666
switch (id) {
67-
case "thumbnail":
67+
case "avatar":
6868
control = new osparc.ui.basic.UserThumbnail(32).set({
6969
marginTop: 4,
7070
alignY: "top",
@@ -149,18 +149,18 @@ qx.Class.define("osparc.conversation.MessageUI", {
149149
const messageContent = this.getChildControl("message-content");
150150
messageContent.setValue(message["content"]);
151151

152-
const thumbnail = this.getChildControl("thumbnail");
152+
const avatar = this.getChildControl("avatar");
153153
const userName = this.getChildControl("user-name");
154154
if (message["userGroupId"] === "system") {
155155
userName.setValue("Support");
156156
} else {
157157
osparc.store.Users.getInstance().getUser(message["userGroupId"])
158158
.then(user => {
159-
thumbnail.setUser(user);
159+
avatar.setUser(user);
160160
userName.setValue(user ? user.getLabel() : "Unknown user");
161161
})
162162
.catch(() => {
163-
thumbnail.setSource(osparc.utils.Avatar.emailToThumbnail());
163+
avatar.setSource(osparc.utils.Avatar.emailToThumbnail());
164164
userName.setValue("Unknown user");
165165
});
166166
}

services/static-webserver/client/source/class/osparc/data/model/Conversation.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ qx.Class.define("osparc.data.model.Conversation", {
2424

2525
/**
2626
* @param conversationData {Object} Object containing the serialized Conversation Data
27+
* @param studyId {String} ID of the Study
2728
* */
28-
construct: function(conversationData) {
29+
construct: function(conversationData, studyId) {
2930
this.base(arguments);
3031

3132
this.set({
@@ -37,6 +38,7 @@ qx.Class.define("osparc.data.model.Conversation", {
3738
modified: new Date(conversationData.modified),
3839
projectId: conversationData.projectUuid || null,
3940
extraContext: conversationData.extraContext || null,
41+
studyId: studyId || null,
4042
});
4143

4244
this.__messages = [];
@@ -134,6 +136,12 @@ qx.Class.define("osparc.data.model.Conversation", {
134136
event: "changeLastMessage",
135137
apply: "__applyLastMessage",
136138
},
139+
140+
studyId: {
141+
check: "String",
142+
nullable: true,
143+
init: null,
144+
},
137145
},
138146

139147
events: {
@@ -221,6 +229,10 @@ qx.Class.define("osparc.data.model.Conversation", {
221229
limit: 42
222230
}
223231
};
232+
if (this.getStudyId()) {
233+
params.url.studyId = this.getStudyId();
234+
}
235+
224236
const nextRequestParams = this.__nextRequestParams;
225237
if (nextRequestParams) {
226238
params.url.offset = nextRequestParams.offset;
@@ -229,7 +241,10 @@ qx.Class.define("osparc.data.model.Conversation", {
229241
const options = {
230242
resolveWResponse: true
231243
};
232-
return osparc.data.Resources.fetch("conversationsSupport", "getMessagesPage", params, options)
244+
const promise = this.getStudyId() ?
245+
osparc.data.Resources.fetch("conversationsStudies", "getMessagesPage", params, options) :
246+
osparc.data.Resources.fetch("conversationsSupport", "getMessagesPage", params, options);
247+
return promise
233248
.then(resp => {
234249
const messages = resp["data"];
235250
messages.forEach(message => this.addMessage(message));

services/static-webserver/client/source/class/osparc/desktop/StudyEditor.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ qx.Class.define("osparc.desktop.StudyEditor", {
313313
this.__listenToNodeUpdated();
314314
this.__listenToNodeProgress();
315315
this.__listenToNoMoreCreditsEvents();
316-
this.__listenToEvent();
316+
this.__listenToServiceCustomEvents();
317317
this.__listenToServiceStatus();
318318
this.__listenToStatePorts();
319319

@@ -537,7 +537,7 @@ qx.Class.define("osparc.desktop.StudyEditor", {
537537
}
538538
},
539539

540-
__listenToEvent: function() {
540+
__listenToServiceCustomEvents: function() {
541541
const socket = osparc.wrapper.WebSocket.getInstance();
542542

543543
// callback for events

services/static-webserver/client/source/class/osparc/desktop/organizations/MembersList.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,10 +158,7 @@ qx.Class.define("osparc.desktop.organizations.MembersList", {
158158
},
159159
configureItem: item => {
160160
item.subscribeToFilterGroup("organizationMembersList");
161-
item.getChildControl("thumbnail").getContentElement()
162-
.setStyles({
163-
"border-radius": "16px"
164-
});
161+
item.getChildControl("thumbnail").setDecorator("circled");
165162
item.addListener("promoteToMember", e => {
166163
const listedMember = e.getData();
167164
this.__promoteToUser(listedMember);

services/static-webserver/client/source/class/osparc/desktop/organizations/OrganizationsList.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -154,11 +154,7 @@ qx.Class.define("osparc.desktop.organizations.OrganizationsList", {
154154
configureItem: item => {
155155
item.subscribeToFilterGroup("organizationsList");
156156
osparc.utils.Utils.setIdToWidget(item, "organizationListItem");
157-
const thumbnail = item.getChildControl("thumbnail");
158-
thumbnail.getContentElement()
159-
.setStyles({
160-
"border-radius": "16px"
161-
});
157+
item.getChildControl("thumbnail").setDecorator("circled");
162158

163159
item.addListener("openEditOrganization", e => {
164160
const orgKey = e.getData();

0 commit comments

Comments
 (0)