turbo_chat 0.1.8 → 0.1.11
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -0
- data/README.md +9 -0
- data/app/assets/javascripts/turbo_chat/lifecycle_events.js +10 -0
- data/app/assets/javascripts/turbo_chat/messages.js +374 -0
- data/app/assets/javascripts/turbo_chat/realtime.js +150 -12
- data/app/assets/javascripts/turbo_chat/shared.js +17 -2
- data/app/assets/stylesheets/turbo_chat/application.css +251 -1
- data/app/controllers/turbo_chat/chat_memberships_controller.rb +22 -28
- data/app/controllers/turbo_chat/chat_messages_controller.rb +45 -55
- data/app/controllers/turbo_chat/chats_controller.rb +69 -71
- data/app/helpers/turbo_chat/application_helper/config_support.rb +37 -38
- data/app/helpers/turbo_chat/application_helper/message_rendering.rb +37 -49
- data/app/helpers/turbo_chat/application_helper/participant_support.rb +18 -22
- data/app/models/turbo_chat/chat.rb +9 -18
- data/app/models/turbo_chat/chat_membership.rb +5 -19
- data/app/models/turbo_chat/chat_message/blocked_words_moderation.rb +20 -23
- data/app/models/turbo_chat/chat_message/signals.rb +7 -4
- data/app/models/turbo_chat/chat_message.rb +20 -39
- data/app/views/turbo_chat/chats/index.html.erb +6 -1
- data/app/views/turbo_chat/chats/show.html.erb +8 -1
- data/lib/generators/turbo_chat/install/templates/turbo_chat.rb +2 -0
- data/lib/turbo_chat/configuration/defaults.rb +1 -0
- data/lib/turbo_chat/messages.rb +4 -11
- data/lib/turbo_chat/version.rb +1 -1
- data/turbo_chat.gemspec +2 -2
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 97ae091b4d44fd093c6fbff38d64027c8cb85f45c5663f05e2a66cbcb15c043d
|
|
4
|
+
data.tar.gz: 3983790dec17e23c5ce5d99dd44f154e0d3b154ca43b9d4e595bf6491ac91822
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cc0717fd14c06e02daa493ed4887b962147e17b15d1d44880f9305be2cb51869066c3f29c08b8ad89b45cca4fa2e5ec13850d84fe56525a3ce5123eec8c8c936
|
|
7
|
+
data.tar.gz: 7f6d6ceecc1421e73b407d67da671e25fcb32adfdee714eaddacaa6187200e2c6a423f94e9ae955fc93546c2f2c78f3c659ebcf3a21313a8d5d25d1f49b0716b
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `turbo_chat` will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.1.11] - 2026-02-26
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- Smoothed signal-state transitions (for example: thinking to planning to typing) to reduce flicker during rapid updates.
|
|
9
|
+
- Updated signal Turbo Stream updates to use morphing so signal entries transition in place more smoothly.
|
|
10
|
+
|
|
11
|
+
## [0.1.10] - 2026-02-26
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- Unbounded chat now supports a viewport-edge scrollbar proxy on the far right side of the screen while preserving inner chat layout.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- Improved unbounded chat scrolling smoothness for wheel input and maintained synchronization between the edge scrollbar and message list position.
|
|
18
|
+
- Hardened page scroll locking for unbounded chat so the document does not scroll while chat remains scrollable.
|
|
19
|
+
|
|
20
|
+
## [0.1.9] - 2026-02-25
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- Support improved mobile chat behavior to keep the composer pinned and visible on iOS-class viewports, with safer full-screen height handling.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- Updated RubyGems metadata links to the `main` branch.
|
|
27
|
+
|
|
5
28
|
## [0.1.8] - 2026-02-23
|
|
6
29
|
|
|
7
30
|
### Added
|
data/README.md
CHANGED
|
@@ -126,6 +126,7 @@ TurboChat.configure do |config|
|
|
|
126
126
|
config.show_timestamp = true
|
|
127
127
|
config.show_role = false
|
|
128
128
|
config.message_source_labels = TurboChat::Configuration::DEFAULT_MESSAGE_SOURCE_LABELS.dup
|
|
129
|
+
config.chat_style = "chat_style_bounded"
|
|
129
130
|
config.signal_text_sheen = true
|
|
130
131
|
|
|
131
132
|
config.emit_moderation_events = false
|
|
@@ -174,6 +175,14 @@ TurboChat.configure do |config|
|
|
|
174
175
|
end
|
|
175
176
|
```
|
|
176
177
|
|
|
178
|
+
Chat UI layout style is configurable:
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
TurboChat.configure do |config|
|
|
182
|
+
config.chat_style = "chat_style_unbounded" # or "chat_style_bounded"
|
|
183
|
+
end
|
|
184
|
+
```
|
|
185
|
+
|
|
177
186
|
## Roles
|
|
178
187
|
|
|
179
188
|
Built-in roles:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
(function (namespace) {
|
|
2
2
|
var datasetFlagEnabled = namespace.datasetFlagEnabled;
|
|
3
3
|
var parseJsonObject = namespace.parseJsonObject;
|
|
4
|
+
var PAGE_SCROLL_LOCK_CLASS = "chat-page-scroll-locked";
|
|
4
5
|
|
|
5
6
|
function setupInvitationEvents() {
|
|
6
7
|
document.querySelectorAll("[data-chat-index]").forEach(function (element) {
|
|
@@ -64,7 +65,16 @@
|
|
|
64
65
|
});
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
function syncPageScrollLock() {
|
|
69
|
+
var hasUnboundedShell = document.querySelector(".chat-shell--style-unbounded");
|
|
70
|
+
document.documentElement.classList.toggle(PAGE_SCROLL_LOCK_CLASS, Boolean(hasUnboundedShell));
|
|
71
|
+
if (document.body) {
|
|
72
|
+
document.body.classList.toggle(PAGE_SCROLL_LOCK_CLASS, Boolean(hasUnboundedShell));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
67
76
|
function setupTurboChatUi() {
|
|
77
|
+
syncPageScrollLock();
|
|
68
78
|
setupInvitationEvents();
|
|
69
79
|
setupChatLifecycleEvents();
|
|
70
80
|
|
|
@@ -1,6 +1,8 @@
|
|
|
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";
|
|
4
6
|
|
|
5
7
|
var datasetFlagEnabled = namespace.datasetFlagEnabled;
|
|
6
8
|
var parseMentionTokens = namespace.parseMentionTokens;
|
|
@@ -10,6 +12,7 @@
|
|
|
10
12
|
var setupMentionAutocomplete = namespace.setupMentionAutocomplete;
|
|
11
13
|
var scrollMessageIntoView = namespace.scrollMessageIntoView;
|
|
12
14
|
var scrollLastMessageIntoView = namespace.scrollLastMessageIntoView;
|
|
15
|
+
var prefersReducedMotion = namespace.prefersReducedMotion || function () { return false; };
|
|
13
16
|
|
|
14
17
|
function syncOwnMessageClasses(container) {
|
|
15
18
|
if (!container || !container.dataset) {
|
|
@@ -432,10 +435,381 @@
|
|
|
432
435
|
});
|
|
433
436
|
|
|
434
437
|
observer.observe(container, { childList: true });
|
|
438
|
+
|
|
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;
|
|
686
|
+
|
|
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
|
+
});
|
|
435
807
|
}
|
|
436
808
|
|
|
437
809
|
function setupAllMessageAutoScroll() {
|
|
438
810
|
document.querySelectorAll(".chat-messages").forEach(setupMessageAutoScroll);
|
|
811
|
+
syncAllGlobalScrollbars();
|
|
812
|
+
cleanupDetachedGlobalScrollbars();
|
|
439
813
|
}
|
|
440
814
|
|
|
441
815
|
namespace.setupAllMessageAutoScroll = setupAllMessageAutoScroll;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
var SIGNAL_IDLE_GRACE_MS = constants.SIGNAL_IDLE_GRACE_MS || 2500;
|
|
6
6
|
var SIGNAL_HEARTBEAT_MS = constants.SIGNAL_HEARTBEAT_MS || 4000;
|
|
7
7
|
var SIGNAL_RETREAT_MS = constants.SIGNAL_RETREAT_MS || 180;
|
|
8
|
+
var SIGNAL_EMPTY_GRACE_MS = constants.SIGNAL_EMPTY_GRACE_MS || 180;
|
|
8
9
|
var MENTION_BLUR_HIDE_DELAY_MS = constants.MENTION_BLUR_HIDE_DELAY_MS || 120;
|
|
9
10
|
var COMPOSER_MAX_HEIGHT_PX = constants.COMPOSER_MAX_HEIGHT_PX || 210;
|
|
10
11
|
|
|
@@ -40,26 +41,89 @@
|
|
|
40
41
|
});
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
function
|
|
44
|
+
function containerBottomPadding(container) {
|
|
45
|
+
if (!container || typeof window === "undefined") {
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
var cssPadding = window.getComputedStyle(container).paddingBottom;
|
|
50
|
+
var parsedPadding = parseFloat(cssPadding);
|
|
51
|
+
if (isNaN(parsedPadding) || parsedPadding <= 0) {
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return parsedPadding;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function visibleSignalNode(container) {
|
|
44
59
|
if (!container) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return container.querySelector(".chat-typing-indicator:not(.chat-typing-indicator--leaving)");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function clearSignalDeactivateTimer(container) {
|
|
67
|
+
if (!container || !container.__chatSignalDeactivateTimeoutId) {
|
|
45
68
|
return;
|
|
46
69
|
}
|
|
47
70
|
|
|
71
|
+
clearTimeout(container.__chatSignalDeactivateTimeoutId);
|
|
72
|
+
container.__chatSignalDeactivateTimeoutId = null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function queueSignalDeactivate(container) {
|
|
76
|
+
if (!container || container.__chatSignalDeactivateTimeoutId) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
container.__chatSignalDeactivateTimeoutId = setTimeout(function () {
|
|
81
|
+
container.__chatSignalDeactivateTimeoutId = null;
|
|
82
|
+
if (!container.isConnected) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
syncSignalContainerState(container, { forceInactive: true });
|
|
87
|
+
}, SIGNAL_EMPTY_GRACE_MS);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function shouldStickMessagesToBottom(messagesContainer) {
|
|
91
|
+
if (!messagesContainer) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
var reservedBottomPadding = containerBottomPadding(messagesContainer);
|
|
96
|
+
var distanceFromBottom = messagesContainer.scrollHeight -
|
|
97
|
+
(messagesContainer.scrollTop + messagesContainer.clientHeight) -
|
|
98
|
+
reservedBottomPadding;
|
|
99
|
+
return distanceFromBottom <= 24;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function syncSignalContainerState(container, options) {
|
|
103
|
+
if (!container) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
options = options || {};
|
|
48
108
|
hideOwnSignals(container);
|
|
49
109
|
|
|
50
|
-
var hasVisibleSignals = container
|
|
51
|
-
|
|
52
|
-
)
|
|
53
|
-
|
|
110
|
+
var hasVisibleSignals = Boolean(visibleSignalNode(container));
|
|
111
|
+
|
|
112
|
+
if (!hasVisibleSignals && !options.forceInactive && container.classList.contains("chat-signals--active")) {
|
|
113
|
+
queueSignalDeactivate(container);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (hasVisibleSignals) {
|
|
118
|
+
clearSignalDeactivateTimer(container);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
container.classList.toggle("chat-signals--active", hasVisibleSignals);
|
|
54
122
|
|
|
55
123
|
var chatWindow = container.closest(".chat-window");
|
|
56
124
|
if (chatWindow) {
|
|
57
125
|
var messagesContainer = chatWindow.querySelector(".chat-messages");
|
|
58
|
-
var shouldStickToBottom =
|
|
59
|
-
if (messagesContainer) {
|
|
60
|
-
var distanceFromBottom = messagesContainer.scrollHeight - (messagesContainer.scrollTop + messagesContainer.clientHeight);
|
|
61
|
-
shouldStickToBottom = distanceFromBottom <= 24;
|
|
62
|
-
}
|
|
126
|
+
var shouldStickToBottom = shouldStickMessagesToBottom(messagesContainer);
|
|
63
127
|
|
|
64
128
|
var signalOffset = hasVisibleSignals ? Math.ceil(container.scrollHeight) + 8 : 0;
|
|
65
129
|
chatWindow.style.setProperty("--chat-signal-offset", signalOffset + "px");
|
|
@@ -86,11 +150,31 @@
|
|
|
86
150
|
container.dataset.chatSignalsBound = "true";
|
|
87
151
|
syncSignalContainerState(container);
|
|
88
152
|
|
|
153
|
+
var syncRafId = null;
|
|
154
|
+
function queueSignalContainerSync() {
|
|
155
|
+
if (syncRafId) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (typeof window === "undefined" || typeof window.requestAnimationFrame !== "function") {
|
|
160
|
+
syncSignalContainerState(container);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
syncRafId = window.requestAnimationFrame(function () {
|
|
165
|
+
syncRafId = null;
|
|
166
|
+
if (!container.isConnected) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
syncSignalContainerState(container);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
89
173
|
var observer = new MutationObserver(function () {
|
|
90
|
-
|
|
174
|
+
queueSignalContainerSync();
|
|
91
175
|
});
|
|
92
176
|
|
|
93
|
-
observer.observe(container, { childList: true });
|
|
177
|
+
observer.observe(container, { childList: true, subtree: true, characterData: true });
|
|
94
178
|
}
|
|
95
179
|
|
|
96
180
|
function setupAllSignalContainers() {
|
|
@@ -154,6 +238,8 @@
|
|
|
154
238
|
var pendingSignalClear = false;
|
|
155
239
|
var emitTypingEvents = datasetFlagEnabled(element, "chatEmitTypingEvents");
|
|
156
240
|
var emitMessageEvents = datasetFlagEnabled(element, "chatEmitMessageEvents");
|
|
241
|
+
var unboundedShell = element.closest(".chat-shell--style-unbounded");
|
|
242
|
+
var composerClearanceRafId = null;
|
|
157
243
|
var mentionsEnabled = datasetFlagEnabled(element, "chatEnableMentions");
|
|
158
244
|
var mentionOptions = mentionsEnabled ? parseMentionOptions(element.dataset.chatMentionOptions) : [];
|
|
159
245
|
var mentionAutocomplete = setupMentionAutocomplete(messageInput, {
|
|
@@ -177,15 +263,67 @@
|
|
|
177
263
|
setMentionOptions: updateMentionOptions
|
|
178
264
|
};
|
|
179
265
|
|
|
266
|
+
function syncComposerClearance() {
|
|
267
|
+
if (!unboundedShell) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
var viewportHeight = (typeof window !== "undefined" && window.innerHeight) || document.documentElement.clientHeight || 0;
|
|
272
|
+
var composerRect = element.getBoundingClientRect();
|
|
273
|
+
if (viewportHeight > 0 && composerRect && composerRect.height > 0) {
|
|
274
|
+
var coveredHeight = Math.max(0, viewportHeight - composerRect.top);
|
|
275
|
+
if (coveredHeight > 0) {
|
|
276
|
+
unboundedShell.style.setProperty("--chat-floating-composer-clearance", Math.ceil(coveredHeight + 8) + "px");
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
var composerShell = element.querySelector(".chat-composer-shell");
|
|
282
|
+
if (!composerShell) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
var composerHeight = Math.ceil(composerShell.getBoundingClientRect().height);
|
|
287
|
+
if (!composerHeight || composerHeight <= 0) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
var clearance = composerHeight + 28;
|
|
292
|
+
unboundedShell.style.setProperty("--chat-floating-composer-clearance", clearance + "px");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function queueComposerClearanceSync() {
|
|
296
|
+
if (!unboundedShell || typeof window === "undefined" || typeof window.requestAnimationFrame !== "function") {
|
|
297
|
+
syncComposerClearance();
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (composerClearanceRafId) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
composerClearanceRafId = window.requestAnimationFrame(function () {
|
|
306
|
+
composerClearanceRafId = null;
|
|
307
|
+
syncComposerClearance();
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
180
311
|
function autoResizeComposerInput() {
|
|
181
312
|
messageInput.style.height = "auto";
|
|
182
313
|
var contentHeight = messageInput.scrollHeight;
|
|
183
314
|
var nextHeight = Math.min(contentHeight, COMPOSER_MAX_HEIGHT_PX);
|
|
184
315
|
messageInput.style.height = nextHeight + "px";
|
|
185
316
|
messageInput.style.overflowY = contentHeight > COMPOSER_MAX_HEIGHT_PX ? "auto" : "hidden";
|
|
317
|
+
queueComposerClearanceSync();
|
|
186
318
|
}
|
|
187
319
|
|
|
188
320
|
autoResizeComposerInput();
|
|
321
|
+
if (typeof window !== "undefined") {
|
|
322
|
+
window.addEventListener("resize", queueComposerClearanceSync);
|
|
323
|
+
if (window.visualViewport && typeof window.visualViewport.addEventListener === "function") {
|
|
324
|
+
window.visualViewport.addEventListener("resize", queueComposerClearanceSync);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
189
327
|
|
|
190
328
|
function emitTypingEvent(eventName) {
|
|
191
329
|
if (!emitTypingEvents) {
|