turbo_chat 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +21 -0
  3. data/README.md +741 -0
  4. data/app/assets/config/chat_gem_manifest.js +6 -0
  5. data/app/assets/javascripts/chat_gem/application.js +4 -0
  6. data/app/assets/javascripts/chat_gem/lifecycle_events.js +93 -0
  7. data/app/assets/javascripts/chat_gem/messages.js +442 -0
  8. data/app/assets/javascripts/chat_gem/realtime.js +398 -0
  9. data/app/assets/javascripts/chat_gem/shared.js +488 -0
  10. data/app/assets/stylesheets/chat_gem/application.css +741 -0
  11. data/app/controllers/chat_gem/application_controller.rb +41 -0
  12. data/app/controllers/chat_gem/chat_memberships_controller.rb +81 -0
  13. data/app/controllers/chat_gem/chat_messages_controller.rb +144 -0
  14. data/app/controllers/chat_gem/chats_controller/event_payload_support.rb +58 -0
  15. data/app/controllers/chat_gem/chats_controller/invitation_support.rb +31 -0
  16. data/app/controllers/chat_gem/chats_controller.rb +125 -0
  17. data/app/helpers/chat_gem/application_helper/config_support.rb +41 -0
  18. data/app/helpers/chat_gem/application_helper/mention_support/entry_builder.rb +55 -0
  19. data/app/helpers/chat_gem/application_helper/mention_support/permission_support.rb +28 -0
  20. data/app/helpers/chat_gem/application_helper/mention_support/token_builder.rb +49 -0
  21. data/app/helpers/chat_gem/application_helper/mention_support.rb +80 -0
  22. data/app/helpers/chat_gem/application_helper/message_rendering.rb +165 -0
  23. data/app/helpers/chat_gem/application_helper/participant_support.rb +81 -0
  24. data/app/helpers/chat_gem/application_helper.rb +12 -0
  25. data/app/models/chat_gem/application_record.rb +5 -0
  26. data/app/models/chat_gem/chat.rb +127 -0
  27. data/app/models/chat_gem/chat_membership.rb +136 -0
  28. data/app/models/chat_gem/chat_message/blocked_words_moderation.rb +120 -0
  29. data/app/models/chat_gem/chat_message/body_length_validation.rb +20 -0
  30. data/app/models/chat_gem/chat_message/broadcasting.rb +61 -0
  31. data/app/models/chat_gem/chat_message/formatting.rb +81 -0
  32. data/app/models/chat_gem/chat_message/mention_validation.rb +85 -0
  33. data/app/models/chat_gem/chat_message/signals.rb +61 -0
  34. data/app/models/chat_gem/chat_message.rb +40 -0
  35. data/app/views/chat_gem/chat_messages/_chat_message.html.erb +1 -0
  36. data/app/views/chat_gem/chat_messages/_form.html.erb +22 -0
  37. data/app/views/chat_gem/chat_messages/_message.html.erb +83 -0
  38. data/app/views/chat_gem/chat_messages/_signal.html.erb +3 -0
  39. data/app/views/chat_gem/chat_messages/_signals.html.erb +24 -0
  40. data/app/views/chat_gem/chat_messages/index.html.erb +1 -0
  41. data/app/views/chat_gem/chats/index.html.erb +51 -0
  42. data/app/views/chat_gem/chats/new.html.erb +13 -0
  43. data/app/views/chat_gem/chats/show.html.erb +95 -0
  44. data/app/views/layouts/chat_gem/application.html.erb +20 -0
  45. data/config/routes.rb +16 -0
  46. data/db/migrate/20260215000000_create_chat_gem_chats.rb +8 -0
  47. data/db/migrate/20260215000001_create_chat_gem_chat_memberships.rb +19 -0
  48. data/db/migrate/20260215000002_create_chat_gem_chat_messages.rb +14 -0
  49. data/db/migrate/20260218000011_add_closed_at_to_chat_gem_chats.rb +6 -0
  50. data/db/migrate/20260218000012_add_custom_role_key_to_chat_memberships.rb +6 -0
  51. data/db/migrate/20260218000013_add_invitation_accepted_to_chat_gem_chat_memberships.rb +5 -0
  52. data/lib/chat_gem/configuration.rb +242 -0
  53. data/lib/chat_gem/engine.rb +29 -0
  54. data/lib/chat_gem/model_extensions/chat_participant.rb +45 -0
  55. data/lib/chat_gem/moderation.rb +194 -0
  56. data/lib/chat_gem/permission.rb +193 -0
  57. data/lib/chat_gem/signals.rb +26 -0
  58. data/lib/chat_gem/version.rb +3 -0
  59. data/lib/chat_gem.rb +24 -0
  60. data/lib/generators/chat_gem/install/install_generator.rb +18 -0
  61. data/lib/generators/chat_gem/install/templates/chat_gem.rb +36 -0
  62. data/lib/generators/turbo_chat/install/install_generator.rb +18 -0
  63. data/lib/generators/turbo_chat/install/templates/turbo_chat.rb +36 -0
  64. data/lib/tasks/chat_gem_tasks.rake +1 -0
  65. data/lib/tasks/turbo_chat_tasks.rake +10 -0
  66. data/lib/turbo_chat/version.rb +5 -0
  67. data/lib/turbo_chat.rb +24 -0
  68. data/turbo_chat.gemspec +31 -0
  69. metadata +155 -0
