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
@@ -1,8 +1,6 @@
1
1
  (function (namespace) {
2
2
  var constants = namespace.constants || {};
3
3
  var MENTION_BLUR_HIDE_DELAY_MS = constants.MENTION_BLUR_HIDE_DELAY_MS || 120;
4
- var GLOBAL_SCROLLBAR_CLASS = "chat-global-scrollbar";
5
- var GLOBAL_SCROLLBAR_HIDDEN_CLASS = "chat-global-scrollbar--hidden";
6
4
 
7
5
  var datasetFlagEnabled = namespace.datasetFlagEnabled;
8
6
  var parseMentionTokens = namespace.parseMentionTokens;
@@ -12,7 +10,9 @@
12
10
  var setupMentionAutocomplete = namespace.setupMentionAutocomplete;
13
11
  var scrollMessageIntoView = namespace.scrollMessageIntoView;
14
12
  var scrollLastMessageIntoView = namespace.scrollLastMessageIntoView;
15
- var prefersReducedMotion = namespace.prefersReducedMotion || function () { return false; };
13
+ var setupUnboundedWheelScrollProxy = namespace.setupUnboundedWheelScrollProxy;
14
+ var syncAllGlobalScrollbars = namespace.syncAllGlobalScrollbars;
15
+ var cleanupDetachedGlobalScrollbars = namespace.cleanupDetachedGlobalScrollbars;
16
16
 
17
17
  function syncOwnMessageClasses(container) {
18
18
  if (!container || !container.dataset) {
@@ -414,6 +414,15 @@
414
414
  }
415
415
  }
416
416
 
417
+ function syncScrollFade(container) {
418
+ var scrolled = container.scrollTop > 10;
419
+ container.classList.toggle("chat-messages--scrolled", scrolled);
420
+ var chatWindow = container.closest(".chat-window");
421
+ if (chatWindow) {
422
+ chatWindow.classList.toggle("chat-window--scrolled", scrolled);
423
+ }
424
+ }
425
+
417
426
  function setupMessageAutoScroll(container) {
418
427
  if (!container || container.dataset.chatAutoscrollBound === "true") {
419
428
  return;
@@ -424,6 +433,7 @@
424
433
  syncOwnMessageClasses(container);
425
434
  syncMentionHighlights(container, { emitEvents: false });
426
435
  scrollLastMessageIntoView(container);
436
+ syncScrollFade(container);
427
437
  });
428
438
 
429
439
  var observer = new MutationObserver(function () {
@@ -431,379 +441,17 @@
431
441
  syncOwnMessageClasses(container);
432
442
  syncMentionHighlights(container, { emitEvents: true });
433
443
  scrollLastMessageIntoView(container);
444
+ syncScrollFade(container);
434
445
  });
435
446
  });
436
447
 
437
448
  observer.observe(container, { childList: true });
438
449
 
