turbo_chat 0.1.12 → 0.1.15

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fc8d322788826376f34b0944e94e293a5721494eae3e1e2d7767722f33073a8d
4
- data.tar.gz: 3394be20cb1728f8bc71e76fa652b8ed03dccbc189d527c08c7eb55d7616dcd5
3
+ metadata.gz: 62e1eb06f88659efb5df1cc8a72f33e669b2a969dd734abc8a977aa4331fe925
4
+ data.tar.gz: 5c40f2095d91a51a69cafd0086553ceab8ecf69894696bc8de2f86498170e52a
5
5
  SHA512:
6
- metadata.gz: d796b827e0efca5f8653bcfe7a0bfa6d642b61733920e5526fc6180c73d3a059b2ef6abbc7c5f051761cf75de1bc70b50cf309e5f382ea3c20e5197df6bb0be9
7
- data.tar.gz: 5669ff78684930d750766fccf1e1b18f25c8e90d3d52ac9d5f4aa5cd5f9b6eea3ffc4c3b357de8a1c6068e1ef244417649e2ae1dad2fd5e5b51badefc923e94d
6
+ metadata.gz: a0ebcdf8245e4c726f65071d9da1bb673cbef17ae2327c0e3708e86097bedf90a51a9deaef21b9e28b19aef5552f0071b164a21e571a0e332834775dab54db78
7
+ data.tar.gz: 7e9ede1f0c631f03642b8c9da45a5ec4a15b0880edc85ae3f6ea51b2aa4aaf59e25d91786b2ab3e9a30e4c4ea890c47deee143910978ebcb74793d20cfa203ed
data/CHANGELOG.md CHANGED
@@ -2,6 +2,35 @@
2
2
 
3
3
  All notable changes to `turbo_chat` will be documented in this file.
4
4
 
5
+ ## [0.1.15] - 2026-02-28
6
+
7
+ ### Added
8
+ - Configurable chat-header visibility toggles for title, status badge, and close/leave/back actions.
9
+ - Configurable members-area visibility toggles for member list, invite controls, and hidden-members invite fallback.
10
+
11
+ ### Changed
12
+ - Updated `rack` to `3.2.5` and `nokogiri` to `1.19.1` to address advisory findings in release audit.
13
+
14
+ ## [0.1.14] - 2026-02-28
15
+
16
+ ### Added
17
+ - Scoped configuration namespaces for clearer organization: `config.chat`, `config.chat_message`, `config.style`, `config.moderation`, `config.events`, and `config.signals`.
18
+ - Scoped installer initializer output with inline guidance grouped by scope.
19
+
20
+ ### Changed
21
+ - Updated README examples to use scoped configuration.
22
+ - Flat configuration aliases (for example `config.max_message_length`) remain supported for backward compatibility.
23
+
24
+ ## [0.1.13] - 2026-02-26
25
+
26
+ ### Added
27
+ - Configurable timeline insert behavior via `config.message_insert_position` (`append_end` or `append_start`).
28
+ - Configurable composer disable toggle via `config.disable_input` to hide input and reject message posts.
29
+
30
+ ### Changed
31
+ - Signal indicators now render status text for standard signal types (`typing`, `thinking`, `planning`) so shimmer styling can pass over the status text only (not participant names).
32
+ - `config.signal_text_sheen` is now consistently cast as a boolean during signal rendering.
33
+
5
34
  ## [0.1.12] - 2026-02-26
6
35
 
7
36
  ### Added
data/README.md CHANGED
@@ -77,7 +77,7 @@ end
77
77
  Resolution order:
78
78
 
79
79
  1. Host `ApplicationController#current_chat_participant` (if defined)
80
- 2. `config.current_participant_resolver` (if configured)
80
+ 2. `config.chat.current_participant_resolver` (if configured)
81
81
  3. `current_user` (if available)
82
82
  4. Raise `NotImplementedError`
83
83
 
@@ -85,7 +85,7 @@ Optional resolver for non-`current_user` auth:
85
85
 
86
86
  ```ruby
87
87
  TurboChat.configure do |config|
88
- config.current_participant_resolver = ->(controller) { controller.send(:current_member) }
88
+ config.chat.current_participant_resolver = ->(controller) { controller.send(:current_member) }
89
89
  end
90
90
  ```
91
91
 
@@ -110,32 +110,45 @@ Start with a minimal initializer and only expand when needed:
110
110
 
111
111
  ```ruby
112
112
  TurboChat.configure do |config|
113
- config.permission_adapter = TurboChat::Permission
114
-
115
- config.max_chat_participants = 10
116
- config.max_message_length = 1000
117
- config.message_history_limit = 200
118
-
119
- config.enable_mentions = true
120
- config.enable_emoji_aliases = true
121
-
122
- config.blocked_words = []
123
- config.blocked_words_action = :reject # or :scramble
124
-
125
- config.render_message_html = false
126
- config.show_timestamp = true
127
- config.show_role = false
128
- config.message_source_labels = TurboChat::Configuration::DEFAULT_MESSAGE_SOURCE_LABELS.dup
129
- config.chat_style = "chat_style_bounded"
130
- config.signal_ttl_seconds = 60
131
- config.signal_text_sheen = true
132
-
133
- config.emit_moderation_events = false
134
- config.emit_blocked_words_events = false
135
- config.emit_mention_events = false
113
+ config.chat.permission_adapter = TurboChat::Permission
114
+
115
+ config.chat.max_chat_participants = 10
116
+ config.chat_message.max_message_length = 1000
117
+ config.chat_message.message_history_limit = 200
118
+
119
+ config.chat_message.enable_mentions = true
120
+ config.chat_message.enable_emoji_aliases = true
121
+
122
+ config.chat_message.blocked_words = []
123
+ config.chat_message.blocked_words_action = :reject # or :scramble
124
+
125
+ config.chat_message.render_message_html = false
126
+ config.style.show_timestamp = true
127
+ config.style.show_role = false
128
+ config.chat_message.message_source_labels = TurboChat::Configuration::DEFAULT_MESSAGE_SOURCE_LABELS.dup
129
+ config.style.chat_style = "chat_style_bounded"
130
+ config.chat_message.message_insert_position = "append_end" # or "append_start"
131
+ config.chat.disable_input = false
132
+ config.chat.show_members = true
133
+ config.chat.show_members_list = true
134
+ config.chat.show_members_invite_controls = true
135
+ config.chat.show_invite_fallback_when_members_hidden = true
136
+ config.chat.show_header_title = true
137
+ config.chat.show_header_status = true
138
+ config.chat.show_header_close_action = true
139
+ config.chat.show_header_leave_action = true
140
+ config.chat.show_header_back_action = true
141
+ config.signals.signal_ttl_seconds = 60
142
+ config.style.signal_text_sheen = true
143
+
144
+ config.moderation.emit_moderation_events = false
145
+ config.moderation.emit_blocked_words_events = false
146
+ config.events.emit_mention_events = false
136
147
  end
137
148
  ```
