turbo_chat 0.2.0 → 0.3.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -1
  3. data/README.md +178 -190
  4. data/app/assets/config/turbo_chat_manifest.js +3 -0
  5. data/app/assets/javascripts/turbo_chat/application.js +3 -0
  6. data/app/assets/javascripts/turbo_chat/invite_picker.js +19 -392
  7. data/app/assets/javascripts/turbo_chat/member_sync.js +426 -0
  8. data/app/assets/javascripts/turbo_chat/mentions.js +366 -0
  9. data/app/assets/javascripts/turbo_chat/messages.js +18 -370
  10. data/app/assets/javascripts/turbo_chat/realtime.js +3 -10
  11. data/app/assets/javascripts/turbo_chat/scroll_proxy.js +379 -0
  12. data/app/assets/javascripts/turbo_chat/shared.js +7 -383
  13. data/app/assets/stylesheets/turbo_chat/application.css +9 -1646
  14. data/app/assets/stylesheets/turbo_chat/base.css +84 -0
  15. data/app/assets/stylesheets/turbo_chat/components.css +193 -0
  16. data/app/assets/stylesheets/turbo_chat/composer.css +241 -0
  17. data/app/assets/stylesheets/turbo_chat/layout.css +307 -0
  18. data/app/assets/stylesheets/turbo_chat/members.css +264 -0
  19. data/app/assets/stylesheets/turbo_chat/menus.css +172 -0
  20. data/app/assets/stylesheets/turbo_chat/messages.css +430 -0
  21. data/app/controllers/turbo_chat/application_controller.rb +3 -7
  22. data/app/controllers/turbo_chat/chat_memberships_controller.rb +35 -1
  23. data/app/controllers/turbo_chat/chat_messages_controller.rb +4 -8
  24. data/app/controllers/turbo_chat/chats_controller.rb +10 -12
  25. data/app/helpers/turbo_chat/application_helper/config_support.rb +42 -32
  26. data/app/helpers/turbo_chat/application_helper/mention_support.rb +3 -3
  27. data/app/helpers/turbo_chat/application_helper/message_rendering.rb +24 -13
  28. data/app/models/turbo_chat/chat.rb +43 -20
  29. data/app/models/turbo_chat/chat_membership.rb +1 -1
  30. data/app/models/turbo_chat/chat_message/blocked_words_moderation.rb +9 -25
  31. data/app/models/turbo_chat/chat_message/body_length_validation.rb +1 -1
  32. data/app/models/turbo_chat/chat_message/broadcasting.rb +2 -6
  33. data/app/models/turbo_chat/chat_message/formatting.rb +3 -7
  34. data/app/models/turbo_chat/chat_message/mention_validation.rb +1 -1
  35. data/app/models/turbo_chat/chat_message/signals.rb +1 -1
  36. data/app/models/turbo_chat/chat_message.rb +3 -8
  37. data/app/views/turbo_chat/chat_messages/_form.html.erb +9 -9
  38. data/app/views/turbo_chat/chat_messages/_message.html.erb +2 -2
  39. data/app/views/turbo_chat/chat_messages/_signals.html.erb +11 -13
  40. data/app/views/turbo_chat/chat_messages/_system.html.erb +1 -1
  41. data/app/views/turbo_chat/chats/_invite_form.html.erb +1 -1
  42. data/app/views/turbo_chat/chats/_member_entries.html.erb +15 -1
  43. data/app/views/turbo_chat/chats/index.html.erb +1 -1
  44. data/app/views/turbo_chat/chats/new.html.erb +4 -7
  45. data/app/views/turbo_chat/chats/show.html.erb +29 -27
  46. data/config/routes.rb +6 -1
  47. data/db/migrate/20260325000016_add_chat_mode_to_turbo_chat_chats.rb +6 -0
  48. data/lib/generators/turbo_chat/install/templates/turbo_chat.rb +8 -0
  49. data/lib/turbo_chat/configuration/defaults.rb +21 -0
  50. data/lib/turbo_chat/configuration.rb +105 -0
  51. data/lib/turbo_chat/moderation/chat_actions.rb +2 -2
  52. data/lib/turbo_chat/moderation/member_actions.rb +2 -1
  53. data/lib/turbo_chat/moderation/support.rb +5 -9
  54. data/lib/turbo_chat/permission/support.rb +6 -2
  55. data/lib/turbo_chat/permission.rb +1 -5
  56. data/lib/turbo_chat/signals.rb +1 -1
  57. data/lib/turbo_chat/version.rb +1 -1
  58. metadata +13 -2
@@ -59,361 +59,6 @@
59
59
  }
60
60
  }
61
61
 
