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 +4 -4
- data/CHANGELOG.md +29 -0
- data/README.md +86 -34
- data/app/assets/javascripts/turbo_chat/shared.js +11 -0
- data/app/controllers/turbo_chat/application_controller.rb +10 -1
- data/app/controllers/turbo_chat/chat_messages_controller.rb +1 -1
- data/app/controllers/turbo_chat/chats_controller.rb +1 -1
- data/app/helpers/turbo_chat/application_helper/config_support.rb +32 -0
- data/app/models/turbo_chat/chat.rb +6 -1
- data/app/models/turbo_chat/chat_message/broadcasting.rb +30 -7
- data/app/views/turbo_chat/chat_messages/_signals.html.erb +20 -15
- data/app/views/turbo_chat/chats/show.html.erb +74 -41
- data/lib/generators/turbo_chat/install/templates/turbo_chat.rb +84 -66
- data/lib/turbo_chat/configuration/defaults.rb +74 -17
- data/lib/turbo_chat/configuration/emoji_support.rb +4 -4
- data/lib/turbo_chat/configuration.rb +69 -4
- data/lib/turbo_chat/permission.rb +11 -0
- data/lib/turbo_chat/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 62e1eb06f88659efb5df1cc8a72f33e669b2a969dd734abc8a977aa4331fe925
|
|
4
|
+
data.tar.gz: 5c40f2095d91a51a69cafd0086553ceab8ecf69894696bc8de2f86498170e52a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
131
|
-
config.
|
|
132
|
-
|
|
133
|
-
config.
|
|
134
|
-
config.
|
|
135
|
-
config.
|
|
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?
|
|
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?
|
|
14
|
-
|
|
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 =
|
|
2
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
89
|
-
<
|
|
90
|
-
<%=
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
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
|
|
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="<%=
|
|
131
|
-
data-chat-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
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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.
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17
|
+
self.emoji_aliases = effective_emoji_aliases.except(key)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
def clear_emoji_aliases! =
|
|
20
|
+
def clear_emoji_aliases! = self.emoji_aliases = {}
|
|
21
21
|
|
|
22
|
-
def reset_emoji_aliases! =
|
|
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
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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
|
data/lib/turbo_chat/version.rb
CHANGED
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.
|
|
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-
|
|
11
|
+
date: 2026-02-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|