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
@@ -37,7 +37,8 @@ end.sort_by { |(role_name, role_key, role_rank)| [role_rank, role_name.to_s.down
37
37
  data-chat-member-search="<%= invite_search_value %>"
38
38
  data-chat-member-role-key="<%= membership.effective_role_key %>"
39
39
  data-chat-member-role-name="<%= membership.effective_role_name %>"
40
- data-chat-member-role-rank="<%= membership.effective_role_rank %>">
40
+ data-chat-member-role-rank="<%= membership.effective_role_rank %>"
41
+ data-chat-member-muted="<%= membership.muted? %>">
41
42
  <div class="chat-members-item-main">
42
43
  <span class="chat-members-name"><%= participant_name %></span>
43
44
  <span class="chat-members-role"><%= membership.effective_role_name %></span>
@@ -69,6 +70,19 @@ end.sort_by { |(role_name, role_key, role_rank)| [role_rank, role_name.to_s.down
69
70
  class: "chat-btn chat-btn--small chat-member-role-submit",
70
71
  data: { chat_member_role_submit: true } %>
71
72
  <% end %>
73
+
74
+ <div class="chat-member-moderation" data-chat-member-moderation-panel hidden>
75
+ <%= button_to(membership.muted? ? "Unmute" : "Mute",
76
+ TurboChat::Engine.routes.url_helpers.mute_chat_chat_membership_path(chat_id: chat.id, id: membership.id),
77
+ method: :patch,
78
+ class: "chat-btn chat-btn--small chat-btn--ghost",
79
+ data: { chat_member_mute_action: true }) %>
80
+ <%= button_to("Remove",
81
+ TurboChat::Engine.routes.url_helpers.ban_chat_chat_membership_path(chat_id: chat.id, id: membership.id),
82
+ method: :patch,
83
+ class: "chat-btn chat-btn--small chat-btn--danger",
84
+ data: { turbo_confirm: "Remove this member from this chat?", chat_member_ban_action: true }) %>
85
+ </div>
72
86
  </div>
73
87
  </li>
74
88
  <% end %>
@@ -48,7 +48,7 @@
48
48
  <li class="chat-list-item chat-list-item--chat">
49
49
  <%= link_to chat_path(chat), class: "chat-list-link" do %>
50
50
  <span class="chat-list-title"><%= chat.title %></span>
51
- <% if @show_members %>
51
+ <% if chat_show_members?(chat: chat) %>
52
52
  <span class="chat-list-meta"><%= pluralize(member_count, "member") %></span>
53
53
  <% end %>
54
54
  <% end %>
@@ -1,13 +1,10 @@
1
- <section class="chat-shell">
1
+ <section class="chat-shell chat-shell--narrow">
2
2
  <header class="chat-header">
3
3
  <h1>New Chat</h1>
4
4
  </header>
5
5
 
6
- <%= form_with model: @chat, url: chats_path do |f| %>
7
- <div class="chat-field">
8
- <%= f.label :title %>
9
- <%= f.text_field :title, required: true, autofocus: true %>
10
- </div>
11
- <%= f.submit "Create", class: "chat-btn" %>
6
+ <%= form_with model: @chat, url: chats_path, class: "chat-form chat-form--create" do |f| %>
7
+ <%= f.text_field :title, required: true, autofocus: true, placeholder: "Chat title" %>
8
+ <%= f.submit "Create", class: "chat-btn chat-btn--small" %>
12
9
  <% end %>
13
10
  </section>
@@ -3,36 +3,40 @@
3
3
  <% current_participant_id = current_participant.id %>
4
4
  <% self_mention_tokens = chat_self_mention_tokens(chat: @chat, participant: current_participant) %>
5
5
  <% self_role_mention_token = chat_self_role_mention_token(chat: @chat, participant: current_participant) %>
6
- <% mention_container_style = chat_mentions_container_inline_style %>
6
+ <% mention_container_style = chat_mentions_container_inline_style(chat: @chat) %>
7
7
  <% chat_shell_classes = [
8
8
  "chat-shell",
9
- chat_shell_style_class,
9
+ chat_shell_style_class(chat: @chat),
10
10
  ("chat-shell--closed" if @chat.closed?),
11
11
  ("chat-shell--can-manage-member-permissions" if @can_manage_member_permissions)
12
12
  ].compact.join(" ") %>
13
- <% message_insert_position = chat_message_insert_position %>
13
+ <% message_insert_position = chat_message_insert_position(chat: @chat) %>
14
14
  <% chat_messages = @chat_messages.to_a %>
15
15
  <% chat_messages.reverse! if message_insert_position == "append_start" %>
16
- <% can_render_composer = @can_post_message && !chat_disable_input? %>
17
- <% show_header_title = chat_show_header_title? %>
18
- <% show_header_status = chat_show_header_status? %>
19
- <% show_header_close_action = chat_show_header_close_action? %>
20
- <% show_header_leave_action = chat_show_header_leave_action? %>
21
- <% show_header_back_action = chat_show_header_back_action? %>
16
+ <% can_render_composer = @can_post_message && !chat_disable_input?(chat: @chat) %>
17
+ <% show_header_title = chat_show_header_title?(chat: @chat) %>
18
+ <% show_header_status = chat_show_header_status?(chat: @chat) %>
19
+ <% show_header_close_action = chat_show_header_close_action?(chat: @chat) %>
20
+ <% show_header_leave_action = chat_show_header_leave_action?(chat: @chat) %>
21
+ <% show_header_back_action = chat_show_header_back_action?(chat: @chat) %>
22
22
  <% show_header_actions = show_header_close_action || show_header_leave_action || show_header_back_action %>
23
- <% show_members_list = chat_show_members_list? %>
24
- <% show_members_invite_controls = chat_show_members_invite_controls? %>
25
- <% show_invite_fallback_when_members_hidden = chat_show_invite_fallback_when_members_hidden? %>
23
+ <% show_members_list = chat_show_members_list?(chat: @chat) %>
24
+ <% show_members_invite_controls = chat_show_members_invite_controls?(chat: @chat) %>
25
+ <% show_invite_fallback_when_members_hidden = chat_show_invite_fallback_when_members_hidden?(chat: @chat) %>
26
26
  <% show_members_invite_block = show_members_invite_controls && @can_invite_member %>
27
27
  <% show_members_panel = @show_members && (show_members_list || show_members_invite_block) %>
28
28
  <% show_invite_fallback = !@show_members && show_members_invite_block && show_invite_fallback_when_members_hidden %>
29
29
  <section class="<%= chat_shell_classes %>"
30
30
  data-chat-id="<%= @chat.id %>"
31
- data-chat-style="<%= chat_style_key %>"
31
+ data-chat-style="<%= chat_style_key(chat: @chat) %>"
32
+ data-chat-mode="<%= chat_mode_key(@chat) %>"
32
33
  data-chat-self-participant-type="<%= current_participant_type %>"
33
34
  data-chat-self-participant-id="<%= current_participant_id %>"
34
35
  data-chat-can-manage-member-permissions="<%= @can_manage_member_permissions %>"
35
- data-chat-emit-chat-lifecycle-events="<%= chat_emit_chat_lifecycle_events? %>"
36
+ data-chat-can-grant-member-permissions="<%= @can_grant_member_permissions %>"
37
+ data-chat-can-mute-member="<%= @can_mute_member %>"
38
+ data-chat-can-ban-member="<%= @can_ban_member %>"
39
+ data-chat-emit-chat-lifecycle-events="<%= chat_emit_chat_lifecycle_events?(chat: @chat) %>"
36
40
  data-chat-lifecycle-event="<%= json_escape(@chat_lifecycle_event.to_json) if @chat_lifecycle_event.present? %>">
37
41
  <% invite_option_rows = if @can_invite_member && @invitable_participants.present?
38
42
  @invitable_participants.map do |participant|
@@ -47,7 +51,6 @@
47
51
  end %>
48
52
  <header class="chat-header">
49
53
  <div class="chat-header-copy">
50
- <p class="chat-header-kicker"><%= @chat.closed? ? "Conversation closed" : "Conversation active" %></p>
51
54
  <% if show_header_title || show_header_status %>
52
55
  <div class="chat-header-title-row">
53
56
  <% if show_header_title %>
@@ -150,21 +153,20 @@
150
153
  data-chat-self-participant-id="<%= current_participant_id %>"
151
154
  data-chat-self-mention-tokens="<%= json_escape(self_mention_tokens.to_json) %>"
152
155
  data-chat-self-role-mention-token="<%= self_role_mention_token %>"
153
- data-chat-mention-filter-exclude-self="<%= chat_mention_filter_exclude_self? %>"
154
- data-chat-mention-filter-hide-roles="<%= chat_mention_filter_hide_roles? %>"
155
- data-chat-emit-mention-events="<%= chat_emit_mention_events? %>"
156
+ data-chat-mention-filter-exclude-self="<%= chat_mention_filter_exclude_self?(chat: @chat) %>"
157
+ data-chat-mention-filter-hide-roles="<%= chat_mention_filter_hide_roles?(chat: @chat) %>"
158
+ data-chat-emit-mention-events="<%= chat_emit_mention_events?(chat: @chat) %>"
156
159
  data-chat-can-edit-own-messages="<%= @can_edit_own_messages %>"
157
160
  <%= %(style="#{mention_container_style}") if mention_container_style.present? %>>
158
161
  <%= render chat_messages %>
159
- </div>
160
-
161
- <div id="<%= dom_id(@chat, :signals) %>"
162
- class="chat-signals"
163
- data-chat-show-self-signals="<%= chat_show_self_signals? %>"
164
- data-chat-signal-ttl-seconds="<%= chat_signal_ttl_seconds %>"
165
- data-chat-self-participant-type="<%= current_participant_type %>"
166
- data-chat-self-participant-id="<%= current_participant_id %>">
167
- <%= render "turbo_chat/chat_messages/signals", chat: @chat %>
162
+ <div id="<%= dom_id(@chat, :signals) %>"
163
+ class="chat-signals"
164
+ data-chat-show-self-signals="<%= chat_show_self_signals?(chat: @chat) %>"
165
+ data-chat-signal-ttl-seconds="<%= chat_signal_ttl_seconds(chat: @chat) %>"
166
+ data-chat-self-participant-type="<%= current_participant_type %>"
167
+ data-chat-self-participant-id="<%= current_participant_id %>">
168
+ <%= render "turbo_chat/chat_messages/signals", chat: @chat %>
169
+ </div>
168
170
  </div>
169
171
  </section>
170
172
 
data/config/routes.rb CHANGED
@@ -10,7 +10,12 @@ TurboChat::Engine.routes.draw do
10
10
  patch :reopen
11
11
  end
12
12
 
13
- resources :chat_memberships, only: %i[create update]
13
+ resources :chat_memberships, only: %i[create update] do
14
+ member do
15
+ patch :mute
16
+ patch :ban
17
+ end
18
+ end
14
19
  resources :chat_messages, only: %i[index create update]
15
20
  end
16
21
  end
@@ -0,0 +1,6 @@
1
+ class AddChatModeToTurboChatChats < ActiveRecord::Migration[7.0]
2
+ def change
3
+ add_column :turbo_chat_chats, :chat_mode, :integer, null: false, default: 0
4
+ add_index :turbo_chat_chats, :chat_mode
5
+ end
6
+ end
@@ -133,4 +133,12 @@ TurboChat.configure do |config|
133
133
  # Shows your own active typing/signal indicators.
134
134
  # config.signals.replace_signals_on_message_submit = true
135
135
  # Replaces existing signals when sending a message.
136
+
137
+ # Chat mode overrides
138
+ # Assistant chats default to a simplified 1:1 profile.
139
+ # Uncomment any of these to customize assistant-mode behavior.
140
+ # config.mode(:assistant).show_members = true
141
+ # config.mode(:assistant).enable_mentions = true
142
+ # config.mode(:assistant).show_header_close_action = true
143
+ # config.mode(:assistant).max_chat_participants = 2
136
144
  end
@@ -155,6 +155,27 @@ class TurboChat::Configuration
155
155
  defaults[attribute] = SCOPED_DEFAULTS.fetch(scope_name).fetch(attribute)
156
156
  end.freeze
157
157
 
158
+ ASSISTANT_MODE_DEFAULTS = {
159
+ max_chat_participants: 2,
160
+ show_members: false,
161
+ show_members_list: false,
162
+ show_members_invite_controls: false,
163
+ show_invite_fallback_when_members_hidden: false,
164
+ system_messages: false,
165
+ show_header_close_action: false,
166
+ enable_mentions: false,
167
+ emit_mention_events: false,
168
+ emit_invitation_events: false,
169
+ emit_chat_lifecycle_events: false,
170
+ emit_moderation_events: false
171
+ }.freeze
172
+
173
+ MODE_DEFAULTS = {
174
+ standard: {}.freeze,
175
+ assistant: ASSISTANT_MODE_DEFAULTS
176
+ }.freeze
177
+
178
+ MODE_NAMES = MODE_DEFAULTS.keys.freeze
158
179
  SCOPE_NAMES = SCOPED_DEFAULTS.keys.freeze
159
180
  ATTRIBUTES = ATTRIBUTE_SCOPES.keys.freeze
160
181
  end
@@ -25,6 +25,31 @@ class TurboChat::Configuration
25
25
  end
26
26
  end
27
27
 
28
+ class ModeSettings
29
+ class << self
30
+ attr_reader :mode_attributes
31
+
32
+ def configure_attributes(attributes)
33
+ @mode_attributes = attributes
34
+ attr_accessor(*attributes)
35
+ end
36
+ end
37
+
38
+ configure_attributes(ATTRIBUTES)
39
+
40
+ def initialize(defaults = {})
41
+ self.class.mode_attributes.each do |attribute|
42
+ instance_variable_set("@#{attribute}", nil)
43
+ end
44
+
45
+ defaults.each do |attribute, value|
46
+ next unless respond_to?("#{attribute}=")
47
+
48
+ public_send("#{attribute}=", TurboChat::Configuration.resolve_default_value(value))
49
+ end
50
+ end
51
+ end
52
+
28
53
  class Chat < Scope
29
54
  configure_defaults(SCOPED_DEFAULTS.fetch(:chat))
30
55
  end
@@ -75,11 +100,91 @@ class TurboChat::Configuration
75
100
  instance_variable_set("@#{scope_name}", klass.new)
76
101
  end
77
102
  @additional_roles = {}
103
+ reset_modes!
104
+ end
105
+
106
+ def mode(name)
107
+ normalized = self.class.normalize_mode_name(name)
108
+ raise ArgumentError, "unknown chat mode: #{name.inspect}" if normalized.nil?
109
+
110
+ @modes[normalized]
111
+ end
112
+
113
+ def reset_mode!(name)
114
+ normalized = self.class.normalize_mode_name(name)
115
+ raise ArgumentError, "unknown chat mode: #{name.inspect}" if normalized.nil?
116
+
117
+ @modes[normalized] = ModeSettings.new(MODE_DEFAULTS.fetch(normalized))
118
+ end
119
+
120
+ def reset_modes!
121
+ @modes = MODE_DEFAULTS.each_with_object({}) do |(mode_name, defaults), modes|
122
+ modes[mode_name] = ModeSettings.new(defaults)
123
+ end
124
+ end
125
+
126
+ def resolved_value(attribute, chat: nil, default: nil)
127
+ base_value = if respond_to?(attribute)
128
+ public_send(attribute)
129
+ else
130
+ default
131
+ end
132
+
133
+ mode_name = self.class.extract_chat_mode(chat)
134
+ return base_value if mode_name.nil? || mode_name == :standard
135
+
136
+ mode_settings = @modes[mode_name]
137
+ return base_value unless mode_settings&.respond_to?(attribute)
138
+
139
+ override_value = mode_settings.public_send(attribute)
140
+ override_value.nil? ? base_value : override_value
141
+ rescue NoMethodError, TypeError, ArgumentError
142
+ default
78
143
  end
79
144
 
80
145
  class << self
81
146
  def resolve_default_value(default_value)
82
147
  default_value.respond_to?(:call) ? default_value.call : (default_value.dup rescue default_value)
83
148
  end
149
+
150
+ def config_value(method_name, default: nil, chat: nil)
151
+ config = TurboChat.configuration
152
+ return config.resolved_value(method_name, chat: chat, default: default) if config.respond_to?(:resolved_value)
153
+ return config.public_send(method_name) if config.respond_to?(method_name)
154
+
155
+ default
156
+ rescue NoMethodError, TypeError, ArgumentError
157
+ default
158
+ end
159
+
160
+ def config_boolean(method_name, default:, chat: nil)
161
+ value = config_value(method_name, default: default, chat: chat)
162
+ ActiveModel::Type::Boolean.new.cast(value)
163
+ rescue NoMethodError, TypeError
164
+ default
165
+ end
166
+
167
+ def extract_chat_mode(chat_or_mode)
168
+ case chat_or_mode
169
+ when nil
170
+ nil
171
+ when String, Symbol
172
+ normalize_mode_name(chat_or_mode)
173
+ else
174
+ return nil unless chat_or_mode.respond_to?(:chat_mode)
175
+
176
+ normalize_mode_name(chat_or_mode.chat_mode)
177
+ end
178
+ end
179
+
180
+ def normalize_mode_name(value)
181
+ normalized = value.to_s.strip
182
+ return nil if normalized.blank?
183
+
184
+ mode_name = normalized.to_sym
185
+ return nil unless MODE_NAMES.include?(mode_name)
186
+
187
+ mode_name
188
+ end
84
189
  end
85
190
  end
@@ -6,7 +6,7 @@ module TurboChat::Moderation
6
6
 
7
7
  payload = moderation_message_payload(message)
8
8
  message.destroy!
9
- emit_moderation_event("turbo_chat.moderation.message_deleted", actor: actor, payload: payload)
9
+ emit_moderation_event("turbo_chat.moderation.message_deleted", actor: actor, payload: payload, chat: message.chat)
10
10
  true
11
11
  end
12
12
 
@@ -37,7 +37,7 @@ module TurboChat::Moderation
37
37
  def update_chat!(actor:, chat:, gate:, error_message:, event_name:, chat_event:)
38
38
  authorize_chat_action!(actor: actor, chat: chat, gate: gate, error_message: error_message)
39
39
  chat.public_send(chat_event)
40
- emit_moderation_event(event_name, actor: actor, payload: moderation_chat_payload(chat))
40
+ emit_moderation_event(event_name, actor: actor, payload: moderation_chat_payload(chat), chat: chat)
41
41
  chat
42
42
  end
43
43
  end
@@ -31,6 +31,7 @@ module TurboChat::Moderation
31
31
  "turbo_chat.moderation.member_timed_out",
32
32
  actor: actor,
33
33
  membership: membership,
34
+ chat: membership.chat,
34
35
  extra: { timed_out_until: membership.timed_out_until }
35
36
  )
36
37
  create_membership_system_message(actor: actor, membership: membership, event: :timed_out)
@@ -68,7 +69,7 @@ module TurboChat::Moderation
68
69
  action: action,
69
70
  attributes: attributes
70
71
  )
71
- emit_moderation_event(event_name, actor: actor, membership: updated_membership)
72
+ emit_moderation_event(event_name, actor: actor, membership: updated_membership, chat: updated_membership.chat)
72
73
  create_membership_system_message(actor: actor, membership: updated_membership, event: system_event)
73
74
  updated_membership
74
75
  end
@@ -37,21 +37,17 @@ module TurboChat::Moderation
37
37
  )
38
38
  end
39
39
 
40
- def emit_moderation_event(name, actor:, membership: nil, payload: nil, extra: {})
41
- return unless moderation_events_enabled?
40
+ def emit_moderation_event(name, actor:, membership: nil, payload: nil, extra: {}, chat: nil)
41
+ event_chat = chat || membership&.chat
42
+ return unless moderation_events_enabled?(event_chat)
42
43
  return unless defined?(ActiveSupport::Notifications)
43
44
 
44
45
  event_payload = payload.presence || moderation_membership_payload(membership)
45
46
  ActiveSupport::Notifications.instrument(name, event_payload.merge(actor_payload(actor)).merge(extra))
46
47
  end
47
48
 
48
- def moderation_events_enabled?
49
- config = TurboChat.configuration
50
- return false unless config.respond_to?(:emit_moderation_events)
51
-
52
- ActiveModel::Type::Boolean.new.cast(config.emit_moderation_events)
53
- rescue NoMethodError, TypeError
54
- false
49
+ def moderation_events_enabled?(chat = nil)
50
+ TurboChat::Configuration.config_boolean(:emit_moderation_events, default: false, chat: chat)
55
51
  end
56
52
 
57
53
  def moderation_membership_payload(membership)
@@ -24,7 +24,7 @@ class TurboChat::Permission
24
24
  return nil unless chat_present?
25
25
  return nil if target_participant.nil?
26
26
 
27
- chat.chat_memberships.active.find_by(participant: target_participant)
27
+ lookup_membership(target_participant)
28
28
  end
29
29
 
30
30
  def role_permission?(permission) = role_permissions.include?(permission.to_sym)
@@ -39,7 +39,7 @@ class TurboChat::Permission
39
39
  return @actor_membership if defined?(@actor_membership)
40
40
 
41
41
  @actor_membership = if chat_present? && participant_present?
42
- chat.chat_memberships.active.find_by(participant: participant)
42
+ lookup_membership(participant)
43
43
  end
44
44
  end
45
45
 
@@ -64,5 +64,9 @@ class TurboChat::Permission
64
64
  def message_in_chat?(message) = message.chat_id == chat&.id
65
65
 
66
66
  def membership_in_chat?(target_membership) = target_membership.chat_id == chat&.id
67
+
68
+ def lookup_membership(target)
69
+ chat.find_active_membership(target)
70
+ end
67
71
  end
68
72
  end
@@ -70,10 +70,6 @@ class TurboChat::Permission
70
70
  private
71
71
 
72
72
  def chat_input_disabled?
73
- configuration = TurboChat.configuration
74
- value = configuration.respond_to?(:disable_input) ? configuration.disable_input : false
75
- ActiveModel::Type::Boolean.new.cast(value)
76
- rescue NoMethodError, TypeError
77
- false
73
+ TurboChat::Configuration.config_boolean(:disable_input, default: false, chat: chat)
78
74
  end
79
75
  end
@@ -3,7 +3,7 @@ module TurboChat
3
3
  module_function
4
4
 
5
5
  def start!(chat:, participant:, signal_type: :typing, signal_text: nil)
6
- TurboChat::ChatMessage.replace_signal!(
6
+ TurboChat::ChatMessage.start_signal!(
7
7
  chat: chat,
8
8
  participant: participant,
9
9
  signal_type: signal_type,
@@ -1,3 +1,3 @@
1
1
  module TurboChat
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: turbo_chat
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Haumer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-02 00:00:00.000000000 Z
11
+ date: 2026-03-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -64,10 +64,20 @@ files:
64
64
  - app/assets/javascripts/turbo_chat/application.js
65
65
  - app/assets/javascripts/turbo_chat/invite_picker.js
66
66
  - app/assets/javascripts/turbo_chat/lifecycle_events.js
67
+ - app/assets/javascripts/turbo_chat/member_sync.js
68
+ - app/assets/javascripts/turbo_chat/mentions.js
67
69
  - app/assets/javascripts/turbo_chat/messages.js
68
70
  - app/assets/javascripts/turbo_chat/realtime.js
71
+ - app/assets/javascripts/turbo_chat/scroll_proxy.js
69
72
  - app/assets/javascripts/turbo_chat/shared.js
70
73
  - app/assets/stylesheets/turbo_chat/application.css
74
+ - app/assets/stylesheets/turbo_chat/base.css
75
+ - app/assets/stylesheets/turbo_chat/components.css
76
+ - app/assets/stylesheets/turbo_chat/composer.css
77
+ - app/assets/stylesheets/turbo_chat/layout.css
78
+ - app/assets/stylesheets/turbo_chat/members.css
79
+ - app/assets/stylesheets/turbo_chat/menus.css
80
+ - app/assets/stylesheets/turbo_chat/messages.css
71
81
  - app/controllers/turbo_chat/application_controller.rb
72
82
  - app/controllers/turbo_chat/chat_memberships_controller.rb
73
83
  - app/controllers/turbo_chat/chat_messages_controller.rb
@@ -114,6 +124,7 @@ files:
114
124
  - db/migrate/20260218000013_add_invitation_accepted_to_turbo_chat_chat_memberships.rb
115
125
  - db/migrate/20260223000014_add_source_fields_to_turbo_chat_chat_messages.rb
116
126
  - db/migrate/20260302000015_add_kind_index_to_turbo_chat_chat_messages.rb
127
+ - db/migrate/20260325000016_add_chat_mode_to_turbo_chat_chats.rb
117
128
  - lib/generators/turbo_chat/install/install_generator.rb
118
129
  - lib/generators/turbo_chat/install/templates/turbo_chat.rb
119
130
  - lib/tasks/turbo_chat_tasks.rake