138
149
 
150
+ Flat aliases (for example `config.max_message_length`) still work for backward compatibility.
151
+
139
152
  ## Message Ingest API
140
153
 
141
154
  Post messages as a specific participant, including external sources like WhatsApp:
@@ -168,7 +181,7 @@ Source labels shown in message badges are configurable:
168
181
 
169
182
  ```ruby
170
183
  TurboChat.configure do |config|
171
- config.message_source_labels = {
184
+ config.chat_message.message_source_labels = {
172
185
  "app" => "In App",
173
186
  "whatsapp" => "WhatsApp",
174
187
  "sms_gateway" => "SMS"
@@ -180,7 +193,46 @@ Chat UI layout style is configurable:
180
193
 
181
194
  ```ruby
182
195
  TurboChat.configure do |config|
183
- config.chat_style = "chat_style_unbounded" # or "chat_style_bounded"
196
+ config.style.chat_style = "chat_style_unbounded" # or "chat_style_bounded"
197
+ end
198
+ ```
199
+
200
+ Timeline insert position is configurable:
201
+
202
+ ```ruby
203
+ TurboChat.configure do |config|
204
+ config.chat_message.message_insert_position = "append_end" # or "append_start"
205
+ end
206
+ ```
207
+
208
+ Composer input can be disabled globally:
209
+
210
+ ```ruby
211
+ TurboChat.configure do |config|
212
+ config.chat.disable_input = true
213
+ end
214
+ ```
215
+
216
+ Chat header elements can also be disabled globally:
217
+
218
+ ```ruby
219
+ TurboChat.configure do |config|
220
+ config.chat.show_header_title = false
221
+ config.chat.show_header_status = false
222
+ config.chat.show_header_close_action = false
223
+ config.chat.show_header_leave_action = false
224
+ config.chat.show_header_back_action = false
225
+ end
226
+ ```
227
+
228
+ Members area visibility can be tuned independently:
229
+
230
+ ```ruby
231
+ TurboChat.configure do |config|
232
+ config.chat.show_members = true
233
+ config.chat.show_members_list = true
234
+ config.chat.show_members_invite_controls = true
235
+ config.chat.show_invite_fallback_when_members_hidden = true
184
236
  end
185
237
  ```
186
238
 
@@ -244,13 +296,13 @@ Enable only what you consume:
244
296
 
245
297
  ```ruby
246
298
  TurboChat.configure do |config|
247
- config.emit_typing_events = true
248
- config.emit_message_events = true
249
- config.emit_mention_events = true
250
- config.emit_invitation_events = true
251
- config.emit_chat_lifecycle_events = true
252
- config.emit_moderation_events = true
253
- config.emit_blocked_words_events = true
299
+ config.events.emit_typing_events = true
300
+ config.events.emit_message_events = true
301
+ config.events.emit_mention_events = true
302
+ config.events.emit_invitation_events = true
303
+ config.events.emit_chat_lifecycle_events = true
304
+ config.moderation.emit_moderation_events = true
305
+ config.moderation.emit_blocked_words_events = true
254
306
  end
