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
@@ -113,6 +113,8 @@
113
113
  var filteredOptions = [];
114
114
  var activeIndex = 0;
115
115
  var selectedOption = null;
116
+ var submitBtn = form.querySelector("[data-chat-invite-submit]");
117
+ if (submitBtn) submitBtn.disabled = true;
116
118
 
117
119
  function persistOptions() {
118
120
  form.dataset.chatInviteOptions = serializeOptions(allOptions);
@@ -121,6 +123,7 @@
121
123
  function clearSelection() {
122
124
  selectedOption = null;
123
125
  participantIdInput.value = "";
126
+ if (submitBtn) submitBtn.disabled = true;
124
127
  }
125
128
 
126
129
  function setSelection(optionData) {
@@ -133,7 +136,9 @@
133
136
  participantIdInput.value = optionData.participantId;
134
137
  queryInput.value = optionData.label;
135
138
  queryInput.setCustomValidity("");
139
+ if (submitBtn) submitBtn.disabled = false;
136
140
  hideMenu();
141
+ hideHint();
137
142
  }
138
143
 
139
144
  function setActiveIndex(index) {
@@ -202,10 +207,14 @@
202
207
  if (exactMatch) {
203
208
  selectedOption = exactMatch;
204
209
  participantIdInput.value = exactMatch.participantId;
205
- } else {
206
- clearSelection();
210
+ if (submitBtn) submitBtn.disabled = false;
211
+ hideMenu();
212
+ hideHint();
213
+ return;
207
214
  }
208
215
 
216
+ clearSelection();
217
+
209
218
  if (query.length < MIN_QUERY_LENGTH) {
210
219
  hideMenu();
211
220
  if (!query.length) {
@@ -216,6 +225,12 @@
216
225
  return;
217
226
  }
218
227
 
228
+ if (!allOptions.length && query.length) {
229
+ hideMenu();
230
+ showHint("All eligible participants have been invited.");
231
+ return;
232
+ }
233
+
219
234
  filteredOptions = matchingOptions(allOptions, query);
220
235
  if (!filteredOptions.length) {
221
236
  hideMenu();
@@ -352,6 +367,7 @@
352
367
  setTimeout(function () {
353
368
  clearSelection();
354
369
  queryInput.value = "";
370
+ if (submitBtn) submitBtn.disabled = true;
355
371
  syncQueryState();
356
372
  }, 0);
357
373
  });
@@ -367,401 +383,12 @@
367
383
  return form.__chatInvitePickerApi;
368
384
  }
369
385
 
370
- function parseMemberEntry(node) {
371
- if (!node || !node.dataset || node.dataset.chatMemberEntry !== "true") {
372
- return null;
373
- }
374
-
375
- var chatId = String(node.dataset.chatId || "").trim();
376
- var participantType = String(node.dataset.chatMemberParticipantType || "").trim();
377
- var participantId = String(node.dataset.chatMemberParticipantId || "").trim();
378
- var mentionToken = String(node.dataset.chatMemberMentionToken || "").trim();
379
- var roleKey = String(node.dataset.chatMemberRoleKey || "").trim();
380
- var roleName = String(node.dataset.chatMemberRoleName || "").trim();
381
- var roleRank = parseInt(node.dataset.chatMemberRoleRank || "-1", 10);
382
- var name = String(node.dataset.chatMemberName || "").trim();
383
- var inviteLabel = String(node.dataset.chatMemberInviteLabel || "").trim();
384
- var search = normalize(node.dataset.chatMemberSearch || inviteLabel || name);
385
-
386
- if (!chatId || !participantType || !participantId) {
387
- return null;
388
- }
389
-
390
- return {
391
- identity: participantType + ":" + participantId,
392
- chatId: chatId,
393
- participantType: participantType,
394
- participantId: participantId,
395
- mentionToken: mentionToken,
396
- name: name,
397
- roleKey: roleKey,
398
- roleName: roleName,
399
- roleRank: isNaN(roleRank) ? -1 : roleRank,
400
- inviteOption: {
401
- label: inviteLabel,
402
- participantId: participantId,
403
- search: search
404
- }
405
- };
406
- }
407
-
408
- function collectMemberEntries(sourceNode, entryMap) {
409
- if (!sourceNode || sourceNode.nodeType !== 1) {
410
- return;
411
- }
412
-
413
- var sourceElement = sourceNode;
414
- var sourceEntry = parseMemberEntry(sourceElement);
415
- if (sourceEntry) {
416
- entryMap[sourceEntry.identity] = sourceEntry;
417
- }
418
-
419
- if (typeof sourceElement.querySelectorAll !== "function") {
420
- return;
421
- }
422
-
423
- sourceElement.querySelectorAll("[data-chat-member-entry='true']").forEach(function (memberEntryNode) {
424
- var memberEntry = parseMemberEntry(memberEntryNode);
425
- if (!memberEntry) {
426
- return;
427
- }
428
-
429
- entryMap[memberEntry.identity] = memberEntry;
430
- });
431
- }
432
-
433
- function findInvitePickerApis(chatId) {
434
- var apis = [];
435
- var normalizedChatId = String(chatId || "").trim();
436
- if (!normalizedChatId) {
437
- return apis;
438
- }
439
-
440
- document.querySelectorAll("[data-chat-invite-form]").forEach(function (form) {
441
- if (String(form.dataset.chatId || "").trim() !== normalizedChatId) {
442
- return;
443
- }
444
-
445
- var api = setupInvitePicker(form);
446
- if (api) {
447
- apis.push(api);
448
- }
449
- });
450
-
451
- return apis;
452
- }
453
-
454
- function memberListForChat(chatId) {
455
- var normalizedChatId = String(chatId || "").trim();
456
- if (!normalizedChatId) {
457
- return null;
458
- }
459
-
460
- var matchedList = null;
461
- document.querySelectorAll("[data-chat-member-list]").forEach(function (memberList) {
462
- if (!matchedList && String(memberList.dataset.chatId || "").trim() === normalizedChatId) {
463
- matchedList = memberList;
464
- }
465
- });
466
- return matchedList;
467
- }
468
-
469
- function shellForChat(chatId) {
470
- var normalizedChatId = String(chatId || "").trim();
471
- if (!normalizedChatId) {
472
- return null;
473
- }
474
-
475
- var matchedShell = null;
476
- document.querySelectorAll(".chat-shell[data-chat-id]").forEach(function (shell) {
477
- if (!matchedShell && String(shell.dataset.chatId || "").trim() === normalizedChatId) {
478
- matchedShell = shell;
479
- }
480
- });
481
- return matchedShell;
482
- }
483
-
484
- function syncInviteOptionsFromMemberMutations(chatId, mutationRecords) {
485
- var invitePickerApis = findInvitePickerApis(chatId);
486
- if (!invitePickerApis.length) {
487
- return;
488
- }
489
-
490
- var removedEntries = {};
491
- var addedEntries = {};
492
-
493
- mutationRecords.forEach(function (record) {
494
- if (!record) {
495
- return;
496
- }
497
-
498
- record.removedNodes.forEach(function (removedNode) {
499
- collectMemberEntries(removedNode, removedEntries);
500
- });
501
-
502
- record.addedNodes.forEach(function (addedNode) {
503
- collectMemberEntries(addedNode, addedEntries);
504
- });
505
- });
506
-
507
- Object.keys(removedEntries).forEach(function (entryKey) {
508
- if (addedEntries[entryKey]) {
509
- return;
510
- }
511
-
512
- var removedEntry = removedEntries[entryKey];
513
- invitePickerApis.forEach(function (invitePickerApi) {
514
- invitePickerApi.upsertOption(removedEntry.inviteOption);
515
- });
516
- });
517
-
518
- Object.keys(addedEntries).forEach(function (entryKey) {
519
- var addedEntry = addedEntries[entryKey];
520
- invitePickerApis.forEach(function (invitePickerApi) {
521
- invitePickerApi.removeParticipant(addedEntry.participantId);
522
- });
523
- });
524
- }
525
-
526
- function mentionOptionsForComposer(composer, memberEntries) {
527
- if (!composer || composer.dataset.chatEnableMentions !== "true") {
528
- return [];
529
- }
530
-
531
- var canMentionMembers = composer.dataset.chatCanMentionMembers === "true";
532
- var canMentionAll = composer.dataset.chatCanMentionAll === "true";
533
- var canMentionRoles = composer.dataset.chatCanMentionRoles === "true";
534
- var hideRoles = composer.dataset.chatMentionFilterHideRoles === "true";
535
- var excludeSelf = composer.dataset.chatMentionFilterExcludeSelf === "true";
536
- var selfType = String(composer.dataset.chatSelfParticipantType || "").trim();
537
- var selfId = String(composer.dataset.chatSelfParticipantId || "").trim();
538
- var mentionOptions = [];
539
- var seenTokens = {};
540
-
541
- function addMentionOption(optionData) {
542
- if (!optionData || !optionData.token) {
543
- return;
544
- }
545
-
546
- var dedupeToken = normalize(optionData.token);
547
- if (!dedupeToken || seenTokens[dedupeToken]) {
548
- return;
549
- }
550
-
551
- seenTokens[dedupeToken] = true;
552
- mentionOptions.push(optionData);
553
- }
554
-
555
- if (canMentionAll) {
556
- addMentionOption({ token: "@all", label: "All members", kind: "group" });
557
- }
558
-
559
- if (canMentionMembers) {
560
- memberEntries.forEach(function (memberEntry) {
561
- if (excludeSelf && memberEntry.participantType === selfType && memberEntry.participantId === selfId) {
562
- return;
563
- }
564
-
565
- if (!memberEntry.mentionToken) {
566
- return;
567
- }
568
-
569
- addMentionOption({
570
- token: memberEntry.mentionToken,
571
- label: memberEntry.name || memberEntry.mentionToken,
572
- kind: "member",
573
- participant_type: memberEntry.participantType,
574
- participant_id: memberEntry.participantId
575
- });
576
- });
577
- }
578
-
579
- if (canMentionRoles && !hideRoles) {
580
- memberEntries.forEach(function (memberEntry) {
581
- if (!memberEntry.roleKey) {
582
- return;
583
- }
584
-
585
- var roleToken = "@" + memberEntry.roleKey.toUpperCase();
586
- var roleLabel = (memberEntry.roleName || memberEntry.roleKey) + " role";
587
- addMentionOption({
588
- token: roleToken,
589
- label: roleLabel,
590
- kind: "role"
591
- });
592
- });
593
- }
594
-
595
- return mentionOptions;
596
- }
597
-
598
- function syncComposerMentionOptionsForChat(chatId) {
599
- var memberList = memberListForChat(chatId);
600
- if (!memberList) {
601
- return;
602
- }
603
-
604
- var memberEntries = [];
605
- memberList.querySelectorAll("[data-chat-member-entry='true']").forEach(function (memberNode) {
606
- var memberEntry = parseMemberEntry(memberNode);
607
- if (memberEntry) {
608
- memberEntries.push(memberEntry);
609
- }
610
- });
611
-
612
- document.querySelectorAll("[data-chat-composer]").forEach(function (composer) {
613
- if (String(composer.dataset.chatId || "").trim() !== String(chatId).trim()) {
614
- return;
615
- }
616
-
617
- if (!composer.__chatComposerApi || typeof composer.__chatComposerApi.setMentionOptions !== "function") {
618
- return;
619
- }
620
-
621
- composer.__chatComposerApi.setMentionOptions(mentionOptionsForComposer(composer, memberEntries));
622
- });
623
- }
624
-
625
- function setupMemberManageControlsForChat(chatId) {
626
- var memberList = memberListForChat(chatId);
627
- if (!memberList) {
628
- return;
629
- }
630
-
631
- memberList.querySelectorAll("[data-chat-member-entry='true']").forEach(function (memberNode) {
632
- if (!memberNode || memberNode.dataset.chatMemberManageBound === "true") {
633
- return;
634
- }
635
-
636
- var toggle = memberNode.querySelector("[data-chat-member-manage-toggle]");
637
- var panel = memberNode.querySelector("[data-chat-member-manage-panel]");
638
- if (!toggle || !panel) {
639
- return;
640
- }
641
-
642
- memberNode.dataset.chatMemberManageBound = "true";
643
- panel.hidden = true;
644
- toggle.setAttribute("aria-expanded", "false");
645
-
646
- toggle.addEventListener("click", function () {
647
- if (toggle.disabled) {
648
- return;
649
- }
650
-
651
- var expanded = toggle.getAttribute("aria-expanded") === "true";
652
- var parentList = memberNode.closest("[data-chat-member-list]");
653
- if (parentList) {
654
- parentList.querySelectorAll("[data-chat-member-manage-toggle]").forEach(function (otherToggle) {
655
- otherToggle.setAttribute("aria-expanded", "false");
656
- });
657
- parentList.querySelectorAll("[data-chat-member-manage-panel]").forEach(function (otherPanel) {
658
- otherPanel.hidden = true;
659
- });
660
- }
661
-
662
- if (expanded) {
663
- panel.hidden = true;
664
- toggle.setAttribute("aria-expanded", "false");
665
- return;
666
- }
667
-
668
- panel.hidden = false;
669
- toggle.setAttribute("aria-expanded", "true");
670
- var roleInput = panel.querySelector("[data-chat-member-role-input]");
671
- if (roleInput && !roleInput.disabled) {
672
- roleInput.focus();
673
- }
674
- });
675
- });
676
- }
677
-
678
- function syncRoleFormAccessForChat(chatId) {
679
- var memberList = memberListForChat(chatId);
680
- var shell = shellForChat(chatId);
681
- if (!memberList || !shell) {
682
- return;
683
- }
684
-
685
- var canManage = shell.dataset.chatCanManageMemberPermissions === "true";
686
- var selfType = String(shell.dataset.chatSelfParticipantType || "").trim();
687
- var selfId = String(shell.dataset.chatSelfParticipantId || "").trim();
688
- var selfRoleRank = -1;
689
-
690
- memberList.querySelectorAll("[data-chat-member-entry='true']").forEach(function (memberNode) {
691
- if (
692
- String(memberNode.dataset.chatMemberParticipantType || "").trim() === selfType &&
693
- String(memberNode.dataset.chatMemberParticipantId || "").trim() === selfId
694
- ) {
695
- var rank = parseInt(memberNode.dataset.chatMemberRoleRank || "-1", 10);
696
- selfRoleRank = isNaN(rank) ? -1 : rank;
697
- }
698
- });
699
-
700
- memberList.querySelectorAll("[data-chat-member-entry='true']").forEach(function (memberNode) {
701
- var targetType = String(memberNode.dataset.chatMemberParticipantType || "").trim();
702
- var targetId = String(memberNode.dataset.chatMemberParticipantId || "").trim();
703
- var targetRoleRank = parseInt(memberNode.dataset.chatMemberRoleRank || "-1", 10);
704
- if (isNaN(targetRoleRank)) {
705
- targetRoleRank = -1;
706
- }
707
-
708
- var isSelf = targetType === selfType && targetId === selfId;
709
- var targetManageableByRank = selfRoleRank < 0 || targetRoleRank < selfRoleRank;
710
- var controlsEnabled = canManage && !isSelf && targetManageableByRank;
711
- var toggle = memberNode.querySelector("[data-chat-member-manage-toggle]");
712
- var panel = memberNode.querySelector("[data-chat-member-manage-panel]");
713
-
714
- memberNode.querySelectorAll("[data-chat-member-role-input], [data-chat-member-role-submit]").forEach(function (control) {
715
- control.disabled = !controlsEnabled;
716
- });
717
-
718
- if (toggle) {
719
- toggle.disabled = !controlsEnabled;
720
- if (!controlsEnabled) {
721
- toggle.setAttribute("aria-expanded", "false");
722
- }
723
- }
724
-
725
- if (panel && !controlsEnabled) {
726
- panel.hidden = true;
727
- }
728
- });
729
- }
730
-
731
- function setupMemberListSync(memberList) {
732
- if (!memberList || memberList.dataset.chatMemberSyncBound === "true") {
733
- return;
734
- }
735
-
736
- var chatId = String(memberList.dataset.chatId || "").trim();
737
- if (!chatId) {
738
- return;
739
- }
740
-
741
- memberList.dataset.chatMemberSyncBound = "true";
742
- setupMemberManageControlsForChat(chatId);
743
- syncComposerMentionOptionsForChat(chatId);
744
- syncRoleFormAccessForChat(chatId);
745
-
746
- var observer = new MutationObserver(function (mutationRecords) {
747
- syncInviteOptionsFromMemberMutations(chatId, mutationRecords);
748
- setupMemberManageControlsForChat(chatId);
749
- syncComposerMentionOptionsForChat(chatId);
750
- syncRoleFormAccessForChat(chatId);
751
- });
752
- observer.observe(memberList, { childList: true });
753
- }
754
-
755
386
  function setupAllInvitePickers() {
756
387
  document.querySelectorAll("[data-chat-invite-form]").forEach(function (form) {
757
388
  setupInvitePicker(form);
758
389
  });
759
390
  }
760
391
 
761
- function setupAllMemberListSync() {
762
- document.querySelectorAll("[data-chat-member-list]").forEach(setupMemberListSync);
763
- }
764
-
392
+ namespace.setupInvitePicker = setupInvitePicker;
765
393
  namespace.setupAllInvitePickers = setupAllInvitePickers;
766
- namespace.setupAllMemberListSync = setupAllMemberListSync;
767
394
  })(window.TurboChatUI = window.TurboChatUI || {});