@@ -0,0 +1,6 @@
1
+ //= link chat_gem/application.css
2
+ //= link chat_gem/application.js
3
+ //= link chat_gem/shared.js
4
+ //= link chat_gem/messages.js
5
+ //= link chat_gem/realtime.js
6
+ //= link chat_gem/lifecycle_events.js
@@ -0,0 +1,4 @@
1
+ //= require chat_gem/shared
2
+ //= require chat_gem/messages
3
+ //= require chat_gem/realtime
4
+ //= require chat_gem/lifecycle_events
@@ -0,0 +1,93 @@
1
+ (function (namespace) {
2
+ var datasetFlagEnabled = namespace.datasetFlagEnabled;
3
+ var parseJsonObject = namespace.parseJsonObject;
4
+
5
+ function setupInvitationEvents() {
6
+ document.querySelectorAll("[data-chat-index]").forEach(function (element) {
7
+ if (!element || !element.dataset || element.dataset.chatInvitationEventsBound === "true") {
8
+ return;
9
+ }
10
+
11
+ element.dataset.chatInvitationEventsBound = "true";
12
+ if (!datasetFlagEnabled(element, "chatEmitInvitationEvents")) {
13
+ return;
14
+ }
15
+
16
+ var payload = parseJsonObject(element.dataset.chatInvitationAccepted);
17
+ if (!payload) {
18
+ return;
19
+ }
20
+
21
+ var event = new CustomEvent("chat-gem:invitation-accepted", {
22
+ bubbles: true,
23
+ detail: {
24
+ chatId: payload.chatId || null,
25
+ chatTitle: payload.chatTitle || null,
26
+ chatMembershipId: payload.chatMembershipId || null
27
+ }
28
+ });
29
+ element.dispatchEvent(event);
30
+ });
31
+ }
32
+
33
+ function setupChatLifecycleEvents() {
34
+ document.querySelectorAll("[data-chat-lifecycle-event]").forEach(function (element) {
35
+ if (!element || !element.dataset || element.dataset.chatLifecycleEventsBound === "true") {
36
+ return;
37
+ }
38
+
39
+ element.dataset.chatLifecycleEventsBound = "true";
40
+ if (!datasetFlagEnabled(element, "chatEmitChatLifecycleEvents")) {
41
+ return;
42
+ }
43
+
44
+ var payload = parseJsonObject(element.dataset.chatLifecycleEvent);
45
+ if (!payload) {
46
+ return;
47
+ }
48
+
49
+ var eventName = String(payload.eventName || "").trim();
50
+ if (!eventName) {
51
+ return;
52
+ }
53
+
54
+ var event = new CustomEvent(eventName, {
55
+ bubbles: true,
56
+ detail: {
57
+ action: payload.action || null,
58
+ chatId: payload.chatId || null,
59
+ chatTitle: payload.chatTitle || null,
60
+ chatMembershipId: payload.chatMembershipId || null
61
+ }
62
+ });
63
+ element.dispatchEvent(event);
64
+ });
65
+ }
66
+
67
+ function setupChatGemUi() {
68
+ setupInvitationEvents();
69
+ setupChatLifecycleEvents();
70
+
71
+ if (typeof namespace.setupAllComposers === "function") {
72
+ namespace.setupAllComposers();
73
+ }
74
+ if (typeof namespace.setupAllMessageAutoScroll === "function") {
75
+ namespace.setupAllMessageAutoScroll();
76
+ }
77
+ if (typeof namespace.setupAllSignalContainers === "function") {
78
+ namespace.setupAllSignalContainers();
79
+ }
80
+ if (typeof namespace.pruneSignals === "function") {
81
+ namespace.pruneSignals();
82
+ }
83
+ }
84
+
85
+ document.addEventListener("turbo:load", setupChatGemUi);
86
+ document.addEventListener("DOMContentLoaded", setupChatGemUi);
87
+ document.addEventListener("turbo:render", setupChatGemUi);
88
+ setInterval(function () {
89
+ if (typeof namespace.pruneSignals === "function") {
90
+ namespace.pruneSignals();
91
+ }
92
+ }, 1000);
93
+ })(window.ChatGemUI = window.ChatGemUI || {});
@@ -0,0 +1,442 @@
1
+ (function (namespace) {
2
+ var constants = namespace.constants || {};
3
+ var MENTION_BLUR_HIDE_DELAY_MS = constants.MENTION_BLUR_HIDE_DELAY_MS || 120;
4
+
5
+ var datasetFlagEnabled = namespace.datasetFlagEnabled;
6
+ var parseMentionTokens = namespace.parseMentionTokens;
7
+ var parseMentionOptions = namespace.parseMentionOptions;
8
+ var mentionKind = namespace.mentionKind;
9
+ var emptyMentionAutocomplete = namespace.emptyMentionAutocomplete;
10
+ var setupMentionAutocomplete = namespace.setupMentionAutocomplete;
11
+ var scrollMessageIntoView = namespace.scrollMessageIntoView;
12
+ var scrollLastMessageIntoView = namespace.scrollLastMessageIntoView;
13
+
14
+ function syncOwnMessageClasses(container) {
15
+ if (!container || !container.dataset) {
16
+ return;
17
+ }
18
+
19
+ var selfType = container.dataset.chatSelfParticipantType;
20
+ var selfId = container.dataset.chatSelfParticipantId;
21
+ var canEditOwnMessages = container.dataset.chatCanEditOwnMessages === "true";
22
+ if (!selfType || !selfId) {
23
+ return;
24
+ }
25
+
26
+ container
27
+ .querySelectorAll("[data-chat-message-participant-type][data-chat-message-participant-id]")
28
+ .forEach(function (messageNode) {
29
+ var isOwnMessage =
30
+ messageNode.dataset.chatMessageParticipantType === selfType &&
31
+ messageNode.dataset.chatMessageParticipantId === selfId;
32
+ var canEditMessage = isOwnMessage && canEditOwnMessages;
33
+ messageNode.dataset.chatEditAllowed = canEditMessage ? "true" : "false";
34
+
35
+ setupMessageInlineEditing(messageNode);
36
+ messageNode.classList.toggle("chat-bubble--own", isOwnMessage);
37
+
38
+ messageNode.querySelectorAll("[data-chat-message-edit-control]").forEach(function (editControl) {
39
+ editControl.hidden = !canEditMessage || messageNode.dataset.chatInlineEditing === "true";
40
+ });
41
+
42
+ if (!canEditMessage) {
43
+ messageNode.dispatchEvent(new CustomEvent("chat-gem:inline-edit-close"));
44
+ messageNode.querySelectorAll("[data-chat-message-view]").forEach(function (viewContainer) {
45
+ viewContainer.hidden = false;
46
+ });
47
+ messageNode.querySelectorAll("[data-chat-message-edit]").forEach(function (editContainer) {
48
+ editContainer.hidden = true;
49
+ });
50
+ messageNode.classList.remove("chat-bubble--editing");
51
+ messageNode.dataset.chatInlineEditing = "false";
52
+ }
53
+ });
54
+ }
55
+
56
+ function mentionTokensForMessage(messageNode) {
57
+ if (!messageNode) {
58
+ return [];
59
+ }
60
+
61
+ var datasetMentions = parseMentionTokens(messageNode.dataset.chatMessageMentions);
62
+ if (datasetMentions.length) {
63
+ return datasetMentions;
64
+ }
65
+
66
+ var seen = {};
67
+ var mentions = [];
68
+ messageNode.querySelectorAll(".chat-mention").forEach(function (mentionNode) {
69
+ var token = String(mentionNode.textContent || "").trim();
70
+ if (!token || token.charAt(0) !== "@" || /\s/.test(token)) {
71
+ return;
72
+ }
73
+
74
+ var dedupeKey = token.toLowerCase();
75
+ if (seen[dedupeKey]) {
76
+ return;
77
+ }
78
+
79
+ seen[dedupeKey] = true;
80
+ mentions.push(token);
81
+ });
82
+
83
+ return mentions;
84
+ }
85
+
86
+ function ownMessageNode(container, messageNode) {
87
+ if (!container || !messageNode) {
88
+ return false;
89
+ }
90
+
91
+ var selfType = container.dataset.chatSelfParticipantType;
92
+ var selfId = container.dataset.chatSelfParticipantId;
93
+ if (!selfType || !selfId) {
94
+ return false;
95
+ }
96
+
97
+ return (
98
+ messageNode.dataset.chatMessageParticipantType === selfType &&
99
+ messageNode.dataset.chatMessageParticipantId === selfId
100
+ );
101
+ }
102
+
103
+ function mentionTargetsCurrentParticipant(token, context) {
104
+ if (!context) {
105
+ return false;
106
+ }
107
+
108
+ var normalizedToken = String(token || "").trim().toLowerCase();
109
+ if (!normalizedToken) {
110
+ return false;
111
+ }
112
+
113
+ if (context.excludeSelf && context.ownMessage) {
114
+ return false;
115
+ }
116
+
117
+ if (normalizedToken === "@all") {
118
+ return true;
119
+ }
120
+
121
+ if (context.selfRoleMentionToken && normalizedToken === context.selfRoleMentionToken) {
122
+ return true;
123
+ }
124
+
125
+ return Boolean(context.selfMentionTokens[normalizedToken]);
126
+ }
127
+
128
+ function syncMentionHighlights(container, options) {
129
+ if (!container || !container.dataset) {
130
+ return;
131
+ }
132
+
133
+ options = options || {};
134
+ var emitEvents = Boolean(options.emitEvents);
135
+ var emitMentionEvents = datasetFlagEnabled(container, "chatEmitMentionEvents");
136
+ var excludeSelf = datasetFlagEnabled(container, "chatMentionFilterExcludeSelf");
137
+ var selfMentionTokens = {};
138
+ parseMentionTokens(container.dataset.chatSelfMentionTokens).forEach(function (token) {
139
+ selfMentionTokens[token.toLowerCase()] = true;
140
+ });
141
+
142
+ var selfRoleMentionToken = parseMentionTokens(container.dataset.chatSelfRoleMentionToken)[0];
143
+ selfRoleMentionToken = selfRoleMentionToken ? selfRoleMentionToken.toLowerCase() : "";
144
+
145
+ container
146
+ .querySelectorAll("[data-chat-message-participant-type][data-chat-message-participant-id]")
147
+ .forEach(function (messageNode) {
148
+ var mentions = mentionTokensForMessage(messageNode);
149
+ var mentionKinds = mentions
150
+ .map(function (token) {
151
+ return {
152
+ token: token,
153
+ kind: mentionKind(token)
154
+ };
155
+ })
156
+ .filter(function (entry) {
157
+ return Boolean(entry.kind);
158
+ });
159
+
160
+ var context = {
161
+ excludeSelf: excludeSelf,
162
+ ownMessage: ownMessageNode(container, messageNode),
163
+ selfRoleMentionToken: selfRoleMentionToken,
164
+ selfMentionTokens: selfMentionTokens
165
+ };
166
+
167
+ var targetedMentions = mentionKinds
168
+ .map(function (entry) {
169
+ return entry.token;
170
+ })
171
+ .filter(function (token) {
172
+ return mentionTargetsCurrentParticipant(token, context);
173
+ });
174
+
175
+ var targetedMentionLookup = {};
176
+ targetedMentions.forEach(function (token) {
177
+ targetedMentionLookup[token.toLowerCase()] = true;
178
+ });
179
+
180
+ var targetsCurrentParticipant = targetedMentions.length > 0;
181
+ messageNode.classList.remove("chat-bubble--mentioned");
182
+
183
+ messageNode.querySelectorAll(".chat-mention").forEach(function (mentionNode) {
184
+ var mentionToken = String(mentionNode.textContent || "").trim().toLowerCase();
185
+ mentionNode.classList.toggle("chat-mention--targeted", Boolean(targetedMentionLookup[mentionToken]));
186
+ });
187
+
188
+ if (!emitEvents || !emitMentionEvents || messageNode.dataset.chatMentionEventEmitted === "true") {
189
+ messageNode.dataset.chatMentionEventEmitted = "true";
190
+ return;
191
+ }
192
+
193
+ messageNode.dataset.chatMentionEventEmitted = "true";
194
+ if (!mentionKinds.length) {
195
+ return;
196
+ }
197
+
198
+ var event = new CustomEvent("chat-gem:mention", {
199
+ bubbles: true,
200
+ detail: {
201
+ chatId: container.dataset.chatId || null,
202
+ messageId: messageNode.dataset.chatMessageId || null,
203
+ messageElementId: messageNode.id || null,
204
+ participantType: messageNode.dataset.chatMessageParticipantType || null,
205
+ participantId: messageNode.dataset.chatMessageParticipantId || null,
206
+ mentions: mentionKinds.map(function (entry) {
207
+ return entry.token;
208
+ }),
209
+ mentionKinds: mentionKinds,
210
+ targetedMentions: targetedMentions,
211
+ targetsCurrentParticipant: targetsCurrentParticipant
212
+ }
213
+ });
214
+ container.dispatchEvent(event);
215
+ });
216
+ }
217
+
218
+ function setupMessageInlineEditing(messageNode) {
219
+ if (!messageNode || messageNode.dataset.chatInlineEditBound === "true") {
220
+ return;
221
+ }
222
+
223
+ var editContainer = messageNode.querySelector("[data-chat-message-edit]");
224
+ if (!editContainer) {
225
+ return;
226
+ }
227
+
228
+ var viewContainer = messageNode.querySelector("[data-chat-message-view]");
229
+ var textarea = editContainer.querySelector("[data-chat-inline-edit-input]") || editContainer.querySelector("textarea");
230
+ var editButtons = messageNode.querySelectorAll("[data-chat-edit-start]");
231
+ var cancelButtons = messageNode.querySelectorAll("[data-chat-edit-cancel]");
232
+ var forms = messageNode.querySelectorAll("[data-chat-inline-edit-form]");
233
+ var saveButtons = messageNode.querySelectorAll("[data-chat-edit-save]");
234
+ var form = forms[0] || null;
235
+ var originalBody = textarea ? textarea.value : "";
236
+ var mentionContainer = editContainer.querySelector(".chat-inline-edit-field") || form || editContainer;
237
+ var mentionAutocomplete = emptyMentionAutocomplete();
238
+
239
+ if (form && textarea && datasetFlagEnabled(form, "chatEnableMentions")) {
240
+ mentionAutocomplete = setupMentionAutocomplete(textarea, {
241
+ mentionOptions: parseMentionOptions(form.dataset.chatMentionOptions),
242
+ menuHost: mentionContainer,
243
+ menuClassName: "chat-mentions-menu chat-mentions-menu--inline-edit"
244
+ });
245
+ }
246
+
247
+ function canEditMessage() {
248
+ return messageNode.dataset.chatEditAllowed === "true";
249
+ }
250
+
251
+ function canSubmitEdit() {
252
+ if (!textarea) {
253
+ return false;
254
+ }
255
+
256
+ return textarea.value.trim().length > 0 && textarea.value !== originalBody;
257
+ }
258
+
259
+ function updateSaveState() {
260
+ var submitEnabled = canSubmitEdit();
261
+ saveButtons.forEach(function (saveButton) {
262
+ saveButton.disabled = !submitEnabled;
263
+ });
264
+ }
265
+
266
+ function closeOtherEditors() {
267
+ var messagesContainer = messageNode.closest(".chat-messages");
268
+ if (!messagesContainer) {
269
+ return;
270
+ }
271
+
272
+ messagesContainer
273
+ .querySelectorAll("[data-chat-message-participant-type][data-chat-message-participant-id]")
274
+ .forEach(function (otherMessageNode) {
275
+ if (otherMessageNode === messageNode) {
276
+ return;
277
+ }
278
+
279
+ otherMessageNode.dispatchEvent(new CustomEvent("chat-gem:inline-edit-close"));
280
+ });
281
+ }
282
+
283
+ function setEditing(editing, options) {
284
+ options = options || {};
285
+ if (editing && !canEditMessage()) {
286
+ return;
287
+ }
288
+
289
+ if (!editing && options.restoreOriginal && textarea) {
290
+ textarea.value = originalBody;
291
+ }
292
+
293
+ if (editing) {
294
+ closeOtherEditors();
295
+ }
296
+
297
+ if (viewContainer) {
298
+ viewContainer.hidden = editing;
299
+ }
300
+ editContainer.hidden = !editing;
301
+ messageNode.dataset.chatInlineEditing = editing ? "true" : "false";
302
+ messageNode.classList.toggle("chat-bubble--editing", editing);
303
+ editButtons.forEach(function (button) {
304
+ button.hidden = !canEditMessage() || editing;
305
+ });
306
+ updateSaveState();
307
+
308
+ if (editing && textarea) {
309
+ textarea.focus();
310
+ if (typeof textarea.setSelectionRange === "function") {
311
+ var length = textarea.value.length;
312
+ textarea.setSelectionRange(length, length);
313
+ }
314
+
315
+ requestAnimationFrame(function () {
316
+ scrollMessageIntoView(messageNode.closest(".chat-messages"), messageNode);
317
+ });
318
+ }
319
+
320
+ if (!editing) {
321
+ mentionAutocomplete.hideMenu();
322
+ }
323
+ }
324
+
325
+ messageNode.dataset.chatInlineEditBound = "true";
326
+ messageNode.addEventListener("chat-gem:inline-edit-close", function () {
327
+ setEditing(false, { restoreOriginal: true });
328
+ });
329
+
330
+ editButtons.forEach(function (button) {
331
+ button.addEventListener("click", function () {
332
+ setEditing(true);
333
+ });
334
+ });
335
+
336
+ cancelButtons.forEach(function (button) {
337
+ button.addEventListener("click", function () {
338
+ setEditing(false, { restoreOriginal: true });
339
+ });
340
+ });
341
+
342
+ if (textarea) {
343
+ textarea.addEventListener("keydown", function (event) {
344
+ if (mentionAutocomplete.handleKeydown(event)) {
345
+ return;
346
+ }
347
+
348
+ if (event.key === "Escape") {
349
+ event.preventDefault();
350
+ setEditing(false, { restoreOriginal: true });
351
+ return;
352
+ }
353
+
354
+ if (event.key === "Enter" && (event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey) {
355
+ event.preventDefault();
356
+ if (!form || !canSubmitEdit()) {
357
+ return;
358
+ }
359
+
360
+ if (typeof form.requestSubmit === "function") {
361
+ form.requestSubmit();
362
+ } else {
363
+ form.submit();
364
+ }
365
+ }
366
+ });
367
+
368
+ textarea.addEventListener("input", function () {
369
+ mentionAutocomplete.updateMenu();
370
+ updateSaveState();
371
+ });
372
+
373
+ textarea.addEventListener("blur", function () {
374
+ setTimeout(function () {
375
+ mentionAutocomplete.hideMenu();
376
+ }, MENTION_BLUR_HIDE_DELAY_MS);
377
+ });
378
+ }
379
+
380
+ forms.forEach(function (form) {
381
+ form.addEventListener("submit", function () {
382
+ mentionAutocomplete.hideMenu();
383
+ });
384
+
385
+ form.addEventListener("turbo:submit-end", function (event) {
386
+ if (event.detail && event.detail.success) {
387
+ if (textarea) {
388
+ originalBody = textarea.value;
389
+ }
390
+ setEditing(false);
391
+ return;
392
+ }
393
+
394
+ updateSaveState();
395
+ });
396
+ });
397
+
398
+ var initiallyEditing = !editContainer.hidden;
399
+ if (viewContainer) {
400
+ viewContainer.hidden = initiallyEditing;
401
+ }
402
+ messageNode.dataset.chatInlineEditing = initiallyEditing ? "true" : "false";
403
+ messageNode.classList.toggle("chat-bubble--editing", initiallyEditing);
404
+ editButtons.forEach(function (button) {
405
+ button.hidden = !canEditMessage() || initiallyEditing;
406
+ });
407
+ updateSaveState();
408
+
409
+ if (initiallyEditing) {
410
+ closeOtherEditors();
411
+ }
412
+ }
413
+
414
+ function setupMessageAutoScroll(container) {
415
+ if (!container || container.dataset.chatAutoscrollBound === "true") {
416
+ return;
417
+ }
418
+
419
+ container.dataset.chatAutoscrollBound = "true";
420
+ requestAnimationFrame(function () {
421
+ syncOwnMessageClasses(container);
422
+ syncMentionHighlights(container, { emitEvents: false });
423
+ scrollLastMessageIntoView(container);
424
+ });
425
+
426
+ var observer = new MutationObserver(function () {
427
+ requestAnimationFrame(function () {
428
+ syncOwnMessageClasses(container);
429
+ syncMentionHighlights(container, { emitEvents: true });
430
+ scrollLastMessageIntoView(container);
431
+ });
432
+ });
433
+
434
+ observer.observe(container, { childList: true });
435
+ }
436
+
437
+ function setupAllMessageAutoScroll() {
438
+ document.querySelectorAll(".chat-messages").forEach(setupMessageAutoScroll);
439
+ }
440
+
441
+ namespace.setupAllMessageAutoScroll = setupAllMessageAutoScroll;
442
+ })(window.ChatGemUI = window.ChatGemUI || {});