439
- setupUnboundedWheelScrollProxy(container);
440
- }
441
-
442
- function normalizeWheelDeltaY(event) {
443
- if (!event) {
444
- return 0;
445
- }
446
-
447
- var deltaY = event.deltaY;
448
- if (!deltaY) {
449
- return 0;
450
- }
451
-
452
- if (event.deltaMode === 1) {
453
- return deltaY * 16;
454
- }
455
-
456
- if (event.deltaMode === 2) {
457
- var viewportHeight = (typeof window !== "undefined" && window.innerHeight) || 800;
458
- return deltaY * viewportHeight;
459
- }
460
-
461
- return deltaY;
462
- }
463
-
464
- function shouldIgnoreWheelProxy(event, container) {
465
- if (!event) {
466
- return true;
467
- }
468
-
469
- if (container.contains(event.target)) {
470
- return true;
471
- }
472
-
473
- if (
474
- event.target &&
475
- typeof event.target.closest === "function" &&
476
- event.target.closest(
477
- ".chat-members-list-shell, .chat-invite-menu, .chat-mentions-menu, textarea, input, select, [contenteditable='true']"
478
- )
479
- ) {
480
- return true;
481
- }
482
-
483
- return false;
484
- }
485
-
486
- function clampScrollTop(container, scrollTop) {
487
- if (!container) {
488
- return 0;
489
- }
490
-
491
- var maxScrollTop = Math.max(0, container.scrollHeight - container.clientHeight);
492
- if (scrollTop < 0) {
493
- return 0;
494
- }
495
-
496
- if (scrollTop > maxScrollTop) {
497
- return maxScrollTop;
498
- }
499
-
500
- return scrollTop;
501
- }
502
-
503
- function startSmoothWheelScroll(state) {
504
- if (!state || state.smoothScrollRafId) {
505
- return;
506
- }
507
-
508
- function step() {
509
- state.smoothScrollRafId = null;
510
-
511
- var container = state.container;
512
- if (!container || !container.isConnected) {
513
- return;
514
- }
515
-
516
- var targetScrollTop = clampScrollTop(container, state.smoothTargetScrollTop);
517
- state.smoothTargetScrollTop = targetScrollTop;
518
-
519
- var currentScrollTop = container.scrollTop;
520
- var remainingDelta = targetScrollTop - currentScrollTop;
521
- if (Math.abs(remainingDelta) <= 0.5) {
522
- if (currentScrollTop !== targetScrollTop) {
523
- state.syncingFromWheelAnimation = true;
524
- container.scrollTop = targetScrollTop;
525
- state.syncingFromWheelAnimation = false;
526
- queueGlobalScrollbarSync(state);
527
- }
528
- return;
529
- }
530
-
531
- state.syncingFromWheelAnimation = true;
532
- container.scrollTop = currentScrollTop + remainingDelta * 0.22;
533
- state.syncingFromWheelAnimation = false;
534
- queueGlobalScrollbarSync(state);
535
- state.smoothScrollRafId = window.requestAnimationFrame(step);
536
- }
537
-
538
- state.smoothScrollRafId = window.requestAnimationFrame(step);
539
- }
540
-
541
- function proxyWheelToMessages(state, event) {
542
- var container = state && state.container;
543
- if (!container) {
544
- return false;
545
- }
546
-
547
- var deltaY = normalizeWheelDeltaY(event);
548
- if (!deltaY) {
549
- return false;
550
- }
551
-
552
- var currentTarget = typeof state.smoothTargetScrollTop === "number"
553
- ? state.smoothTargetScrollTop
554
- : container.scrollTop;
555
- var nextScrollTop = clampScrollTop(container, currentTarget + deltaY);
556
-
557
- if (nextScrollTop === currentTarget) {
558
- return false;
559
- }
560
-
561
- state.smoothTargetScrollTop = nextScrollTop;
562
- if (prefersReducedMotion()) {
563
- container.scrollTop = nextScrollTop;
564
- queueGlobalScrollbarSync(state);
565
- return true;
566
- }
567
-
568
- if (typeof window === "undefined" || typeof window.requestAnimationFrame !== "function") {
569
- container.scrollTop = nextScrollTop;
570
- queueGlobalScrollbarSync(state);
571
- return true;
572
- }
573
-
574
- startSmoothWheelScroll(state);
575
- return true;
576
- }
577
-
578
- function createGlobalScrollbar() {
579
- var scrollbar = document.createElement("div");
580
- scrollbar.className = GLOBAL_SCROLLBAR_CLASS + " " + GLOBAL_SCROLLBAR_HIDDEN_CLASS;
581
- scrollbar.setAttribute("aria-hidden", "true");
582
-
583
- var spacer = document.createElement("div");
584
- spacer.className = "chat-global-scrollbar-spacer";
585
- scrollbar.appendChild(spacer);
586
-
587
- if (document.body) {
588
- document.body.appendChild(scrollbar);
589
- }
590
-
591
- return { scrollbar: scrollbar, spacer: spacer };
592
- }
593
-
594
- function ensureGlobalScrollbar(state) {
595
- if (state.globalScrollbar && state.globalScrollbar.isConnected && state.globalScrollbarSpacer) {
596
- return;
597
- }
598
-
599
- var created = createGlobalScrollbar();
600
- state.globalScrollbar = created.scrollbar;
601
- state.globalScrollbarSpacer = created.spacer;
602
-
603
- state.globalScrollbar.addEventListener("scroll", function () {
604
- var container = state.container;
605
- if (!container || !container.isConnected || state.syncingFromMessages) {
606
- return;
607
- }
608
-
609
- state.syncingFromScrollbar = true;
610
- var targetTop = clampScrollTop(container, state.globalScrollbar.scrollTop);
611
- state.smoothTargetScrollTop = targetTop;
612
- container.scrollTop = targetTop;
613
- state.syncingFromScrollbar = false;
614
- });
615
- }
616
-
617
- function syncGlobalScrollbar(state) {
618
- var container = state.container;
619
- if (!container || !container.isConnected) {
620
- return;
621
- }
622
-
623
- ensureGlobalScrollbar(state);
624
- var globalScrollbar = state.globalScrollbar;
625
- var spacer = state.globalScrollbarSpacer;
626
- if (!globalScrollbar || !spacer) {
627
- return;
628
- }
629
-
630
- var maxScrollTop = Math.max(0, container.scrollHeight - container.clientHeight);
631
- var globalViewportHeight = globalScrollbar.clientHeight || ((typeof window !== "undefined" && window.innerHeight) || 0);
632
- var spacerHeight = Math.max(globalViewportHeight, Math.ceil(maxScrollTop + globalViewportHeight));
633
- spacer.style.height = spacerHeight + "px";
634
-
635
- var hideScrollbar = maxScrollTop <= 1;
636
- globalScrollbar.classList.toggle(GLOBAL_SCROLLBAR_HIDDEN_CLASS, hideScrollbar);
637
- if (hideScrollbar) {
638
- if (globalScrollbar.scrollTop !== 0) {
639
- globalScrollbar.scrollTop = 0;
640
- }
641
- return;
642
- }
643
-
644
- var targetScrollTop = Math.min(maxScrollTop, container.scrollTop);
645
- if (!state.smoothScrollRafId) {
646
- state.smoothTargetScrollTop = targetScrollTop;
647
- }
648
- if (Math.abs(globalScrollbar.scrollTop - targetScrollTop) > 1) {
649
- state.syncingFromMessages = true;
650
- globalScrollbar.scrollTop = targetScrollTop;
651
- state.syncingFromMessages = false;
652
- }
653
- }
654
-
655
- function queueGlobalScrollbarSync(state) {
656
- if (!state || state.syncRafId) {
657
- return;
658
- }
659
-
660
- if (typeof window === "undefined" || typeof window.requestAnimationFrame !== "function") {
661
- syncGlobalScrollbar(state);
662
- return;
663
- }
664
-
665
- state.syncRafId = window.requestAnimationFrame(function () {
666
- state.syncRafId = null;
667
- syncGlobalScrollbar(state);
668
- });
669
- }
670
-
671
- function bindContainerScrollProxy(state, container) {
672
- if (state.boundContainer === container) {
673
- return;
674
- }
675
-
676
- if (state.boundContainer && state.onContainerScroll) {
677
- state.boundContainer.removeEventListener("scroll", state.onContainerScroll);
678
- }
679
-
680
- if (state.mutationObserver) {
681
- state.mutationObserver.disconnect();
682
- }
683
-
684
- state.boundContainer = container;
685
- state.container = container;
450
+ container.addEventListener("scroll", function () {
451
+ syncScrollFade(container);
452
+ }, { passive: true });
686
453
 
687
- state.onContainerScroll = function () {
688
- if (!state.globalScrollbar || state.syncingFromScrollbar) {
689
- return;
690
- }
691
-
692
- if (!state.syncingFromWheelAnimation) {
693
- state.smoothTargetScrollTop = container.scrollTop;
694
- }
695
- state.syncingFromMessages = true;
696
- state.globalScrollbar.scrollTop = container.scrollTop;
697
- state.syncingFromMessages = false;
698
- };
699
- container.addEventListener("scroll", state.onContainerScroll, { passive: true });
700
-
701
- state.mutationObserver = new MutationObserver(function () {
702
- queueGlobalScrollbarSync(state);
703
- });
704
- state.mutationObserver.observe(container, { childList: true, subtree: true, characterData: true });
705
-
706
- queueGlobalScrollbarSync(state);
707
- }
708
-
709
- function cleanupDetachedGlobalScrollbars() {
710
- if (document.querySelector(".chat-shell--style-unbounded")) {
711
- return;
712
- }
713
-
714
- document.querySelectorAll("." + GLOBAL_SCROLLBAR_CLASS).forEach(function (node) {
715
- node.remove();
716
- });
717
- }
718
-
719
- function setupUnboundedWheelScrollProxy(container) {
720
- if (!container) {
721
- return;
722
- }
723
-
724
- var shell = container.closest(".chat-shell--style-unbounded");
725
- if (!shell) {
726
- return;
727
- }
728
-
729
- var state = shell.__chatWheelProxyState;
730
- if (!state) {
731
- state = {
732
- shell: shell,
733
- container: container,
734
- boundContainer: null,
735
- globalScrollbar: null,
736
- globalScrollbarSpacer: null,
737
- mutationObserver: null,
738
- syncingFromMessages: false,
739
- syncingFromScrollbar: false,
740
- syncRafId: null,
741
- onContainerScroll: null,
742
- smoothTargetScrollTop: container.scrollTop,
743
- smoothScrollRafId: null,
744
- syncingFromWheelAnimation: false
745
- };
746
- shell.__chatWheelProxyState = state;
747
-
748
- state.forwardWheel = function (event) {
749
- if (state.globalScrollbar && state.globalScrollbar.contains(event.target)) {
750
- return;
751
- }
752
-
753
- var activeContainer = state.container;
754
- if (!activeContainer || !activeContainer.isConnected) {
755
- return;
756
- }
757
-
758
- if (shouldIgnoreWheelProxy(event, activeContainer)) {
759
- return;
760
- }
761
-
762
- if (proxyWheelToMessages(state, event)) {
763
- event.preventDefault();
764
- }
765
- };
766
-
767
- state.forwardWheelOutsideShell = function (event) {
768
- if (!shell.isConnected || !state.container || !state.container.isConnected) {
769
- return;
770
- }
771
-
772
- if (shell.contains(event.target)) {
773
- return;
774
- }
775
-
776
- state.forwardWheel(event);
777
- };
778
-
779
- state.onViewportResize = function () {
780
- queueGlobalScrollbarSync(state);
781
- };
782
-
783
- shell.addEventListener("wheel", state.forwardWheel, { passive: false });
784
- document.addEventListener("wheel", state.forwardWheelOutsideShell, { passive: false });
785
- if (typeof window !== "undefined") {
786
- window.addEventListener("resize", state.onViewportResize);
787
- if (window.visualViewport && typeof window.visualViewport.addEventListener === "function") {
788
- window.visualViewport.addEventListener("resize", state.onViewportResize);
789
- }
790
- }
791
- } else {
792
- state.container = container;
793
- }
794
-
795
- bindContainerScrollProxy(state, container);
796
- }
797
-
798
- function syncAllGlobalScrollbars() {
799
- document.querySelectorAll(".chat-shell--style-unbounded").forEach(function (shell) {
800
- var state = shell.__chatWheelProxyState;
801
- if (!state || !state.container || !state.container.isConnected) {
802
- return;
803
- }
804
-
805
- queueGlobalScrollbarSync(state);
806
- });
454
+ setupUnboundedWheelScrollProxy(container);
807
455
  }
808
456
 
809
457
  function setupAllMessageAutoScroll() {
@@ -125,16 +125,9 @@
125
125
 
126
126
  container.classList.toggle("chat-signals--active", hasVisibleSignals);
127
127
 
128
- var chatWindow = container.closest(".chat-window");
129
- if (chatWindow) {
130
- var messagesContainer = chatWindow.querySelector(".chat-messages");
131
- var shouldStickToBottom = shouldStickMessagesToBottom(messagesContainer);
132
-
133
- var signalOffset = hasVisibleSignals ? Math.ceil(container.scrollHeight) + 8 : 0;
134
- chatWindow.style.setProperty("--chat-signal-offset", signalOffset + "px");
135
- chatWindow.classList.toggle("chat-window--signals-active", signalOffset > 0);
136
-
137
- if (messagesContainer && shouldStickToBottom) {
128
+ if (hasVisibleSignals) {
129
+ var messagesContainer = container.closest(".chat-messages");
130
+ if (messagesContainer && shouldStickMessagesToBottom(messagesContainer)) {
138
131
  requestAnimationFrame(function () {
139
132
  scrollLastMessageIntoView(messagesContainer);
140
133
  });