62
- function parseMentionOptions(raw) {
63
- if (!raw) {
64
- return [];
65
- }
66
-
67
- var parsed;
68
- try {
69
- parsed = JSON.parse(raw);
70
- } catch (_error) {
71
- return [];
72
- }
73
-
74
- if (!Array.isArray(parsed)) {
75
- return [];
76
- }
77
-
78
- var seen = {};
79
- var options = [];
80
-
81
- parsed.forEach(function (entry) {
82
- if (!entry || typeof entry !== "object") {
83
- return;
84
- }
85
-
86
- var token = String(entry.token || "").trim();
87
- if (!token || token.charAt(0) !== "@" || /\s/.test(token)) {
88
- return;
89
- }
90
-
91
- var dedupeKey = token.toLowerCase();
92
- if (seen[dedupeKey]) {
93
- return;
94
- }
95
-
96
- seen[dedupeKey] = true;
97
- options.push({
98
- token: token,
99
- label: String(entry.label || token),
100
- kind: String(entry.kind || "member")
101
- });
102
- });
103
-
104
- return options;
105
- }
106
-
107
- function parseMentionTokens(raw) {
108
- if (!raw) {
109
- return [];
110
- }
111
-
112
- var parsed = raw;
113
- if (typeof raw === "string") {
114
- var trimmed = raw.trim();
115
- if (!trimmed) {
116
- return [];
117
- }
118
-
119
- if (trimmed.charAt(0) === "[") {
120
- try {
121
- parsed = JSON.parse(trimmed);
122
- } catch (_error) {
123
- parsed = trimmed.split(",");
124
- }
125
- } else {
126
- parsed = trimmed.split(",");
127
- }
128
- }
129
-
130
- var source = Array.isArray(parsed) ? parsed : [parsed];
131
- var seen = {};
132
- var tokens = [];
133
-
134
- source.forEach(function (entry) {
135
- var token = String(entry || "").trim();
136
- if (!token || token.charAt(0) !== "@" || /\s/.test(token)) {
137
- return;
138
- }
139
-
140
- var dedupeKey = token.toLowerCase();
141
- if (seen[dedupeKey]) {
142
- return;
143
- }
144
-
145
- seen[dedupeKey] = true;
146
- tokens.push(token);
147
- });
148
-
149
- return tokens;
150
- }
151
-
152
- function mentionKind(token) {
153
- var mentionToken = String(token || "").trim();
154
- if (!mentionToken) {
155
- return null;
156
- }
157
-
158
- if (mentionToken.toLowerCase() === "@all") {
159
- return "group";
160
- }
161
-
162
- if (constants.ROLE_MENTION_TOKEN_PATTERN.test(mentionToken)) {
163
- return "role";
164
- }
165
-
166
- if (constants.MEMBER_MENTION_TOKEN_PATTERN.test(mentionToken)) {
167
- return "member";
168
- }
169
-
170
- return null;
171
- }
172
-
173
- function emptyMentionAutocomplete() {
174
- return {
175
- hideMenu: function () {},
176
- updateMenu: function () {},
177
- handleKeydown: function () {
178
- return false;
179
- }
180
- };
181
- }
182
-
183
- function setupMentionAutocomplete(input, options) {
184
- if (!input) {
185
- return emptyMentionAutocomplete();
186
- }
187
-
188
- var mentionOptions = Array.isArray(options && options.mentionOptions) ? options.mentionOptions : [];
189
- var mentionOptionsResolver = options && typeof options.mentionOptionsResolver === "function"
190
- ? options.mentionOptionsResolver
191
- : null;
192
- var menuHost = options && options.menuHost ? options.menuHost : input.parentNode;
193
- var menuClassName = options && options.menuClassName ? options.menuClassName : "chat-mentions-menu";
194
- if ((!mentionOptions.length && !mentionOptionsResolver) || !menuHost) {
195
- return emptyMentionAutocomplete();
196
- }
197
-
198
- var mentionMenu = null;
199
- var mentionMatches = [];
200
- var mentionActiveIndex = 0;
201
-
202
- function availableMentionOptions() {
203
- if (mentionOptionsResolver) {
204
- var resolvedMentionOptions = mentionOptionsResolver();
205
- return Array.isArray(resolvedMentionOptions) ? resolvedMentionOptions : [];
206
- }
207
-
208
- return mentionOptions;
209
- }
210
-
211
- function ensureMentionMenu() {
212
- if (mentionMenu) {
213
- return mentionMenu;
214
- }
215
-
216
- mentionMenu = document.createElement("div");
217
- mentionMenu.className = menuClassName;
218
- mentionMenu.hidden = true;
219
- menuHost.appendChild(mentionMenu);
220
- return mentionMenu;
221
- }
222
-
223
- function hideMentionMenu() {
224
- if (!mentionMenu) {
225
- return;
226
- }
227
-
228
- mentionMenu.hidden = true;
229
- mentionMenu.classList.remove("chat-mentions-menu--open");
230
- mentionMenu.innerHTML = "";
231
- mentionMatches = [];
232
- mentionActiveIndex = 0;
233
- }
234
-
235
- function setMentionActiveIndex(index) {
236
- if (!mentionMatches.length) {
237
- return;
238
- }
239
-
240
- mentionActiveIndex = ((index % mentionMatches.length) + mentionMatches.length) % mentionMatches.length;
241
- if (!mentionMenu) {
242
- return;
243
- }
244
-
245
- mentionMenu.querySelectorAll(".chat-mentions-item").forEach(function (item, itemIndex) {
246
- item.classList.toggle("chat-mentions-item--active", itemIndex === mentionActiveIndex);
247
- });
248
- }
249
-
250
- function mentionContext() {
251
- var caret = input.selectionStart;
252
- if (typeof caret !== "number") {
253
- return null;
254
- }
255
-
256
- var beforeCaret = input.value.slice(0, caret);
257
- var atIndex = beforeCaret.lastIndexOf("@");
258
- if (atIndex < 0) {
259
- return null;
260
- }
261
-
262
- var previousCharacter = atIndex > 0 ? beforeCaret.charAt(atIndex - 1) : "";
263
- if (/[a-zA-Z0-9_]/.test(previousCharacter)) {
264
- return null;
265
- }
266
-
267
- var query = beforeCaret.slice(atIndex + 1);
268
- if (!/^[a-zA-Z0-9_]*$/.test(query)) {
269
- return null;
270
- }
271
-
272
- return {
273
- start: atIndex,
274
- end: caret,
275
- query: query
276
- };
277
- }
278
-
279
- function matchingMentionOptions(query) {
280
- var normalizedQuery = String(query || "").toLowerCase();
281
- return availableMentionOptions()
282
- .filter(function (option) {
283
- return option.token.slice(1).toLowerCase().indexOf(normalizedQuery) === 0;
284
- })
285
- .slice(0, constants.MENTION_MAX_RESULTS);
286
- }
287
-
288
- function insertMentionOption(option) {
289
- var context = mentionContext();
290
- if (!context) {
291
- hideMentionMenu();
292
- return;
293
- }
294
-
295
- var before = input.value.slice(0, context.start);
296
- var after = input.value.slice(context.end);
297
- var needsTrailingSpace = !after.match(/^[\s,.!?;:)]/);
298
- var insertion = option.token + (needsTrailingSpace ? " " : "");
299
- input.value = before + insertion + after;
300
-
301
- var caretPosition = before.length + insertion.length;
302
- input.setSelectionRange(caretPosition, caretPosition);
303
- input.focus();
304
-
305
- hideMentionMenu();
306
- input.dispatchEvent(new Event("input", { bubbles: true }));
307
- }
308
-
309
- function renderMentionMenu() {
310
- if (!mentionMatches.length) {
311
- hideMentionMenu();
312
- return;
313
- }
314
-
315
- var menu = ensureMentionMenu();
316
- menu.innerHTML = "";
317
- mentionMatches.forEach(function (option, index) {
318
- var item = document.createElement("button");
319
- item.type = "button";
320
- item.className = "chat-mentions-item";
321
- item.setAttribute("data-chat-mention-index", String(index));
322
- if (index === mentionActiveIndex) {
323
- item.classList.add("chat-mentions-item--active");
324
- }
325
-
326
- var token = document.createElement("span");
327
- token.className = "chat-mentions-item__token";
328
- token.textContent = option.token;
329
-
330
- var label = document.createElement("span");
331
- label.className = "chat-mentions-item__label";
332
- label.textContent = option.label;
333
-
334
- item.appendChild(token);
335
- item.appendChild(label);
336
- item.addEventListener("mousedown", function (event) {
337
- event.preventDefault();
338
- });
339
- item.addEventListener("click", function () {
340
- insertMentionOption(option);
341
- });
342
- menu.appendChild(item);
343
- });
344
-
345
- menu.hidden = false;
346
- menu.classList.add("chat-mentions-menu--open");
347
- }
348
-
349
- function selectActiveMention() {
350
- if (!mentionMatches.length) {
351
- return false;
352
- }
353
-
354
- insertMentionOption(mentionMatches[mentionActiveIndex]);
355
- return true;
356
- }
357
-
358
- function updateMentionMenu() {
359
- var context = mentionContext();
360
- if (!context) {
361
- hideMentionMenu();
362
- return;
363
- }
364
-
365
- mentionMatches = matchingMentionOptions(context.query);
366
- if (!mentionMatches.length) {
367
- hideMentionMenu();
368
- return;
369
- }
370
-
371
- mentionActiveIndex = 0;
372
- renderMentionMenu();
373
- }
374
-
375
- function handleMentionKeydown(event) {
376
- if (!mentionMenu || mentionMenu.hidden || !mentionMatches.length) {
377
- return false;
378
- }
379
-
380
- if (event.key === "ArrowDown") {
381
- event.preventDefault();
382
- setMentionActiveIndex(mentionActiveIndex + 1);
383
- return true;
384
- }
385
-
386
- if (event.key === "ArrowUp") {
387
- event.preventDefault();
388
- setMentionActiveIndex(mentionActiveIndex - 1);
389
- return true;
390
- }
391
-
392
- if (event.key === "Enter" || event.key === "Tab") {
393
- event.preventDefault();
394
- return selectActiveMention();
395
- }
396
-
397
- if (event.key === "Escape") {
398
- event.preventDefault();
399
- hideMentionMenu();
400
- return true;
401
- }
402
-
403
- return false;
404
- }
405
-
406
- return {
407
- hideMenu: hideMentionMenu,
408
- updateMenu: updateMentionMenu,
409
- handleKeydown: handleMentionKeydown,
410
- setOptions: function (nextMentionOptions) {
411
- mentionOptions = Array.isArray(nextMentionOptions) ? nextMentionOptions : [];
412
- updateMentionMenu();
413
- }
414
- };
415
- }
416
-
417
62
  function scrollLastMessageIntoView(container) {
418
63
  if (!container) {
419
64
  return;
@@ -430,13 +75,17 @@
430
75
  return;
431
76
  }
432
77
 
433
- var lastMessage = container.lastElementChild;
78
+ // Skip the signals container (always last child) to find the last actual message.
79
+ var lastChild = container.lastElementChild;
80
+ var lastMessage = (lastChild && lastChild.classList.contains("chat-signals"))
81
+ ? lastChild.previousElementSibling
82
+ : lastChild;
434
83
  if (!lastMessage) {
435
84
  return;
436
85
  }
437
86
 
438
87
  // Keep the last message fully visible with a tiny breathing space.
439
- var bottomPadding = Math.max(containerBottomPadding(container), signalOverlayOffset(container));
88
+ var bottomPadding = containerBottomPadding(container);
440
89
  var lastBottom = lastMessage.offsetTop + lastMessage.offsetHeight;
441
90
  var targetScrollTop = Math.max(0, lastBottom - container.clientHeight + bottomPadding + 2);
442
91
  container.scrollTop = targetScrollTop;
@@ -456,25 +105,6 @@
456
105
  return parsedPadding;
457
106
  }
458
107
 
459
- function signalOverlayOffset(container) {
460
- if (!container || typeof window === "undefined") {
461
- return 0;
462
- }
463
-
464
- var chatWindow = container.closest(".chat-window");
465
- if (!chatWindow) {
466
- return 0;
467
- }
468
-
469
- var cssOffset = window.getComputedStyle(chatWindow).getPropertyValue("--chat-signal-offset");
470
- var parsedOffset = parseFloat(cssOffset);
471
- if (isNaN(parsedOffset) || parsedOffset <= 0) {
472
- return 0;
473
- }
474
-
475
- return parsedOffset;
476
- }
477
-
478
108
  function prefersReducedMotion() {
479
109
  return (
480
110
  typeof window !== "undefined" &&
@@ -491,7 +121,7 @@
491
121
  var containerRect = container.getBoundingClientRect();
492
122
  var messageRect = messageNode.getBoundingClientRect();
493
123
  var topPadding = 12;
494
- var bottomPadding = Math.max(14, containerBottomPadding(container), 14 + signalOverlayOffset(container));
124
+ var bottomPadding = Math.max(14, containerBottomPadding(container));
495
125
  var aboveVisibleArea = messageRect.top < containerRect.top + topPadding;
496
126
  var belowVisibleArea = messageRect.bottom > containerRect.bottom - bottomPadding;
497
127
  if (!aboveVisibleArea && !belowVisibleArea) {
@@ -518,14 +148,8 @@
518
148
  namespace.renderTurboStreamResponse = renderTurboStreamResponse;
519
149
  namespace.datasetFlagEnabled = datasetFlagEnabled;
520
150
  namespace.parseJsonObject = parseJsonObject;
521
- namespace.parseMentionOptions = parseMentionOptions;
522
- namespace.parseMentionTokens = parseMentionTokens;
523
- namespace.mentionKind = mentionKind;
524
- namespace.emptyMentionAutocomplete = emptyMentionAutocomplete;
525
- namespace.setupMentionAutocomplete = setupMentionAutocomplete;
526
151
  namespace.scrollLastMessageIntoView = scrollLastMessageIntoView;
527
152
  namespace.containerBottomPadding = containerBottomPadding;
528
- namespace.signalOverlayOffset = signalOverlayOffset;
529
153
  namespace.prefersReducedMotion = prefersReducedMotion;
530
154
  namespace.scrollMessageIntoView = scrollMessageIntoView;
531
155
  })(window.TurboChatUI = window.TurboChatUI || {});