255
307
  ```
256
308
 
@@ -419,6 +419,17 @@
419
419
  return;
420
420
  }
421
421
 
422
+ var insertPosition = String(container.dataset.chatMessageInsertPosition || "").trim().toLowerCase();
423
+ if (insertPosition === "append_start") {
424
+ var firstMessage = container.firstElementChild;
425
+ if (!firstMessage) {
426
+ return;
427
+ }
428
+
429
+ container.scrollTop = Math.max(0, firstMessage.offsetTop - 2);
430
+ return;
431
+ }
432
+
422
433
  var lastMessage = container.lastElementChild;
423
434
  if (!lastMessage) {
424
435
  return;
@@ -27,7 +27,7 @@ module TurboChat
27
27
  return [send(:current_user), "#current_user"]
28
28
  end
29
29
 
30
- raise NotImplementedError, "Define #current_chat_participant, configure TurboChat.configuration.current_participant_resolver, or expose #current_user"
30
+ raise NotImplementedError, "Define #current_chat_participant, configure TurboChat.configuration.chat.current_participant_resolver, or expose #current_user"
31
31
  end
32
32
 
33
33
  def invoke_current_participant_resolver(resolver)
@@ -58,9 +58,18 @@ module TurboChat
58
58
  end
59
59
 
60
60
  def authorize_post_message!(chat)
61
+ return head(:forbidden) if chat_input_disabled?
61
62
  return if permission_for(chat).can_post_message?
62
63
 
63
64
  head :forbidden
64
65
  end
66
+
67
+ def chat_input_disabled?
68
+ configuration = TurboChat.configuration
69
+ value = configuration.respond_to?(:disable_input) ? configuration.disable_input : false
70
+ ActiveModel::Type::Boolean.new.cast(value)
71
+ rescue StandardError
72
+ false
73
+ end
65
74
  end
66
75
  end
@@ -82,7 +82,7 @@ module TurboChat
82
82
  def respond_to_chat_message_create_failure
83
83
  @chat_messages = @chat.visible_messages
84
84
  @chat_permission = permission_for(@chat)
85
- @can_post_message = @chat_permission.can_post_message?
85
+ @can_post_message = @chat_permission.can_post_message? && !chat_input_disabled?
86
86
  @show_members = chat_config_boolean(:show_members, default: true)
87
87
  respond_to do |format|
88
88
  format.turbo_stream { render "turbo_chat/chats/show", status: :unprocessable_entity }
@@ -61,7 +61,7 @@ module TurboChat
61
61
  @chat_lifecycle_event = chat_lifecycle_event_payload
62
62
  @chat_permission = permission_for(@chat)
63
63
  @chat_messages = @chat.visible_messages
64
- @can_post_message = @chat_permission.can_post_message?
64
+ @can_post_message = @chat_permission.can_post_message? && !chat_input_disabled?
65
65
  @show_members = show_members_enabled?
66
66
  @can_invite_member = permission_allows?(@chat_permission, :can_invite_member?)
67
67
  @can_manage_member_permissions = permission_allows?(@chat_permission, :can_grant_member_permissions?)
@@ -8,6 +8,16 @@ module TurboChat
8
8
  emit_invitation_events: false,
9
9
  emit_chat_lifecycle_events: false,
10
10
  show_members: true,
11
+ show_members_list: true,
12
+ show_members_invite_controls: true,
13
+ show_invite_fallback_when_members_hidden: true,
14
+ show_self_signals: false,
15
+ disable_input: false,
16
+ show_header_title: true,
17
+ show_header_status: true,
18
+ show_header_close_action: true,
19
+ show_header_leave_action: true,
20
+ show_header_back_action: true,
11
21
  composer_add_files_display: false,
12
22
  composer_add_files_active: false,
13
23
  composer_microphone_display: false,
@@ -79,6 +89,28 @@ module TurboChat
79
89
  chat_unbounded_style? ? "chat-shell--style-unbounded" : "chat-shell--style-bounded"
80
90
  end
81
91
 
92
+ def chat_message_insert_position
93
+ raw_position = chat_config_value(:message_insert_position, default: "append_end")
94
+ normalized = raw_position.to_s.strip.downcase
95
+
96
+ case normalized
97
+ when "append_start", "start", "prepend"
98
+ "append_start"
99
+ else
100
+ "append_end"
101
+ end
102
+ end
103
+
104
+ def chat_message_append_start?
105
+ chat_message_insert_position == "append_start"
106
+ end
107
+
108
+ def chat_signal_ttl_seconds
109
+ value = chat_config_value(:signal_ttl_seconds, default: 60)
110
+ ttl = value.to_i
111
+ ttl.positive? ? ttl : 60
112
+ end
113
+
82
114
  private
83
115
 
84
116
  def chat_config_value(method_name, default: nil)
@@ -105,7 +105,12 @@ module TurboChat
105
105
  end
106
106
 
107
107
  def self.signal_window_seconds(window = nil)
108
- value = window.nil? ? TurboChat.configuration.signal_ttl_seconds : window
108
+ value = if window.nil?
109
+ configuration = TurboChat.configuration
110
+ configuration.respond_to?(:signal_ttl_seconds) ? configuration.signal_ttl_seconds : 60
111
+ else
112
+ window
113
+ end
109
114
  seconds = value.to_i
110
115
  return seconds if seconds.positive?
111
116
 
@@ -10,13 +10,8 @@ module TurboChat
10
10
 
11
11
  stream = stream_name
12
12
 
13
- if appendable_timeline_message? && respond_to?(:broadcast_append_to)
14
- broadcast_append_to(
15
- stream,
16
- target: ActionView::RecordIdentifier.dom_id(chat, :messages),
17
- partial: CHAT_MESSAGE_PARTIAL,
18
- locals: { chat_message: self }
19
- )
13
+ if appendable_timeline_message?
14
+ broadcast_timeline_create(stream)
20
15
  end
21
16
 
22
17
  broadcast_update_to(
@@ -60,6 +55,34 @@ module TurboChat
60
55
  def appendable_timeline_message?
61
56
  message? || system?
62
57
  end
58
+
59
+ def broadcast_timeline_create(stream)
60
+ options = {
61
+ target: ActionView::RecordIdentifier.dom_id(chat, :messages),
62
+ partial: CHAT_MESSAGE_PARTIAL,
63
+ locals: { chat_message: self }
64
+ }
65
+
66
+ if append_start_position?
67
+ return unless respond_to?(:broadcast_prepend_to)
68
+
69
+ broadcast_prepend_to(stream, **options)
70
+ return
71
+ end
72
+
73
+ return unless respond_to?(:broadcast_append_to)
74
+
75
+ broadcast_append_to(stream, **options)
76
+ end
77
+
78
+ def append_start_position?
79
+ configuration = TurboChat.configuration
80
+ value = configuration.respond_to?(:message_insert_position) ? configuration.message_insert_position : "append_end"
81
+ normalized = value.to_s.strip.downcase
82
+ %w[append_start start prepend].include?(normalized)
83
+ rescue StandardError
84
+ false
85
+ end
63
86
  end
64
87
  end
65
88
  end
@@ -1,5 +1,11 @@
1
- <% show_self_signals = TurboChat.configuration.show_self_signals %>
2
- <% signal_text_sheen_enabled = TurboChat.configuration.signal_text_sheen %>
1
+ <% show_self_signals = if respond_to?(:chat_show_self_signals?, true)
2
+ chat_show_self_signals?
3
+ else
4
+ configuration = TurboChat.configuration
5
+ value = configuration.respond_to?(:show_self_signals) ? configuration.show_self_signals : false
6
+ ActiveModel::Type::Boolean.new.cast(value)
7
+ end %>
8
+ <% signal_text_sheen_enabled = ActiveModel::Type::Boolean.new.cast(TurboChat.configuration.signal_text_sheen) %>
3
9
  <% current_participant = respond_to?(:current_chat_participant, true) ? current_chat_participant : nil %>
4
10
  <% current_participant_type = current_participant&.class&.base_class&.name %>
5
11
  <% current_participant_id = current_participant&.id %>
@@ -17,19 +23,18 @@
17
23
  data-chat-signal-at="<%= signal_message.created_at.to_i %>"
18
24
  data-chat-signal-participant-type="<%= signal_message.participant_type %>"
19
25
  data-chat-signal-participant-id="<%= signal_message.participant_id %>">
26
+ <% signal_text = if signal_message.signal_type_custom? && signal_message.signal_text.present?
27
+ signal_message.signal_text
28
+ else
29
+ signal_message.signal_type.to_s
30
+ end %>
20
31
  <strong><%= signal_message.participant_display_name %></strong>
21
- <% if signal_message.signal_type_custom? && signal_message.signal_text.present? %>
22
- <span class="chat-signal-text<%= " chat-signal-text--sheen" if signal_text_sheen_enabled %>">
23
- <% if signal_text_sheen_enabled %>
24
- <span class="chat-signal-text-sheen"><%= signal_message.signal_text %></span>
25
- <% else %>
26
- <%= signal_message.signal_text %>
27
- <% end %>
28
- </span>
29
- <% else %>
30
- <span class="chat-dots">
31
- <i></i><i></i><i></i>
32
- </span>
33
- <% end %>
32
+ <span class="chat-signal-text<%= " chat-signal-text--sheen" if signal_text_sheen_enabled %>">
33
+ <% if signal_text_sheen_enabled %>
34
+ <span class="chat-signal-text-sheen"><%= signal_text %></span>
35
+ <% else %>
36
+ <%= signal_text %>
37
+ <% end %>
38
+ </span>
34
39
  </div>
35
40
  <% end %>
@@ -10,6 +10,22 @@
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 %>
14
+ <% chat_messages = @chat_messages.to_a %>
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? %>
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? %>
26
+ <% show_members_invite_block = show_members_invite_controls && @can_invite_member %>
27
+ <% show_members_panel = @show_members && (show_members_list || show_members_invite_block) %>
28
+ <% show_invite_fallback = !@show_members && show_members_invite_block && show_invite_fallback_when_members_hidden %>
13
29
  <section class="<%= chat_shell_classes %>"
14
30
  data-chat-id="<%= @chat.id %>"
15
31
  data-chat-style="<%= chat_style_key %>"
@@ -32,47 +48,61 @@
32
48
  <header class="chat-header">
33
49
  <div class="chat-header-copy">
34
50
  <p class="chat-header-kicker"><%= @chat.closed? ? "Conversation closed" : "Conversation active" %></p>
35
- <div class="chat-header-title-row">
36
- <h1><%= @chat.title %></h1>
37
- <span class="chat-status <%= @chat.closed? ? "chat-status--closed" : "chat-status--open" %>">
38
- <%= @chat.closed? ? "Closed" : "Open" %>
39
- </span>
40
- </div>
41
- </div>
42
- <div class="chat-header-actions">
43
- <% if @chat.opened? && @can_close_chat %>
44
- <%= button_to "Close",
45
- close_chat_path(@chat),
46
- method: :patch,
47
- form_class: "chat-inline-form",
48
- class: "chat-btn chat-btn--danger",
49
- data: { turbo_confirm: "Close this chat?" } %>
50
- <% elsif @chat.closed? && @can_reopen_chat %>
51
- <%= button_to "Reopen",
52
- reopen_chat_path(@chat),
53
- method: :patch,
54
- form_class: "chat-inline-form",
55
- class: "chat-btn chat-btn--success" %>
51
+ <% if show_header_title || show_header_status %>
52
+ <div class="chat-header-title-row">
53
+ <% if show_header_title %>
54
+ <h1><%= @chat.title %></h1>
55
+ <% end %>
56
+ <% if show_header_status %>
57
+ <span class="chat-status <%= @chat.closed? ? "chat-status--closed" : "chat-status--open" %>">
58
+ <%= @chat.closed? ? "Closed" : "Open" %>
59
+ </span>
60
+ <% end %>
61
+ </div>
56
62
  <% end %>
57
-
58
- <%= button_to "Leave",
59
- leave_chat_path(@chat),
60
- method: :patch,
61
- form_class: "chat-inline-form",
62
- class: "chat-btn chat-btn--danger",
63
- data: { turbo_confirm: "Leave this chat?" } %>
64
- <%= link_to "Back", chats_path, class: "chat-btn chat-btn--ghost" %>
65
63
  </div>
64
+ <% if show_header_actions %>
65
+ <div class="chat-header-actions">
66
+ <% if show_header_close_action %>
67
+ <% if @chat.opened? && @can_close_chat %>
68
+ <%= button_to "Close",
69
+ close_chat_path(@chat),
70
+ method: :patch,
71
+ form_class: "chat-inline-form",
72
+ class: "chat-btn chat-btn--danger",
73
+ data: { turbo_confirm: "Close this chat?" } %>
74
+ <% elsif @chat.closed? && @can_reopen_chat %>
75
+ <%= button_to "Reopen",
76
+ reopen_chat_path(@chat),
77
+ method: :patch,
78
+ form_class: "chat-inline-form",
79
+ class: "chat-btn chat-btn--success" %>
80
+ <% end %>
81
+ <% end %>
82
+
83
+ <% if show_header_leave_action %>
84
+ <%= button_to "Leave",
85
+ leave_chat_path(@chat),
86
+ method: :patch,
87
+ form_class: "chat-inline-form",
88
+ class: "chat-btn chat-btn--danger",
89
+ data: { turbo_confirm: "Leave this chat?" } %>
90
+ <% end %>
91
+ <% if show_header_back_action %>
92
+ <%= link_to "Back", chats_path, class: "chat-btn chat-btn--ghost" %>
93
+ <% end %>
94
+ </div>
95
+ <% end %>
66
96
  </header>
67
97
 
68
- <% if @show_members %>
98
+ <% if show_members_panel %>
69
99
  <section class="chat-members">
70
100
  <details class="chat-members-panel">
71
101
  <summary class="chat-members-summary">
72
102
  <span>Members</span>
73
103
  </summary>
74
104
  <div class="chat-members-content">
75
- <% if @can_invite_member %>
105
+ <% if show_members_invite_block %>
76
106
  <div class="chat-members-invite <%= invite_option_rows.present? ? "chat-members-invite--active" : "chat-members-invite--hint" %>">
77
107
  <% if invite_option_rows.present? %>
78
108
  <%= render "turbo_chat/chats/invite_form",
@@ -85,15 +115,17 @@
85
115
  <% end %>
86
116
  </div>
87
117
  <% end %>
88
- <div class="chat-members-list-shell">
89
- <ul id="<%= dom_id(@chat, :member_entries) %>" class="chat-members-list" data-chat-member-list="true" data-chat-id="<%= @chat.id %>">
90
- <%= render "turbo_chat/chats/member_entries", chat: @chat %>
91
- </ul>
92
- </div>
118
+ <% if show_members_list %>
119
+ <div class="chat-members-list-shell">
120
+ <ul id="<%= dom_id(@chat, :member_entries) %>" class="chat-members-list" data-chat-member-list="true" data-chat-id="<%= @chat.id %>">
121
+ <%= render "turbo_chat/chats/member_entries", chat: @chat %>
122
+ </ul>
123
+ </div>
124
+ <% end %>
93
125
  </div>
94
126
  </details>
95
127
  </section>
96
- <% elsif @can_invite_member %>
128
+ <% elsif show_invite_fallback %>
97
129
  <div class="chat-invite">
98
130
  <% if invite_option_rows.present? %>
99
131
  <%= render "turbo_chat/chats/invite_form",
@@ -113,6 +145,7 @@
113
145
  <div id="<%= dom_id(@chat, :messages) %>"
114
146
  class="chat-messages"
115
147
  data-chat-id="<%= @chat.id %>"
148
+ data-chat-message-insert-position="<%= message_insert_position %>"
116
149
  data-chat-self-participant-type="<%= current_participant_type %>"
117
150
  data-chat-self-participant-id="<%= current_participant_id %>"
118
151
  data-chat-self-mention-tokens="<%= json_escape(self_mention_tokens.to_json) %>"
@@ -122,20 +155,20 @@
122
155
  data-chat-emit-mention-events="<%= chat_emit_mention_events? %>"
123
156
  data-chat-can-edit-own-messages="<%= @can_edit_own_messages %>"
124
157
  <%= %(style="#{mention_container_style}") if mention_container_style.present? %>>
125
- <%= render @chat_messages %>
158
+ <%= render chat_messages %>
126
159
  </div>
127
160
 
128
161
  <div id="<%= dom_id(@chat, :signals) %>"
129
162
  class="chat-signals"
130
- data-chat-show-self-signals="<%= TurboChat.configuration.show_self_signals %>"
131
- data-chat-signal-ttl-seconds="<%= TurboChat.configuration.signal_ttl_seconds %>"
163
+ data-chat-show-self-signals="<%= chat_show_self_signals? %>"
164
+ data-chat-signal-ttl-seconds="<%= chat_signal_ttl_seconds %>"
132
165
  data-chat-self-participant-type="<%= current_participant_type %>"
133
166
  data-chat-self-participant-id="<%= current_participant_id %>">
134
167
  <%= render "turbo_chat/chat_messages/signals", chat: @chat %>
135
168
  </div>
136
169
  </section>
137
170
 
138
- <% if @can_post_message %>
171
+ <% if can_render_composer %>
139
172
  <%= render "turbo_chat/chat_messages/form",
140
173
  chat: @chat,
141
174
  chat_message: @chat_message,
@@ -1,118 +1,136 @@
1
1
  TurboChat.configure do |config|
2
- # Permissions
3
- config.permission_adapter = TurboChat::Permission
2
+ # Chat scope
3
+ config.chat.permission_adapter = TurboChat::Permission
4
4
  # Class that answers permission checks for participants/chats.
5
5
 
6
6
  # Authentication
7
- # config.current_participant_resolver = ->(controller) { controller.send(:current_member) }
7
+ # config.chat.current_participant_resolver = ->(controller) { controller.send(:current_member) }
8
8
  # Fallback resolver when you do not define `current_chat_participant`.
9
9
 
10
- # Limits
11
- config.max_chat_participants = 10
10
+ # Chat limits
11
+ config.chat.max_chat_participants = 10
12
12
  # Maximum active members allowed in a chat.
13
- config.max_message_length = 1000
13
+ config.chat_message.max_message_length = 1000
14
14
  # Maximum characters allowed in a message body.
15
- config.message_history_limit = 200
15
+ config.chat_message.message_history_limit = 200
16
16
  # Number of recent messages loaded in chat history.
17
- config.active_chat_window = 5.minutes
17
+ config.chat.active_chat_window = 5.minutes
18
18
  # Duration used to classify chats as active.
19
+ config.chat.show_members = true
20
+ # Shows member list panel in chat UI.
21
+ config.chat.show_members_list = true
22
+ # Shows member entries list inside the members panel.
23
+ config.chat.show_members_invite_controls = true
24
+ # Shows invite controls in members panel (and in fallback invite area when enabled).
25
+ config.chat.show_invite_fallback_when_members_hidden = true
26
+ # Shows fallback invite area when members panel is hidden.
27
+ config.chat.system_messages = true
28
+ # Shows system timeline messages (joins, invites, moderation).
29
+ # config.chat.disable_input = true
30
+ # Disables composer rendering and blocks message submissions.
31
+ # config.chat.show_header_title = false
32
+ # Hides the chat title in the conversation header.
33
+ # config.chat.show_header_status = false
34
+ # Hides the Open/Closed status tag in the conversation header.
35
+ # config.chat.show_header_close_action = false
36
+ # Hides the Close/Reopen action in the conversation header.
37
+ # config.chat.show_header_leave_action = false
38
+ # Hides the Leave action in the conversation header.
39
+ # config.chat.show_header_back_action = false
40
+ # Hides the Back action in the conversation header.
19
41
 
20
- # Mentions and emoji
21
- config.enable_mentions = true
42
+ # Chat message behavior
43
+ config.chat_message.enable_mentions = true
22
44
  # Enables mention parsing/highlighting and mention UI.
23
- config.mention_filter_exclude_self = true
45
+ config.chat_message.mention_filter_exclude_self = true
24
46
  # Hides self from mention suggestions.
25
- config.mention_filter_hide_roles = true
47
+ config.chat_message.mention_filter_hide_roles = true
26
48
  # Hides @ROLE suggestions in mention picker.
27
- config.enable_emoji_aliases = true
49
+ config.chat_message.enable_emoji_aliases = true
28
50
  # Expands :alias: tokens using configured emoji aliases.
29
- config.emoji_aliases = TurboChat::Configuration::DEFAULT_EMOJI_ALIASES.dup
51
+ config.chat_message.emoji_aliases = TurboChat::Configuration::DEFAULT_EMOJI_ALIASES.dup
30
52
  # Hash of emoji alias mappings, e.g. "smile" => "😄".
31
53
 
32
54
  # Blocked words
33
- config.blocked_words = []
55
+ config.chat_message.blocked_words = []
34
56
  # Case-insensitive word list checked before message save.
35
- config.blocked_words_action = :reject
57
+ config.chat_message.blocked_words_action = :reject
36
58
  # `:reject` adds a validation error, `:scramble` rewrites matched words.
37
- config.blocked_words_scramble_chars = TurboChat::Configuration::DEFAULT_BLOCKED_WORDS_SCRAMBLE_CHARS.dup
59
+ config.chat_message.blocked_words_scramble_chars = TurboChat::Configuration::DEFAULT_BLOCKED_WORDS_SCRAMBLE_CHARS.dup
38
60
  # Character pool used when scrambling blocked words.
61
+ config.chat_message.message_source_labels = TurboChat::Configuration::DEFAULT_MESSAGE_SOURCE_LABELS.dup
62
+ # Maps message source keys to labels shown on non-default source badges.
63
+ # config.chat_message.message_insert_position = "append_end"
64
+ # Timeline insert behavior: `append_end` (near composer) or `append_start` (top of list).
65
+ # config.chat_message.render_message_html = true
66
+ # Renders/sanitizes HTML in message bodies.
67
+ config.chat_message.message_html_tags = %w[a b br code em i li ol p pre strong ul]
68
+ # Allowed HTML tags when `render_message_html` is true.
69
+ config.chat_message.message_html_attributes = %w[href target rel class]
70
+ # Allowed HTML attributes when `render_message_html` is true.
71
+ config.chat_message.timestamp_formatter = ->(timestamp, _chat_message) { I18n.l(timestamp.in_time_zone, format: :long) }
72
+ # Formatter lambda for displayed timestamps.
73
+ config.chat_message.role_formatter = ->(role, _chat_message) { role.to_s.humanize }
74
+ # Formatter lambda for displayed role labels.
39
75
 
40
- # Visuals
41
- # config.mention_mark_hex_color = "b42318"
76
+ # Style scope
77
+ # config.style.mention_mark_hex_color = "b42318"
42
78
  # Mention highlight color (without #).
43
- # config.mention_highlight_hex_color = "b42318"
79
+ # config.style.mention_highlight_hex_color = "b42318"
44
80
  # Legacy mention highlight color fallback.
45
- # config.own_message_hex_color = "eef6ff"
81
+ # config.style.own_message_hex_color = "eef6ff"
46
82
  # Bubble background color for current participant messages.
47
- # config.other_message_hex_color = "ffffff"
83
+ # config.style.other_message_hex_color = "ffffff"
48
84
  # Bubble background color for other participant messages.
49
- config.role_message_hex_colors = {}
85
+ config.style.role_message_hex_colors = {}
50
86
  # Map role key to message bubble color.
51
- config.show_timestamp = true
87
+ config.style.show_timestamp = true
52
88
  # Shows formatted timestamp on each message.
53
- # config.show_role = true
89
+ # config.style.show_role = true
54
90
  # Shows participant role label near messages.
55
- config.show_members = true
56
- # Shows member list panel in chat UI.
57
- config.system_messages = true
58
- # Shows system timeline messages (joins, invites, moderation).
59
- config.message_source_labels = TurboChat::Configuration::DEFAULT_MESSAGE_SOURCE_LABELS.dup
60
- # Maps message source keys to labels shown on non-default source badges.
61
- config.chat_style = "chat_style_bounded"
91
+ config.style.chat_style = "chat_style_bounded"
62
92
  # Chat layout style: `chat_style_bounded` or `chat_style_unbounded`.
63
- config.composer_placeholder_text = "start chatting"
93
+ config.style.composer_placeholder_text = "start chatting"
64
94
  # Placeholder text for the message composer input.
95
+ config.style.signal_text_sheen = true
96
+ # Adds bracketed sheen styling to custom signal text.
97
+ # config.style.message_css_class_resolver = ->(_chat_message) { "chat-message" }
98
+ # Returns extra CSS class names for a message wrapper.
65
99
 
66
100
  # Optional composer controls (disabled by default)
67
- # config.composer_add_files_display = true
101
+ # config.style.composer_add_files_display = true
68
102
  # Shows the add-files control in the composer.
69
- # config.composer_add_files_active = true
103
+ # config.style.composer_add_files_active = true
70
104
  # Enables add-files control interaction.
71
- # config.composer_microphone_display = true
105
+ # config.style.composer_microphone_display = true
72
106
  # Shows microphone control in composer.
73
- # config.composer_microphone_active = true
107
+ # config.style.composer_microphone_active = true
74
108
  # Enables microphone control interaction.
75
109
 
76
- # Optional browser events (disabled by default)
77
- # config.emit_typing_events = true
110
+ # Events scope (disabled by default)
111
+ # config.events.emit_typing_events = true
78
112
  # Emits `turbo-chat:typing-started` and `turbo-chat:typing-ended`.
79
- # config.emit_message_events = true
113
+ # config.events.emit_message_events = true
80
114
  # Emits `turbo-chat:message-sent`.
81
- # config.emit_mention_events = true
115
+ # config.events.emit_mention_events = true
82
116
  # Emits `turbo-chat:mention`.
83
- # config.emit_invitation_events = true
117
+ # config.events.emit_invitation_events = true
84
118
  # Emits `turbo-chat:invitation-accepted`.
85
- # config.emit_chat_lifecycle_events = true
119
+ # config.events.emit_chat_lifecycle_events = true
86
120
  # Emits `turbo-chat:chat-*` lifecycle events.
87
121
 
88
- # Optional ActiveSupport::Notifications events (disabled by default)
89
- # config.emit_moderation_events = true
122
+ # Moderation scope (disabled by default)
123
+ # config.moderation.emit_moderation_events = true
90
124
  # Emits `turbo_chat.moderation.*` notifications.
91
- # config.emit_blocked_words_events = true
125
+ # config.moderation.emit_blocked_words_events = true
92
126
  # Emits `turbo_chat.blocked_words.*` notifications.
93
127
 
94
- # Signal behavior
95
- config.signal_ttl_seconds = 60
128
+ # Signals scope
129
+ config.signals.signal_ttl_seconds = 60
96
130
  # Maximum age (in seconds) for active typing/signal indicators.
97
- config.signal_text_sheen = true
98
- # Adds bracketed sheen styling to custom signal text.
99
131
  # Optional toggles (disabled by default)
100
- # config.show_self_signals = true
132
+ # config.signals.show_self_signals = true
101
133
  # Shows your own active typing/signal indicators.
102
- # config.replace_signals_on_message_submit = true
134
+ # config.signals.replace_signals_on_message_submit = true
103
135
  # Replaces existing signals when sending a message.
104
-
105
- # Optional message rendering hooks
106
- # config.message_css_class_resolver = ->(_chat_message) { "chat-message" }
107
- # Returns extra CSS class names for a message wrapper.
108
- # config.render_message_html = true
109
- # Renders/sanitizes HTML in message bodies.
110
- config.message_html_tags = %w[a b br code em i li ol p pre strong ul]
111
- # Allowed HTML tags when `render_message_html` is true.
112
- config.message_html_attributes = %w[href target rel class]
113
- # Allowed HTML attributes when `render_message_html` is true.
114
- config.timestamp_formatter = ->(timestamp, _chat_message) { I18n.l(timestamp.in_time_zone, format: :long) }
115
- # Formatter lambda for displayed timestamps.
116
- config.role_formatter = ->(role, _chat_message) { role.to_s.humanize }
117
- # Formatter lambda for displayed role labels.
118
136
  end
@@ -1,4 +1,18 @@
1
1
  class TurboChat::Configuration
2
+ def self.build_attribute_scopes(scoped_defaults)
3
+ scoped_defaults.each_with_object({}) do |(scope_name, defaults), mapping|
4
+ defaults.each_key do |attribute|
5
+ if mapping.key?(attribute)
6
+ existing_scope = mapping.fetch(attribute)
7
+ raise ArgumentError, "Duplicate configuration attribute `#{attribute}` across scopes: #{existing_scope}, #{scope_name}"
8
+ end
9
+
10
+ mapping[attribute] = scope_name
11
+ end
12
+ end.freeze
13
+ end
14
+ private_class_method :build_attribute_scopes
15
+
2
16
  DEFAULT_ROLE_DEFINITIONS = {
3
17
  "member" => {
4
18
  name: "Member",
@@ -51,10 +65,25 @@ class TurboChat::Configuration
51
65
  "whatsapp" => "WhatsApp"
52
66
  }.freeze
53
67
 
54
- DEFAULTS = {
68
+ CHAT_DEFAULTS = {
55
69
  permission_adapter: -> { TurboChat::Permission },
56
70
  current_participant_resolver: nil,
57
71
  max_chat_participants: 10,
72
+ active_chat_window: -> { 5.minutes },
73
+ show_members: true,
74
+ show_members_list: true,
75
+ show_members_invite_controls: true,
76
+ show_invite_fallback_when_members_hidden: true,
77
+ system_messages: true,
78
+ disable_input: false,
79
+ show_header_title: true,
80
+ show_header_status: true,
81
+ show_header_close_action: true,
82
+ show_header_leave_action: true,
83
+ show_header_back_action: true
84
+ }.freeze
85
+
86
+ CHAT_MESSAGE_DEFAULTS = {
58
87
  max_message_length: 1000,
59
88
  message_history_limit: 200,
60
89
  enable_mentions: true,
@@ -65,6 +94,16 @@ class TurboChat::Configuration
65
94
  blocked_words: -> { [] },
66
95
  blocked_words_action: DEFAULT_BLOCKED_WORDS_ACTION,
67
96
  blocked_words_scramble_chars: -> { DEFAULT_BLOCKED_WORDS_SCRAMBLE_CHARS.dup },
97
+ message_insert_position: "append_end",
98
+ message_source_labels: -> { DEFAULT_MESSAGE_SOURCE_LABELS.dup },
99
+ render_message_html: false,
100
+ message_html_tags: -> { DEFAULT_MESSAGE_HTML_TAGS.dup },
101
+ message_html_attributes: -> { DEFAULT_MESSAGE_HTML_ATTRIBUTES.dup },
102
+ timestamp_formatter: -> { ->(timestamp, _chat_message = nil) { I18n.l(timestamp.in_time_zone, format: :long) } },
103
+ role_formatter: -> { ->(role, _chat_message = nil) { role.to_s.humanize } }
104
+ }.freeze
105
+
106
+ STYLE_DEFAULTS = {
68
107
  mention_mark_hex_color: nil,
69
108
  mention_highlight_hex_color: nil,
70
109
  own_message_hex_color: nil,
@@ -72,32 +111,50 @@ class TurboChat::Configuration
72
111
  role_message_hex_colors: -> { {} },
73
112
  show_timestamp: true,
74
113
  show_role: false,
75
- show_members: true,
76
- system_messages: true,
77
114
  composer_placeholder_text: "start chatting",
78
115
  composer_add_files_display: false,
79
116
  composer_add_files_active: false,
80
117
  composer_microphone_display: false,
81
118
  composer_microphone_active: false,
82
- active_chat_window: -> { 5.minutes },
119
+ chat_style: "chat_style_bounded",
120
+ message_css_class_resolver: nil,
121
+ signal_text_sheen: true
122
+ }.freeze
123
+
124
+ MODERATION_DEFAULTS = {
125
+ emit_moderation_events: false,
126
+ emit_blocked_words_events: false
127
+ }.freeze
128
+
129
+ EVENTS_DEFAULTS = {
83
130
  emit_typing_events: false,
84
131
  emit_message_events: false,
85
132
  emit_mention_events: false,
86
133
  emit_invitation_events: false,
87
- emit_chat_lifecycle_events: false,
88
- emit_moderation_events: false,
89
- emit_blocked_words_events: false,
134
+ emit_chat_lifecycle_events: false
135
+ }.freeze
136
+
137
+ SIGNALS_DEFAULTS = {
90
138
  signal_ttl_seconds: 60,
91
- signal_text_sheen: true,
92
139
  show_self_signals: false,
93
- replace_signals_on_message_submit: false,
94
- message_css_class_resolver: nil,
95
- message_source_labels: -> { DEFAULT_MESSAGE_SOURCE_LABELS.dup },
96
- chat_style: "chat_style_bounded",
97
- render_message_html: false,
98
- message_html_tags: -> { DEFAULT_MESSAGE_HTML_TAGS.dup },
99
- message_html_attributes: -> { DEFAULT_MESSAGE_HTML_ATTRIBUTES.dup },
100
- timestamp_formatter: -> { ->(timestamp, _chat_message = nil) { I18n.l(timestamp.in_time_zone, format: :long) } },
101
- role_formatter: -> { ->(role, _chat_message = nil) { role.to_s.humanize } }
140
+ replace_signals_on_message_submit: false
102
141
  }.freeze
142
+
143
+ SCOPED_DEFAULTS = {
144
+ chat: CHAT_DEFAULTS,
145
+ chat_message: CHAT_MESSAGE_DEFAULTS,
146
+ style: STYLE_DEFAULTS,
147
+ moderation: MODERATION_DEFAULTS,
148
+ events: EVENTS_DEFAULTS,
149
+ signals: SIGNALS_DEFAULTS
150
+ }.freeze
151
+
152
+ ATTRIBUTE_SCOPES = build_attribute_scopes(SCOPED_DEFAULTS)
153
+
154
+ DEFAULTS = ATTRIBUTE_SCOPES.each_with_object({}) do |(attribute, scope_name), defaults|
155
+ defaults[attribute] = SCOPED_DEFAULTS.fetch(scope_name).fetch(attribute)
156
+ end.freeze
157
+
158
+ SCOPE_NAMES = SCOPED_DEFAULTS.keys.freeze
159
+ ATTRIBUTES = ATTRIBUTE_SCOPES.keys.freeze
103
160
  end
@@ -7,19 +7,19 @@ class TurboChat::Configuration
7
7
  normalized_value = value.to_s.strip
8
8
  raise ArgumentError, "Emoji alias value cannot be blank" if normalized_value.blank?
9
9
 
10
- @emoji_aliases = effective_emoji_aliases.merge(key => normalized_value)
10
+ self.emoji_aliases = effective_emoji_aliases.merge(key => normalized_value)
11
11
  end
12
12
 
13
13
  def remove_emoji_alias(name)
14
14
  key = normalize_emoji_alias_key(name)
15
15
  return if key.blank?
16
16
 
17
- @emoji_aliases = effective_emoji_aliases.except(key)
17
+ self.emoji_aliases = effective_emoji_aliases.except(key)
18
18
  end
19
19
 
20
- def clear_emoji_aliases! = @emoji_aliases = {}
20
+ def clear_emoji_aliases! = self.emoji_aliases = {}
21
21
 
22
- def reset_emoji_aliases! = @emoji_aliases = DEFAULT_EMOJI_ALIASES.dup
22
+ def reset_emoji_aliases! = self.emoji_aliases = DEFAULT_EMOJI_ALIASES.dup
23
23
 
24
24
  def effective_emoji_aliases
25
25
  source = emoji_aliases.is_a?(Hash) ? emoji_aliases : {}
@@ -8,13 +8,78 @@ class TurboChat::Configuration
8
8
  include EmojiSupport
9
9
  include BlockedWordsSupport
10
10
 
11
- attr_accessor(*DEFAULTS.keys)
11
+ class Scope
12
+ class << self
13
+ attr_reader :scope_defaults
14
+
15
+ def configure_defaults(defaults)
16
+ @scope_defaults = defaults
17
+ attr_accessor(*defaults.keys)
18
+ end
19
+ end
20
+
21
+ def initialize
22
+ self.class.scope_defaults.each do |attribute, default_value|
23
+ instance_variable_set("@#{attribute}", TurboChat::Configuration.resolve_default_value(default_value))
24
+ end
25
+ end
26
+ end
27
+
28
+ class Chat < Scope
29
+ configure_defaults(SCOPED_DEFAULTS.fetch(:chat))
30
+ end
31
+
32
+ class ChatMessage < Scope
33
+ configure_defaults(SCOPED_DEFAULTS.fetch(:chat_message))
34
+ end
35
+
36
+ class Style < Scope
37
+ configure_defaults(SCOPED_DEFAULTS.fetch(:style))
38
+ end
39
+
40
+ class Moderation < Scope
41
+ configure_defaults(SCOPED_DEFAULTS.fetch(:moderation))
42
+ end
43
+
44
+ class Events < Scope
45
+ configure_defaults(SCOPED_DEFAULTS.fetch(:events))
46
+ end
47
+
48
+ class Signals < Scope
49
+ configure_defaults(SCOPED_DEFAULTS.fetch(:signals))
50
+ end
51
+
52
+ SCOPE_CLASSES = {
53
+ chat: Chat,
54
+ chat_message: ChatMessage,
55
+ style: Style,
56
+ moderation: Moderation,
57
+ events: Events,
58
+ signals: Signals
59
+ }.freeze
60
+
61
+ attr_reader(*SCOPE_NAMES)
62
+
63
+ ATTRIBUTE_SCOPES.each do |attribute, scope_name|
64
+ define_method(attribute) do
65
+ public_send(scope_name).public_send(attribute)
66
+ end
67
+
68
+ define_method("#{attribute}=") do |value|
69
+ public_send(scope_name).public_send("#{attribute}=", value)
70
+ end
71
+ end
12
72
 
13
73
  def initialize
14
- DEFAULTS.each do |attribute, default_value|
15
- value = default_value.respond_to?(:call) ? default_value.call : (default_value.dup rescue default_value)
16
- instance_variable_set("@#{attribute}", value)
74
+ SCOPE_CLASSES.each do |scope_name, klass|
75
+ instance_variable_set("@#{scope_name}", klass.new)
17
76
  end
18
77
  @additional_roles = {}
19
78
  end
79
+
80
+ class << self
81
+ def resolve_default_value(default_value)
82
+ default_value.respond_to?(:call) ? default_value.call : (default_value.dup rescue default_value)
83
+ end
84
+ end
20
85
  end
@@ -26,6 +26,7 @@ class TurboChat::Permission
26
26
 
27
27
  def can_post_message?
28
28
  can_view_chat? &&
29
+ !chat_input_disabled? &&
29
30
  role_permission?(:post_message) &&
30
31
  !chat_closed? &&
31
32
  !actor_membership_muted? &&
@@ -65,4 +66,14 @@ class TurboChat::Permission
65
66
  def can_close_chat? = can_view_chat? && role_permission?(:close_chat)
66
67
 
67
68
  def can_reopen_chat? = can_view_chat? && role_permission?(:reopen_chat)
69
+
70
+ private
71
+
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 StandardError
77
+ false
78
+ end
68
79
  end
@@ -1,3 +1,3 @@
1
1
  module TurboChat
2
- VERSION = "0.1.12"
2
+ VERSION = "0.1.15"
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.1.12
4
+ version: 0.1.15
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-02-26 00:00:00.000000000 Z
11
+ date: 2026-02-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails