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
@@ -0,0 +1,379 @@
1
+ (function () {
2
+ "use strict";
3
+ var namespace = (window.TurboChatUI = window.TurboChatUI || {});
4
+ var prefersReducedMotion = namespace.prefersReducedMotion || function () { return false; };
5
+
6
+ var GLOBAL_SCROLLBAR_CLASS = "chat-global-scrollbar";
7
+ var GLOBAL_SCROLLBAR_HIDDEN_CLASS = "chat-global-scrollbar--hidden";
8
+
9
+ function normalizeWheelDeltaY(event) {
10
+ if (!event) {
11
+ return 0;
12
+ }
13
+
14
+ var deltaY = event.deltaY;
15
+ if (!deltaY) {
16
+ return 0;
17
+ }
18
+
19
+ if (event.deltaMode === 1) {
20
+ return deltaY * 16;
21
+ }
22
+
23
+ if (event.deltaMode === 2) {
24
+ var viewportHeight = (typeof window !== "undefined" && window.innerHeight) || 800;
25
+ return deltaY * viewportHeight;
26
+ }
27
+
28
+ return deltaY;
29
+ }
30
+
31
+ function shouldIgnoreWheelProxy(event, container) {
32
+ if (!event) {
33
+ return true;
34
+ }
35
+
36
+ if (container.contains(event.target)) {
37
+ return true;
38
+ }
39
+
40
+ if (
41
+ event.target &&
42
+ typeof event.target.closest === "function" &&
43
+ event.target.closest(
44
+ ".chat-members-list-shell, .chat-invite-menu, .chat-mentions-menu, textarea, input, select, [contenteditable='true']"
45
+ )
46
+ ) {
47
+ return true;
48
+ }
49
+
50
+ return false;
51
+ }
52
+
53
+ function clampScrollTop(container, scrollTop) {
54
+ if (!container) {
55
+ return 0;
56
+ }
57
+
58
+ var maxScrollTop = Math.max(0, container.scrollHeight - container.clientHeight);
59
+ if (scrollTop < 0) {
60
+ return 0;
61
+ }
62
+
63
+ if (scrollTop > maxScrollTop) {
64
+ return maxScrollTop;
65
+ }
66
+
67
+ return scrollTop;
68
+ }
69
+
70
+ function startSmoothWheelScroll(state) {
71
+ if (!state || state.smoothScrollRafId) {
72
+ return;
73
+ }
74
+
75
+ function step() {
76
+ state.smoothScrollRafId = null;
77
+
78
+ var container = state.container;
79
+ if (!container || !container.isConnected) {
80
+ return;
81
+ }
82
+
83
+ var targetScrollTop = clampScrollTop(container, state.smoothTargetScrollTop);
84
+ state.smoothTargetScrollTop = targetScrollTop;
85
+
86
+ var currentScrollTop = container.scrollTop;
87
+ var remainingDelta = targetScrollTop - currentScrollTop;
88
+ if (Math.abs(remainingDelta) <= 0.5) {
89
+ if (currentScrollTop !== targetScrollTop) {
90
+ state.syncingFromWheelAnimation = true;
91
+ container.scrollTop = targetScrollTop;
92
+ state.syncingFromWheelAnimation = false;
93
+ queueGlobalScrollbarSync(state);
94
+ }
95
+ return;
96
+ }
97
+
98
+ state.syncingFromWheelAnimation = true;
99
+ container.scrollTop = currentScrollTop + remainingDelta * 0.22;
100
+ state.syncingFromWheelAnimation = false;
101
+ queueGlobalScrollbarSync(state);
102
+ state.smoothScrollRafId = window.requestAnimationFrame(step);
103
+ }
104
+
105
+ state.smoothScrollRafId = window.requestAnimationFrame(step);
106
+ }
107
+
108
+ function proxyWheelToMessages(state, event) {
109
+ var container = state && state.container;
110
+ if (!container) {
111
+ return false;
112
+ }
113
+
114
+ var deltaY = normalizeWheelDeltaY(event);
115
+ if (!deltaY) {
116
+ return false;
117
+ }
118
+
119
+ var currentTarget = typeof state.smoothTargetScrollTop === "number"
120
+ ? state.smoothTargetScrollTop
121
+ : container.scrollTop;
122
+ var nextScrollTop = clampScrollTop(container, currentTarget + deltaY);
123
+
124
+ if (nextScrollTop === currentTarget) {
125
+ return false;
126
+ }
127
+
128
+ state.smoothTargetScrollTop = nextScrollTop;
129
+ if (prefersReducedMotion()) {
130
+ container.scrollTop = nextScrollTop;
131
+ queueGlobalScrollbarSync(state);
132
+ return true;
133
+ }
134
+
135
+ if (typeof window === "undefined" || typeof window.requestAnimationFrame !== "function") {
136
+ container.scrollTop = nextScrollTop;
137
+ queueGlobalScrollbarSync(state);
138
+ return true;
139
+ }
140
+
141
+ startSmoothWheelScroll(state);
142
+ return true;
143
+ }
144
+
145
+ function createGlobalScrollbar() {
146
+ var scrollbar = document.createElement("div");
147
+ scrollbar.className = GLOBAL_SCROLLBAR_CLASS + " " + GLOBAL_SCROLLBAR_HIDDEN_CLASS;
148
+ scrollbar.setAttribute("aria-hidden", "true");
149
+
150
+ var spacer = document.createElement("div");
151
+ spacer.className = "chat-global-scrollbar-spacer";
152
+ scrollbar.appendChild(spacer);
153
+
154
+ if (document.body) {
155
+ document.body.appendChild(scrollbar);
156
+ }
157
+
158
+ return { scrollbar: scrollbar, spacer: spacer };
159
+ }
160
+
161
+ function ensureGlobalScrollbar(state) {
162
+ if (state.globalScrollbar && state.globalScrollbar.isConnected && state.globalScrollbarSpacer) {
163
+ return;
164
+ }
165
+
166
+ var created = createGlobalScrollbar();
167
+ state.globalScrollbar = created.scrollbar;
168
+ state.globalScrollbarSpacer = created.spacer;
169
+
170
+ state.globalScrollbar.addEventListener("scroll", function () {
171
+ var container = state.container;
172
+ if (!container || !container.isConnected || state.syncingFromMessages) {
173
+ return;
174
+ }
175
+
176
+ state.syncingFromScrollbar = true;
177
+ var targetTop = clampScrollTop(container, state.globalScrollbar.scrollTop);
178
+ state.smoothTargetScrollTop = targetTop;
179
+ container.scrollTop = targetTop;
180
+ state.syncingFromScrollbar = false;
181
+ });
182
+ }
183
+
184
+ function syncGlobalScrollbar(state) {
185
+ var container = state.container;
186
+ if (!container || !container.isConnected) {
187
+ return;
188
+ }
189
+
190
+ ensureGlobalScrollbar(state);
191
+ var globalScrollbar = state.globalScrollbar;
192
+ var spacer = state.globalScrollbarSpacer;
193
+ if (!globalScrollbar || !spacer) {
194
+ return;
195
+ }
196
+
197
+ var maxScrollTop = Math.max(0, container.scrollHeight - container.clientHeight);
198
+ var globalViewportHeight = globalScrollbar.clientHeight || ((typeof window !== "undefined" && window.innerHeight) || 0);
199
+ var spacerHeight = Math.max(globalViewportHeight, Math.ceil(maxScrollTop + globalViewportHeight));
200
+ spacer.style.height = spacerHeight + "px";
201
+
202
+ var hideScrollbar = maxScrollTop <= 1;
203
+ globalScrollbar.classList.toggle(GLOBAL_SCROLLBAR_HIDDEN_CLASS, hideScrollbar);
204
+ if (hideScrollbar) {
205
+ if (globalScrollbar.scrollTop !== 0) {
206
+ globalScrollbar.scrollTop = 0;
207
+ }
208
+ return;
209
+ }
210
+
211
+ var targetScrollTop = Math.min(maxScrollTop, container.scrollTop);
212
+ if (!state.smoothScrollRafId) {
213
+ state.smoothTargetScrollTop = targetScrollTop;
214
+ }
215
+ if (Math.abs(globalScrollbar.scrollTop - targetScrollTop) > 1) {
216
+ state.syncingFromMessages = true;
217
+ globalScrollbar.scrollTop = targetScrollTop;
218
+ state.syncingFromMessages = false;
219
+ }
220
+ }
221
+
222
+ function queueGlobalScrollbarSync(state) {
223
+ if (!state || state.syncRafId) {
224
+ return;
225
+ }
226
+
227
+ if (typeof window === "undefined" || typeof window.requestAnimationFrame !== "function") {
228
+ syncGlobalScrollbar(state);
229
+ return;
230
+ }
231
+
232
+ state.syncRafId = window.requestAnimationFrame(function () {
233
+ state.syncRafId = null;
234
+ syncGlobalScrollbar(state);
235
+ });
236
+ }
237
+
238
+ function bindContainerScrollProxy(state, container) {
239
+ if (state.boundContainer === container) {
240
+ return;
241
+ }
242
+
243
+ if (state.boundContainer && state.onContainerScroll) {
244
+ state.boundContainer.removeEventListener("scroll", state.onContainerScroll);
245
+ }
246
+
247
+ if (state.mutationObserver) {
248
+ state.mutationObserver.disconnect();
249
+ }
250
+
251
+ state.boundContainer = container;
252
+ state.container = container;
253
+
254
+ state.onContainerScroll = function () {
255
+ if (!state.globalScrollbar || state.syncingFromScrollbar) {
256
+ return;
257
+ }
258
+
259
+ if (!state.syncingFromWheelAnimation) {
260
+ state.smoothTargetScrollTop = container.scrollTop;
261
+ }
262
+ state.syncingFromMessages = true;
263
+ state.globalScrollbar.scrollTop = container.scrollTop;
264
+ state.syncingFromMessages = false;
265
+ };
266
+ container.addEventListener("scroll", state.onContainerScroll, { passive: true });
267
+
268
+ state.mutationObserver = new MutationObserver(function () {
269
+ queueGlobalScrollbarSync(state);
270
+ });
271
+ state.mutationObserver.observe(container, { childList: true, subtree: true, characterData: true });
272
+
273
+ queueGlobalScrollbarSync(state);
274
+ }
275
+
276
+ function cleanupDetachedGlobalScrollbars() {
277
+ if (document.querySelector(".chat-shell--style-unbounded")) {
278
+ return;
279
+ }
280
+
281
+ document.querySelectorAll("." + GLOBAL_SCROLLBAR_CLASS).forEach(function (node) {
282
+ node.remove();
283
+ });
284
+ }
285
+
286
+ function setupUnboundedWheelScrollProxy(container) {
287
+ if (!container) {
288
+ return;
289
+ }
290
+
291
+ var shell = container.closest(".chat-shell--style-unbounded");
292
+ if (!shell) {
293
+ return;
294
+ }
295
+
296
+ var state = shell.__chatWheelProxyState;
297
+ if (!state) {
298
+ state = {
299
+ shell: shell,
300
+ container: container,
301
+ boundContainer: null,
302
+ globalScrollbar: null,
303
+ globalScrollbarSpacer: null,
304
+ mutationObserver: null,
305
+ syncingFromMessages: false,
306
+ syncingFromScrollbar: false,
307
+ syncRafId: null,
308
+ onContainerScroll: null,
309
+ smoothTargetScrollTop: container.scrollTop,
310
+ smoothScrollRafId: null,
311
+ syncingFromWheelAnimation: false
312
+ };
313
+ shell.__chatWheelProxyState = state;
314
+
315
+ state.forwardWheel = function (event) {
316
+ if (state.globalScrollbar && state.globalScrollbar.contains(event.target)) {
317
+ return;
318
+ }
319
+
320
+ var activeContainer = state.container;
321
+ if (!activeContainer || !activeContainer.isConnected) {
322
+ return;
323
+ }
324
+
325
+ if (shouldIgnoreWheelProxy(event, activeContainer)) {
326
+ return;
327
+ }
328
+
329
+ if (proxyWheelToMessages(state, event)) {
330
+ event.preventDefault();
331
+ }
332
+ };
333
+
334
+ state.forwardWheelOutsideShell = function (event) {
335
+ if (!shell.isConnected || !state.container || !state.container.isConnected) {
336
+ return;
337
+ }
338
+
339
+ if (shell.contains(event.target)) {
340
+ return;
341
+ }
342
+
343
+ state.forwardWheel(event);
344
+ };
345
+
346
+ state.onViewportResize = function () {
347
+ queueGlobalScrollbarSync(state);
348
+ };
349
+
350
+ shell.addEventListener("wheel", state.forwardWheel, { passive: false });
351
+ document.addEventListener("wheel", state.forwardWheelOutsideShell, { passive: false });
352
+ if (typeof window !== "undefined") {
353
+ window.addEventListener("resize", state.onViewportResize);
354
+ if (window.visualViewport && typeof window.visualViewport.addEventListener === "function") {
355
+ window.visualViewport.addEventListener("resize", state.onViewportResize);
356
+ }
357
+ }
358
+ } else {
359
+ state.container = container;
360
+ }
361
+
362
+ bindContainerScrollProxy(state, container);
363
+ }
364
+
365
+ function syncAllGlobalScrollbars() {
366
+ document.querySelectorAll(".chat-shell--style-unbounded").forEach(function (shell) {
367
+ var state = shell.__chatWheelProxyState;
368
+ if (!state || !state.container || !state.container.isConnected) {
369
+ return;
370
+ }
371
+
372
+ queueGlobalScrollbarSync(state);
373
+ });
374
+ }
375
+
376
+ namespace.setupUnboundedWheelScrollProxy = setupUnboundedWheelScrollProxy;
377
+ namespace.syncAllGlobalScrollbars = syncAllGlobalScrollbars;
378
+ namespace.cleanupDetachedGlobalScrollbars = cleanupDetachedGlobalScrollbars;
379
+ })();