turbo_chat 0.1.11 → 0.1.14
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 +54 -33
- data/app/assets/javascripts/turbo_chat/realtime.js +20 -2
- data/app/assets/javascripts/turbo_chat/shared.js +12 -1
- 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 +24 -0
- data/app/models/turbo_chat/chat.rb +15 -2
- 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 +9 -3
- data/lib/generators/turbo_chat/install/templates/turbo_chat.rb +70 -66
- data/lib/turbo_chat/configuration/defaults.rb +67 -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: b1c6f0dd5991186ff33af55ffef201d7ab37af996f24884e4e25fb2fe8bf6b0c
|
|
4
|
+
data.tar.gz: e0d4b6d10fff71303104ea2d0f797d62222d694d4a27aa7201699730abad6c7a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4968592c0c8462d24a43db894c60229fbcb6d172d0036612934d22e4713e66e454712d2b28c6ce53b9b5e34f8560e6b5f1e7cd5f624e6cfbe696fa1ddfd3747f
|
|
7
|
+
data.tar.gz: eafbf1fdfbc738c0922ca8885c53537eac0377c25ce494dfc04a70038a25e3e63fad432960c39b6b31e25bfa2ce5782cc052e44a797a771d14c53d3a8e7cf0f3
|
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.14] - 2026-02-28
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Scoped configuration namespaces for clearer organization: `config.chat`, `config.chat_message`, `config.style`, `config.moderation`, `config.events`, and `config.signals`.
|
|
9
|
+
- Scoped installer initializer output with inline guidance grouped by scope.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- Updated README examples to use scoped configuration.
|
|
13
|
+
- Flat configuration aliases (for example `config.max_message_length`) remain supported for backward compatibility.
|
|
14
|
+
|
|
15
|
+
## [0.1.13] - 2026-02-26
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- Configurable timeline insert behavior via `config.message_insert_position` (`append_end` or `append_start`).
|
|
19
|
+
- Configurable composer disable toggle via `config.disable_input` to hide input and reject message posts.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- 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).
|
|
23
|
+
- `config.signal_text_sheen` is now consistently cast as a boolean during signal rendering.
|
|
24
|
+
|
|
25
|
+
## [0.1.12] - 2026-02-26
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- Configurable signal lifetime via `config.signal_ttl_seconds` (default `60`) so applications can keep typing/status indicators active longer.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
- Signal visibility/pruning now use the configured signal TTL consistently on both server-rendered active signals and client-side signal cleanup.
|
|
32
|
+
- Chat show renders signal TTL metadata for frontend pruning coordination.
|
|
33
|
+
|
|
5
34
|
## [0.1.11] - 2026-02-26
|
|
6
35
|
|
|
7
36
|
### Changed
|
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,31 +110,36 @@ 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
|
-
|
|
132
|
-
config.
|
|
133
|
-
config.
|
|
134
|
-
|
|
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.signals.signal_ttl_seconds = 60
|
|
133
|
+
config.style.signal_text_sheen = true
|
|
134
|
+
|
|
135
|
+
config.moderation.emit_moderation_events = false
|
|
136
|
+
config.moderation.emit_blocked_words_events = false
|
|
137
|
+
config.events.emit_mention_events = false
|
|
135
138
|
end
|
|
136
139
|
```
|
|
137
140
|
|
|
141
|
+
Flat aliases (for example `config.max_message_length`) still work for backward compatibility.
|
|
142
|
+
|
|
138
143
|
## Message Ingest API
|
|
139
144
|
|
|
140
145
|
Post messages as a specific participant, including external sources like WhatsApp:
|
|
@@ -167,7 +172,7 @@ Source labels shown in message badges are configurable:
|
|
|
167
172
|
|
|
168
173
|
```ruby
|
|
169
174
|
TurboChat.configure do |config|
|
|
170
|
-
config.message_source_labels = {
|
|
175
|
+
config.chat_message.message_source_labels = {
|
|
171
176
|
"app" => "In App",
|
|
172
177
|
"whatsapp" => "WhatsApp",
|
|
173
178
|
"sms_gateway" => "SMS"
|
|
@@ -179,7 +184,23 @@ Chat UI layout style is configurable:
|
|
|
179
184
|
|
|
180
185
|
```ruby
|
|
181
186
|
TurboChat.configure do |config|
|
|
182
|
-
config.chat_style = "chat_style_unbounded" # or "chat_style_bounded"
|
|
187
|
+
config.style.chat_style = "chat_style_unbounded" # or "chat_style_bounded"
|
|
188
|
+
end
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Timeline insert position is configurable:
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
TurboChat.configure do |config|
|
|
195
|
+
config.chat_message.message_insert_position = "append_end" # or "append_start"
|
|
196
|
+
end
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Composer input can be disabled globally:
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
TurboChat.configure do |config|
|
|
203
|
+
config.chat.disable_input = true
|
|
183
204
|
end
|
|
184
205
|
```
|
|
185
206
|
|
|
@@ -243,13 +264,13 @@ Enable only what you consume:
|
|
|
243
264
|
|
|
244
265
|
```ruby
|
|
245
266
|
TurboChat.configure do |config|
|
|
246
|
-
config.emit_typing_events = true
|
|
247
|
-
config.emit_message_events = true
|
|
248
|
-
config.emit_mention_events = true
|
|
249
|
-
config.emit_invitation_events = true
|
|
250
|
-
config.emit_chat_lifecycle_events = true
|
|
251
|
-
config.emit_moderation_events = true
|
|
252
|
-
config.emit_blocked_words_events = true
|
|
267
|
+
config.events.emit_typing_events = true
|
|
268
|
+
config.events.emit_message_events = true
|
|
269
|
+
config.events.emit_mention_events = true
|
|
270
|
+
config.events.emit_invitation_events = true
|
|
271
|
+
config.events.emit_chat_lifecycle_events = true
|
|
272
|
+
config.moderation.emit_moderation_events = true
|
|
273
|
+
config.moderation.emit_blocked_words_events = true
|
|
253
274
|
end
|
|
254
275
|
```
|
|
255
276
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
(function (namespace) {
|
|
2
2
|
var constants = namespace.constants || {};
|
|
3
|
-
var SIGNAL_TTL_SECONDS = constants.SIGNAL_TTL_SECONDS ||
|
|
3
|
+
var SIGNAL_TTL_SECONDS = constants.SIGNAL_TTL_SECONDS || 60;
|
|
4
4
|
var SIGNAL_START_DELAY_MS = constants.SIGNAL_START_DELAY_MS || 750;
|
|
5
5
|
var SIGNAL_IDLE_GRACE_MS = constants.SIGNAL_IDLE_GRACE_MS || 2500;
|
|
6
6
|
var SIGNAL_HEARTBEAT_MS = constants.SIGNAL_HEARTBEAT_MS || 4000;
|
|
@@ -72,6 +72,24 @@
|
|
|
72
72
|
container.__chatSignalDeactivateTimeoutId = null;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
function signalTtlSecondsForNode(node) {
|
|
76
|
+
if (!node || typeof node.closest !== "function") {
|
|
77
|
+
return SIGNAL_TTL_SECONDS;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
var container = node.closest(".chat-signals");
|
|
81
|
+
if (!container || !container.dataset) {
|
|
82
|
+
return SIGNAL_TTL_SECONDS;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
var parsedTtl = parseInt(container.dataset.chatSignalTtlSeconds || "", 10);
|
|
86
|
+
if (isNaN(parsedTtl) || parsedTtl <= 0) {
|
|
87
|
+
return SIGNAL_TTL_SECONDS;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return parsedTtl;
|
|
91
|
+
}
|
|
92
|
+
|
|
75
93
|
function queueSignalDeactivate(container) {
|
|
76
94
|
if (!container || container.__chatSignalDeactivateTimeoutId) {
|
|
77
95
|
return;
|
|
@@ -210,7 +228,7 @@
|
|
|
210
228
|
return;
|
|
211
229
|
}
|
|
212
230
|
|
|
213
|
-
if (now - at >
|
|
231
|
+
if (now - at > signalTtlSecondsForNode(node)) {
|
|
214
232
|
collapseSignalNode(node);
|
|
215
233
|
}
|
|
216
234
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
(function (namespace) {
|
|
2
2
|
var constants = {
|
|
3
|
-
SIGNAL_TTL_SECONDS:
|
|
3
|
+
SIGNAL_TTL_SECONDS: 60,
|
|
4
4
|
SIGNAL_START_DELAY_MS: 750,
|
|
5
5
|
SIGNAL_IDLE_GRACE_MS: 2500,
|
|
6
6
|
SIGNAL_HEARTBEAT_MS: 4000,
|
|
@@ -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,8 @@ module TurboChat
|
|
|
8
8
|
emit_invitation_events: false,
|
|
9
9
|
emit_chat_lifecycle_events: false,
|
|
10
10
|
show_members: true,
|
|
11
|
+
show_self_signals: false,
|
|
12
|
+
disable_input: false,
|
|
11
13
|
composer_add_files_display: false,
|
|
12
14
|
composer_add_files_active: false,
|
|
13
15
|
composer_microphone_display: false,
|
|
@@ -79,6 +81,28 @@ module TurboChat
|
|
|
79
81
|
chat_unbounded_style? ? "chat-shell--style-unbounded" : "chat-shell--style-bounded"
|
|
80
82
|
end
|
|
81
83
|
|
|
84
|
+
def chat_message_insert_position
|
|
85
|
+
raw_position = chat_config_value(:message_insert_position, default: "append_end")
|
|
86
|
+
normalized = raw_position.to_s.strip.downcase
|
|
87
|
+
|
|
88
|
+
case normalized
|
|
89
|
+
when "append_start", "start", "prepend"
|
|
90
|
+
"append_start"
|
|
91
|
+
else
|
|
92
|
+
"append_end"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def chat_message_append_start?
|
|
97
|
+
chat_message_insert_position == "append_start"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def chat_signal_ttl_seconds
|
|
101
|
+
value = chat_config_value(:signal_ttl_seconds, default: 60)
|
|
102
|
+
ttl = value.to_i
|
|
103
|
+
ttl.positive? ? ttl : 60
|
|
104
|
+
end
|
|
105
|
+
|
|
82
106
|
private
|
|
83
107
|
|
|
84
108
|
def chat_config_value(method_name, default: nil)
|
|
@@ -33,8 +33,8 @@ module TurboChat
|
|
|
33
33
|
where.not(id: active(window: window).select(:id))
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
def active_signals(window:
|
|
37
|
-
cutoff = Time.current - window
|
|
36
|
+
def active_signals(window: nil)
|
|
37
|
+
cutoff = Time.current - self.class.signal_window_seconds(window)
|
|
38
38
|
latest_message_at = chat_messages
|
|
39
39
|
.message
|
|
40
40
|
.where("created_at >= ?", cutoff)
|
|
@@ -104,6 +104,19 @@ module TurboChat
|
|
|
104
104
|
raise ArgumentError, "active chat window must be a positive duration"
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
+
def self.signal_window_seconds(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
|
|
114
|
+
seconds = value.to_i
|
|
115
|
+
return seconds if seconds.positive?
|
|
116
|
+
|
|
117
|
+
raise ArgumentError, "signal ttl must be a positive duration"
|
|
118
|
+
end
|
|
119
|
+
|
|
107
120
|
private
|
|
108
121
|
|
|
109
122
|
def normalize_message_limit(limit)
|
|
@@ -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,10 @@
|
|
|
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? %>
|
|
13
17
|
<section class="<%= chat_shell_classes %>"
|
|
14
18
|
data-chat-id="<%= @chat.id %>"
|
|
15
19
|
data-chat-style="<%= chat_style_key %>"
|
|
@@ -113,6 +117,7 @@
|
|
|
113
117
|
<div id="<%= dom_id(@chat, :messages) %>"
|
|
114
118
|
class="chat-messages"
|
|
115
119
|
data-chat-id="<%= @chat.id %>"
|
|
120
|
+
data-chat-message-insert-position="<%= message_insert_position %>"
|
|
116
121
|
data-chat-self-participant-type="<%= current_participant_type %>"
|
|
117
122
|
data-chat-self-participant-id="<%= current_participant_id %>"
|
|
118
123
|
data-chat-self-mention-tokens="<%= json_escape(self_mention_tokens.to_json) %>"
|
|
@@ -122,19 +127,20 @@
|
|
|
122
127
|
data-chat-emit-mention-events="<%= chat_emit_mention_events? %>"
|
|
123
128
|
data-chat-can-edit-own-messages="<%= @can_edit_own_messages %>"
|
|
124
129
|
<%= %(style="#{mention_container_style}") if mention_container_style.present? %>>
|
|
125
|
-
<%= render
|
|
130
|
+
<%= render chat_messages %>
|
|
126
131
|
</div>
|
|
127
132
|
|
|
128
133
|
<div id="<%= dom_id(@chat, :signals) %>"
|
|
129
134
|
class="chat-signals"
|
|
130
|
-
data-chat-show-self-signals="<%=
|
|
135
|
+
data-chat-show-self-signals="<%= chat_show_self_signals? %>"
|
|
136
|
+
data-chat-signal-ttl-seconds="<%= chat_signal_ttl_seconds %>"
|
|
131
137
|
data-chat-self-participant-type="<%= current_participant_type %>"
|
|
132
138
|
data-chat-self-participant-id="<%= current_participant_id %>">
|
|
133
139
|
<%= render "turbo_chat/chat_messages/signals", chat: @chat %>
|
|
134
140
|
</div>
|
|
135
141
|
</section>
|
|
136
142
|
|
|
137
|
-
<% if
|
|
143
|
+
<% if can_render_composer %>
|
|
138
144
|
<%= render "turbo_chat/chat_messages/form",
|
|
139
145
|
chat: @chat,
|
|
140
146
|
chat_message: @chat_message,
|
|
@@ -1,116 +1,120 @@
|
|
|
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
|
|
18
|
-
# Duration
|
|
17
|
+
config.chat.active_chat_window = 5.minutes
|
|
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.system_messages = true
|
|
22
|
+
# Shows system timeline messages (joins, invites, moderation).
|
|
23
|
+
# config.chat.disable_input = true
|
|
24
|
+
# Disables composer rendering and blocks message submissions.
|
|
19
25
|
|
|
20
|
-
#
|
|
21
|
-
config.enable_mentions = true
|
|
26
|
+
# Chat message behavior
|
|
27
|
+
config.chat_message.enable_mentions = true
|
|
22
28
|
# Enables mention parsing/highlighting and mention UI.
|
|
23
|
-
config.mention_filter_exclude_self = true
|
|
29
|
+
config.chat_message.mention_filter_exclude_self = true
|
|
24
30
|
# Hides self from mention suggestions.
|
|
25
|
-
config.mention_filter_hide_roles = true
|
|
31
|
+
config.chat_message.mention_filter_hide_roles = true
|
|
26
32
|
# Hides @ROLE suggestions in mention picker.
|
|
27
|
-
config.enable_emoji_aliases = true
|
|
33
|
+
config.chat_message.enable_emoji_aliases = true
|
|
28
34
|
# Expands :alias: tokens using configured emoji aliases.
|
|
29
|
-
config.emoji_aliases = TurboChat::Configuration::DEFAULT_EMOJI_ALIASES.dup
|
|
35
|
+
config.chat_message.emoji_aliases = TurboChat::Configuration::DEFAULT_EMOJI_ALIASES.dup
|
|
30
36
|
# Hash of emoji alias mappings, e.g. "smile" => "😄".
|
|
31
37
|
|
|
32
38
|
# Blocked words
|
|
33
|
-
config.blocked_words = []
|
|
39
|
+
config.chat_message.blocked_words = []
|
|
34
40
|
# Case-insensitive word list checked before message save.
|
|
35
|
-
config.blocked_words_action = :reject
|
|
41
|
+
config.chat_message.blocked_words_action = :reject
|
|
36
42
|
# `:reject` adds a validation error, `:scramble` rewrites matched words.
|
|
37
|
-
config.blocked_words_scramble_chars = TurboChat::Configuration::DEFAULT_BLOCKED_WORDS_SCRAMBLE_CHARS.dup
|
|
43
|
+
config.chat_message.blocked_words_scramble_chars = TurboChat::Configuration::DEFAULT_BLOCKED_WORDS_SCRAMBLE_CHARS.dup
|
|
38
44
|
# Character pool used when scrambling blocked words.
|
|
45
|
+
config.chat_message.message_source_labels = TurboChat::Configuration::DEFAULT_MESSAGE_SOURCE_LABELS.dup
|
|
46
|
+
# Maps message source keys to labels shown on non-default source badges.
|
|
47
|
+
# config.chat_message.message_insert_position = "append_end"
|
|
48
|
+
# Timeline insert behavior: `append_end` (near composer) or `append_start` (top of list).
|
|
49
|
+
# config.chat_message.render_message_html = true
|
|
50
|
+
# Renders/sanitizes HTML in message bodies.
|
|
51
|
+
config.chat_message.message_html_tags = %w[a b br code em i li ol p pre strong ul]
|
|
52
|
+
# Allowed HTML tags when `render_message_html` is true.
|
|
53
|
+
config.chat_message.message_html_attributes = %w[href target rel class]
|
|
54
|
+
# Allowed HTML attributes when `render_message_html` is true.
|
|
55
|
+
config.chat_message.timestamp_formatter = ->(timestamp, _chat_message) { I18n.l(timestamp.in_time_zone, format: :long) }
|
|
56
|
+
# Formatter lambda for displayed timestamps.
|
|
57
|
+
config.chat_message.role_formatter = ->(role, _chat_message) { role.to_s.humanize }
|
|
58
|
+
# Formatter lambda for displayed role labels.
|
|
39
59
|
|
|
40
|
-
#
|
|
41
|
-
# config.mention_mark_hex_color = "b42318"
|
|
60
|
+
# Style scope
|
|
61
|
+
# config.style.mention_mark_hex_color = "b42318"
|
|
42
62
|
# Mention highlight color (without #).
|
|
43
|
-
# config.mention_highlight_hex_color = "b42318"
|
|
63
|
+
# config.style.mention_highlight_hex_color = "b42318"
|
|
44
64
|
# Legacy mention highlight color fallback.
|
|
45
|
-
# config.own_message_hex_color = "eef6ff"
|
|
65
|
+
# config.style.own_message_hex_color = "eef6ff"
|
|
46
66
|
# Bubble background color for current participant messages.
|
|
47
|
-
# config.other_message_hex_color = "ffffff"
|
|
67
|
+
# config.style.other_message_hex_color = "ffffff"
|
|
48
68
|
# Bubble background color for other participant messages.
|
|
49
|
-
config.role_message_hex_colors = {}
|
|
69
|
+
config.style.role_message_hex_colors = {}
|
|
50
70
|
# Map role key to message bubble color.
|
|
51
|
-
config.show_timestamp = true
|
|
71
|
+
config.style.show_timestamp = true
|
|
52
72
|
# Shows formatted timestamp on each message.
|
|
53
|
-
# config.show_role = true
|
|
73
|
+
# config.style.show_role = true
|
|
54
74
|
# 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"
|
|
75
|
+
config.style.chat_style = "chat_style_bounded"
|
|
62
76
|
# Chat layout style: `chat_style_bounded` or `chat_style_unbounded`.
|
|
63
|
-
config.composer_placeholder_text = "start chatting"
|
|
77
|
+
config.style.composer_placeholder_text = "start chatting"
|
|
64
78
|
# Placeholder text for the message composer input.
|
|
79
|
+
config.style.signal_text_sheen = true
|
|
80
|
+
# Adds bracketed sheen styling to custom signal text.
|
|
81
|
+
# config.style.message_css_class_resolver = ->(_chat_message) { "chat-message" }
|
|
82
|
+
# Returns extra CSS class names for a message wrapper.
|
|
65
83
|
|
|
66
84
|
# Optional composer controls (disabled by default)
|
|
67
|
-
# config.composer_add_files_display = true
|
|
85
|
+
# config.style.composer_add_files_display = true
|
|
68
86
|
# Shows the add-files control in the composer.
|
|
69
|
-
# config.composer_add_files_active = true
|
|
87
|
+
# config.style.composer_add_files_active = true
|
|
70
88
|
# Enables add-files control interaction.
|
|
71
|
-
# config.composer_microphone_display = true
|
|
89
|
+
# config.style.composer_microphone_display = true
|
|
72
90
|
# Shows microphone control in composer.
|
|
73
|
-
# config.composer_microphone_active = true
|
|
91
|
+
# config.style.composer_microphone_active = true
|
|
74
92
|
# Enables microphone control interaction.
|
|
75
93
|
|
|
76
|
-
#
|
|
77
|
-
# config.emit_typing_events = true
|
|
94
|
+
# Events scope (disabled by default)
|
|
95
|
+
# config.events.emit_typing_events = true
|
|
78
96
|
# Emits `turbo-chat:typing-started` and `turbo-chat:typing-ended`.
|
|
79
|
-
# config.emit_message_events = true
|
|
97
|
+
# config.events.emit_message_events = true
|
|
80
98
|
# Emits `turbo-chat:message-sent`.
|
|
81
|
-
# config.emit_mention_events = true
|
|
99
|
+
# config.events.emit_mention_events = true
|
|
82
100
|
# Emits `turbo-chat:mention`.
|
|
83
|
-
# config.emit_invitation_events = true
|
|
101
|
+
# config.events.emit_invitation_events = true
|
|
84
102
|
# Emits `turbo-chat:invitation-accepted`.
|
|
85
|
-
# config.emit_chat_lifecycle_events = true
|
|
103
|
+
# config.events.emit_chat_lifecycle_events = true
|
|
86
104
|
# Emits `turbo-chat:chat-*` lifecycle events.
|
|
87
105
|
|
|
88
|
-
#
|
|
89
|
-
# config.emit_moderation_events = true
|
|
106
|
+
# Moderation scope (disabled by default)
|
|
107
|
+
# config.moderation.emit_moderation_events = true
|
|
90
108
|
# Emits `turbo_chat.moderation.*` notifications.
|
|
91
|
-
# config.emit_blocked_words_events = true
|
|
109
|
+
# config.moderation.emit_blocked_words_events = true
|
|
92
110
|
# Emits `turbo_chat.blocked_words.*` notifications.
|
|
93
111
|
|
|
94
|
-
#
|
|
95
|
-
config.
|
|
96
|
-
#
|
|
112
|
+
# Signals scope
|
|
113
|
+
config.signals.signal_ttl_seconds = 60
|
|
114
|
+
# Maximum age (in seconds) for active typing/signal indicators.
|
|
97
115
|
# Optional toggles (disabled by default)
|
|
98
|
-
# config.show_self_signals = true
|
|
116
|
+
# config.signals.show_self_signals = true
|
|
99
117
|
# Shows your own active typing/signal indicators.
|
|
100
|
-
# config.replace_signals_on_message_submit = true
|
|
118
|
+
# config.signals.replace_signals_on_message_submit = true
|
|
101
119
|
# Replaces existing signals when sending a message.
|
|
102
|
-
|
|
103
|
-
# Optional message rendering hooks
|
|
104
|
-
# config.message_css_class_resolver = ->(_chat_message) { "chat-message" }
|
|
105
|
-
# Returns extra CSS class names for a message wrapper.
|
|
106
|
-
# config.render_message_html = true
|
|
107
|
-
# Renders/sanitizes HTML in message bodies.
|
|
108
|
-
config.message_html_tags = %w[a b br code em i li ol p pre strong ul]
|
|
109
|
-
# Allowed HTML tags when `render_message_html` is true.
|
|
110
|
-
config.message_html_attributes = %w[href target rel class]
|
|
111
|
-
# Allowed HTML attributes when `render_message_html` is true.
|
|
112
|
-
config.timestamp_formatter = ->(timestamp, _chat_message) { I18n.l(timestamp.in_time_zone, format: :long) }
|
|
113
|
-
# Formatter lambda for displayed timestamps.
|
|
114
|
-
config.role_formatter = ->(role, _chat_message) { role.to_s.humanize }
|
|
115
|
-
# Formatter lambda for displayed role labels.
|
|
116
120
|
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,17 @@ 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
|
+
system_messages: true,
|
|
75
|
+
disable_input: false
|
|
76
|
+
}.freeze
|
|
77
|
+
|
|
78
|
+
CHAT_MESSAGE_DEFAULTS = {
|
|
58
79
|
max_message_length: 1000,
|
|
59
80
|
message_history_limit: 200,
|
|
60
81
|
enable_mentions: true,
|
|
@@ -65,6 +86,16 @@ class TurboChat::Configuration
|
|
|
65
86
|
blocked_words: -> { [] },
|
|
66
87
|
blocked_words_action: DEFAULT_BLOCKED_WORDS_ACTION,
|
|
67
88
|
blocked_words_scramble_chars: -> { DEFAULT_BLOCKED_WORDS_SCRAMBLE_CHARS.dup },
|
|
89
|
+
message_insert_position: "append_end",
|
|
90
|
+
message_source_labels: -> { DEFAULT_MESSAGE_SOURCE_LABELS.dup },
|
|
91
|
+
render_message_html: false,
|
|
92
|
+
message_html_tags: -> { DEFAULT_MESSAGE_HTML_TAGS.dup },
|
|
93
|
+
message_html_attributes: -> { DEFAULT_MESSAGE_HTML_ATTRIBUTES.dup },
|
|
94
|
+
timestamp_formatter: -> { ->(timestamp, _chat_message = nil) { I18n.l(timestamp.in_time_zone, format: :long) } },
|
|
95
|
+
role_formatter: -> { ->(role, _chat_message = nil) { role.to_s.humanize } }
|
|
96
|
+
}.freeze
|
|
97
|
+
|
|
98
|
+
STYLE_DEFAULTS = {
|
|
68
99
|
mention_mark_hex_color: nil,
|
|
69
100
|
mention_highlight_hex_color: nil,
|
|
70
101
|
own_message_hex_color: nil,
|
|
@@ -72,31 +103,50 @@ class TurboChat::Configuration
|
|
|
72
103
|
role_message_hex_colors: -> { {} },
|
|
73
104
|
show_timestamp: true,
|
|
74
105
|
show_role: false,
|
|
75
|
-
show_members: true,
|
|
76
|
-
system_messages: true,
|
|
77
106
|
composer_placeholder_text: "start chatting",
|
|
78
107
|
composer_add_files_display: false,
|
|
79
108
|
composer_add_files_active: false,
|
|
80
109
|
composer_microphone_display: false,
|
|
81
110
|
composer_microphone_active: false,
|
|
82
|
-
|
|
111
|
+
chat_style: "chat_style_bounded",
|
|
112
|
+
message_css_class_resolver: nil,
|
|
113
|
+
signal_text_sheen: true
|
|
114
|
+
}.freeze
|
|
115
|
+
|
|
116
|
+
MODERATION_DEFAULTS = {
|
|
117
|
+
emit_moderation_events: false,
|
|
118
|
+
emit_blocked_words_events: false
|
|
119
|
+
}.freeze
|
|
120
|
+
|
|
121
|
+
EVENTS_DEFAULTS = {
|
|
83
122
|
emit_typing_events: false,
|
|
84
123
|
emit_message_events: false,
|
|
85
124
|
emit_mention_events: false,
|
|
86
125
|
emit_invitation_events: false,
|
|
87
|
-
emit_chat_lifecycle_events: false
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
126
|
+
emit_chat_lifecycle_events: false
|
|
127
|
+
}.freeze
|
|
128
|
+
|
|
129
|
+
SIGNALS_DEFAULTS = {
|
|
130
|
+
signal_ttl_seconds: 60,
|
|
91
131
|
show_self_signals: false,
|
|
92
|
-
replace_signals_on_message_submit: false
|
|
93
|
-
message_css_class_resolver: nil,
|
|
94
|
-
message_source_labels: -> { DEFAULT_MESSAGE_SOURCE_LABELS.dup },
|
|
95
|
-
chat_style: "chat_style_bounded",
|
|
96
|
-
render_message_html: false,
|
|
97
|
-
message_html_tags: -> { DEFAULT_MESSAGE_HTML_TAGS.dup },
|
|
98
|
-
message_html_attributes: -> { DEFAULT_MESSAGE_HTML_ATTRIBUTES.dup },
|
|
99
|
-
timestamp_formatter: -> { ->(timestamp, _chat_message = nil) { I18n.l(timestamp.in_time_zone, format: :long) } },
|
|
100
|
-
role_formatter: -> { ->(role, _chat_message = nil) { role.to_s.humanize } }
|
|
132
|
+
replace_signals_on_message_submit: false
|
|
101
133
|
}.freeze
|
|
134
|
+
|
|
135
|
+
SCOPED_DEFAULTS = {
|
|
136
|
+
chat: CHAT_DEFAULTS,
|
|
137
|
+
chat_message: CHAT_MESSAGE_DEFAULTS,
|
|
138
|
+
style: STYLE_DEFAULTS,
|
|
139
|
+
moderation: MODERATION_DEFAULTS,
|
|
140
|
+
events: EVENTS_DEFAULTS,
|
|
141
|
+
signals: SIGNALS_DEFAULTS
|
|
142
|
+
}.freeze
|
|
143
|
+
|
|
144
|
+
ATTRIBUTE_SCOPES = build_attribute_scopes(SCOPED_DEFAULTS)
|
|
145
|
+
|
|
146
|
+
DEFAULTS = ATTRIBUTE_SCOPES.each_with_object({}) do |(attribute, scope_name), defaults|
|
|
147
|
+
defaults[attribute] = SCOPED_DEFAULTS.fetch(scope_name).fetch(attribute)
|
|
148
|
+
end.freeze
|
|
149
|
+
|
|
150
|
+
SCOPE_NAMES = SCOPED_DEFAULTS.keys.freeze
|
|
151
|
+
ATTRIBUTES = ATTRIBUTE_SCOPES.keys.freeze
|
|
102
152
|
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.14
|
|
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
|