turbo_chat 0.1.2 → 0.1.3
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/README.md +192 -607
- data/app/assets/config/turbo_chat_manifest.js +6 -0
- data/app/assets/javascripts/turbo_chat/application.js +4 -0
- data/app/assets/javascripts/{chat_gem → turbo_chat}/lifecycle_events.js +1 -1
- data/app/assets/javascripts/{chat_gem → turbo_chat}/messages.js +4 -4
- data/app/assets/javascripts/{chat_gem → turbo_chat}/realtime.js +3 -3
- data/app/controllers/turbo_chat/application_controller.rb +66 -0
- data/app/controllers/{chat_gem → turbo_chat}/chat_memberships_controller.rb +4 -4
- data/app/controllers/{chat_gem → turbo_chat}/chat_messages_controller.rb +7 -7
- data/app/controllers/{chat_gem → turbo_chat}/chats_controller/event_payload_support.rb +5 -5
- data/app/controllers/{chat_gem → turbo_chat}/chats_controller/invitation_support.rb +4 -4
- data/app/controllers/{chat_gem → turbo_chat}/chats_controller.rb +10 -10
- data/app/helpers/{chat_gem → turbo_chat}/application_helper/config_support.rb +2 -2
- data/app/helpers/{chat_gem → turbo_chat}/application_helper/mention_support/entry_builder.rb +1 -1
- data/app/helpers/{chat_gem → turbo_chat}/application_helper/mention_support/permission_support.rb +2 -2
- data/app/helpers/{chat_gem → turbo_chat}/application_helper/mention_support/token_builder.rb +1 -1
- data/app/helpers/{chat_gem → turbo_chat}/application_helper/mention_support.rb +5 -5
- data/app/helpers/{chat_gem → turbo_chat}/application_helper/message_rendering.rb +11 -11
- data/app/helpers/{chat_gem → turbo_chat}/application_helper/participant_support.rb +2 -2
- data/app/helpers/turbo_chat/application_helper.rb +12 -0
- data/app/models/{chat_gem → turbo_chat}/application_record.rb +1 -1
- data/app/models/{chat_gem → turbo_chat}/chat.rb +7 -7
- data/app/models/{chat_gem → turbo_chat}/chat_membership.rb +5 -5
- data/app/models/{chat_gem → turbo_chat}/chat_message/blocked_words_moderation.rb +7 -7
- data/app/models/{chat_gem → turbo_chat}/chat_message/body_length_validation.rb +2 -2
- data/app/models/{chat_gem → turbo_chat}/chat_message/broadcasting.rb +1 -1
- data/app/models/{chat_gem → turbo_chat}/chat_message/formatting.rb +3 -3
- data/app/models/{chat_gem → turbo_chat}/chat_message/mention_validation.rb +3 -3
- data/app/models/{chat_gem → turbo_chat}/chat_message/signals.rb +2 -2
- data/app/models/{chat_gem → turbo_chat}/chat_message.rb +11 -11
- data/app/views/layouts/turbo_chat/application.html.erb +20 -0
- data/app/views/turbo_chat/chat_messages/_chat_message.html.erb +1 -0
- data/app/views/{chat_gem → turbo_chat}/chat_messages/_form.html.erb +2 -2
- data/app/views/{chat_gem → turbo_chat}/chat_messages/_message.html.erb +2 -2
- data/app/views/{chat_gem → turbo_chat}/chat_messages/_signals.html.erb +1 -1
- data/app/views/{chat_gem → turbo_chat}/chats/show.html.erb +3 -3
- data/config/routes.rb +1 -1
- data/db/migrate/20260215000000_create_turbo_chat_chats.rb +8 -0
- data/db/migrate/{20260215000001_create_chat_gem_chat_memberships.rb → 20260215000001_create_turbo_chat_chat_memberships.rb} +5 -5
- data/db/migrate/{20260215000002_create_chat_gem_chat_messages.rb → 20260215000002_create_turbo_chat_chat_messages.rb} +4 -4
- data/db/migrate/20260218000011_add_closed_at_to_turbo_chat_chats.rb +6 -0
- data/db/migrate/20260218000012_add_custom_role_key_to_chat_memberships.rb +2 -2
- data/db/migrate/20260218000013_add_invitation_accepted_to_turbo_chat_chat_memberships.rb +5 -0
- data/lib/generators/turbo_chat/install/install_generator.rb +1 -1
- data/lib/generators/turbo_chat/install/templates/turbo_chat.rb +2 -0
- data/lib/tasks/turbo_chat_tasks.rake +6 -2
- data/lib/{chat_gem → turbo_chat}/configuration.rb +4 -2
- data/lib/turbo_chat/engine.rb +29 -0
- data/lib/{chat_gem → turbo_chat}/model_extensions/chat_participant.rb +6 -6
- data/lib/{chat_gem → turbo_chat}/moderation.rb +11 -11
- data/lib/{chat_gem → turbo_chat}/permission.rb +1 -1
- data/lib/{chat_gem → turbo_chat}/signals.rb +5 -5
- data/lib/turbo_chat/version.rb +1 -3
- data/lib/turbo_chat.rb +11 -15
- metadata +53 -58
- data/app/assets/config/chat_gem_manifest.js +0 -6
- data/app/assets/javascripts/chat_gem/application.js +0 -4
- data/app/controllers/chat_gem/application_controller.rb +0 -41
- data/app/helpers/chat_gem/application_helper.rb +0 -12
- data/app/views/chat_gem/chat_messages/_chat_message.html.erb +0 -1
- data/app/views/layouts/chat_gem/application.html.erb +0 -20
- data/db/migrate/20260215000000_create_chat_gem_chats.rb +0 -8
- data/db/migrate/20260218000011_add_closed_at_to_chat_gem_chats.rb +0 -6
- data/db/migrate/20260218000013_add_invitation_accepted_to_chat_gem_chat_memberships.rb +0 -5
- data/lib/chat_gem/engine.rb +0 -29
- data/lib/chat_gem/version.rb +0 -3
- data/lib/chat_gem.rb +0 -24
- data/lib/generators/chat_gem/install/install_generator.rb +0 -18
- data/lib/generators/chat_gem/install/templates/chat_gem.rb +0 -36
- data/lib/tasks/chat_gem_tasks.rake +0 -1
- /data/app/assets/javascripts/{chat_gem → turbo_chat}/shared.js +0 -0
- /data/app/assets/stylesheets/{chat_gem → turbo_chat}/application.css +0 -0
- /data/app/views/{chat_gem → turbo_chat}/chat_messages/_signal.html.erb +0 -0
- /data/app/views/{chat_gem → turbo_chat}/chat_messages/index.html.erb +0 -0
- /data/app/views/{chat_gem → turbo_chat}/chats/index.html.erb +0 -0
- /data/app/views/{chat_gem → turbo_chat}/chats/new.html.erb +0 -0
data/README.md
CHANGED
|
@@ -1,64 +1,65 @@
|
|
|
1
1
|
# TurboChat
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
## Quick Start
|
|
25
|
-
|
|
26
|
-
1. Add the gem to your host app:
|
|
3
|
+
TurboChat is a mountable Rails chat engine for server-rendered apps.
|
|
4
|
+
|
|
5
|
+
What you get:
|
|
6
|
+
- Turbo Stream chat UI.
|
|
7
|
+
- Role-based permissions.
|
|
8
|
+
- Mentions, typing signals, invites, moderation.
|
|
9
|
+
- Practical customization hooks without rewriting everything.
|
|
10
|
+
|
|
11
|
+
What this is not:
|
|
12
|
+
- A hosted chat service.
|
|
13
|
+
- A React-first component library.
|
|
14
|
+
- A "just add JS" widget.
|
|
15
|
+
|
|
16
|
+
## Use This If
|
|
17
|
+
|
|
18
|
+
Use TurboChat if your app is Rails-first and you want chat now, not after building a custom permission/event/moderation system.
|
|
19
|
+
|
|
20
|
+
Skip it if you want fully custom frontend architecture from day one.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
27
23
|
|
|
28
24
|
```ruby
|
|
29
25
|
# Gemfile
|
|
30
26
|
gem "turbo_chat"
|
|
31
27
|
```
|
|
32
28
|
|
|
33
|
-
2. Install and copy setup files:
|
|
34
|
-
|
|
35
29
|
```bash
|
|
36
30
|
bundle install
|
|
37
31
|
bin/rails generate turbo_chat:install
|
|
38
32
|
bin/rails db:migrate
|
|
39
33
|
```
|
|
40
34
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
```bash
|
|
44
|
-
bin/rails turbo_chat:install:migrations
|
|
45
|
-
bin/rails db:migrate
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
3. Mount the engine:
|
|
35
|
+
Mount with an explicit helper prefix:
|
|
49
36
|
|
|
50
37
|
```ruby
|
|
51
38
|
# config/routes.rb
|
|
52
|
-
mount TurboChat::Engine => "/"
|
|
39
|
+
mount TurboChat::Engine => "/chat", as: "turbo_chat"
|
|
53
40
|
```
|
|
54
41
|
|
|
55
|
-
|
|
42
|
+
Opinionated recommendation: mount under `/chat` (or another scoped path), not root.
|
|
43
|
+
|
|
44
|
+
## Participant Resolution (Most Important Section)
|
|
45
|
+
|
|
46
|
+
This is where installs usually fail. TurboChat now resolves the current participant in this order:
|
|
47
|
+
|
|
48
|
+
1. `current_chat_participant` on your host `ApplicationController` (preferred explicit hook).
|
|
49
|
+
2. `config.current_participant_resolver` (custom resolver lambda).
|
|
50
|
+
3. `current_user` (if your app exposes it).
|
|
51
|
+
|
|
52
|
+
If none are available, TurboChat raises `NotImplementedError` with guidance.
|
|
53
|
+
|
|
54
|
+
Return value must be:
|
|
55
|
+
- `nil` (unauthenticated), or
|
|
56
|
+
- a model that uses `acts_as_chat_participant`.
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
If you use Devise and your `User` model is a chat participant, this works out of the box.
|
|
58
59
|
|
|
59
|
-
|
|
60
|
+
## Host Contract
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
### Participant model opt-in
|
|
62
63
|
|
|
63
64
|
```ruby
|
|
64
65
|
class User < ApplicationRecord
|
|
@@ -66,122 +67,75 @@ class User < ApplicationRecord
|
|
|
66
67
|
end
|
|
67
68
|
```
|
|
68
69
|
|
|
69
|
-
###
|
|
70
|
-
|
|
71
|
-
Expose the current participant from your host `ApplicationController`:
|
|
70
|
+
### Optional explicit hook (recommended for clarity)
|
|
72
71
|
|
|
73
72
|
```ruby
|
|
74
73
|
class ApplicationController < ActionController::Base
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def chat_current_participant
|
|
78
|
-
Current.user
|
|
74
|
+
def current_chat_participant
|
|
75
|
+
current_user
|
|
79
76
|
end
|
|
80
77
|
end
|
|
81
78
|
```
|
|
82
79
|
|
|
83
|
-
|
|
80
|
+
### Optional custom resolver (for non-`current_user` auth)
|
|
84
81
|
|
|
85
|
-
|
|
82
|
+
```ruby
|
|
83
|
+
TurboChat.configure do |config|
|
|
84
|
+
config.current_participant_resolver = ->(controller) { controller.send(:current_member) }
|
|
85
|
+
end
|
|
86
|
+
```
|
|
86
87
|
|
|
87
|
-
|
|
88
|
+
## First Working Usage
|
|
88
89
|
|
|
89
90
|
```ruby
|
|
90
91
|
chat = TurboChat::Chat.create!(title: "Support")
|
|
91
|
-
TurboChat::ChatMembership.create!(chat: chat, participant:
|
|
92
|
+
TurboChat::ChatMembership.create!(chat: chat, participant: current_user, role: :admin)
|
|
92
93
|
```
|
|
93
94
|
|
|
94
95
|
```erb
|
|
95
|
-
<%= link_to "Open chat",
|
|
96
|
+
<%= link_to "Open chat", turbo_chat.chat_path(chat) %>
|
|
96
97
|
```
|
|
97
98
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
## Feature Overview
|
|
101
|
-
|
|
102
|
-
- Mountable chat UI with Turbo Stream updates.
|
|
103
|
-
- Message rows + signal rows (`typing`, `thinking`, `planning`).
|
|
104
|
-
- Role-aware permissions, mentions, moderation, and chat close/reopen.
|
|
105
|
-
- Configurable styling and optional sanitized HTML rendering.
|
|
106
|
-
- Optional browser events for typing and message submit lifecycle.
|
|
107
|
-
- Programmatic signal helpers for user-facing typing/thinking/planning states.
|
|
108
|
-
|
|
109
|
-
## Configuration
|
|
110
|
-
|
|
111
|
-
### Key options by concern
|
|
112
|
-
|
|
113
|
-
#### Access and limits
|
|
114
|
-
|
|
115
|
-
- `config.permission_adapter` (`TurboChat::Permission` by default).
|
|
116
|
-
- `config.max_chat_participants` (`10` by default).
|
|
117
|
-
- `config.max_message_length` (`1000` by default).
|
|
118
|
-
- `config.message_history_limit` (`200` by default; set `nil` or `0` to disable).
|
|
119
|
-
|
|
120
|
-
#### Behavior and lifecycle
|
|
121
|
-
|
|
122
|
-
- `config.active_chat_window` (`5.minutes` by default).
|
|
123
|
-
- `config.show_self_signals` (`false` by default).
|
|
124
|
-
- `config.replace_signals_on_message_submit` (`false` by default; clears a participant's existing signals when they submit a regular message).
|
|
125
|
-
|
|
126
|
-
#### Mentions and emoji
|
|
127
|
-
|
|
128
|
-
- `config.enable_mentions` (`true` by default).
|
|
129
|
-
- `config.mention_filter_exclude_self` (`true` by default; hides current participant from mention autocomplete options).
|
|
130
|
-
- `config.mention_filter_hide_roles` (`true` by default; hides role mention options like `@ADMIN` from autocomplete).
|
|
131
|
-
- `config.enable_emoji_aliases` (`true` by default).
|
|
132
|
-
- `config.emoji_aliases` (`TurboChat::Configuration::DEFAULT_EMOJI_ALIASES.dup` by default).
|
|
133
|
-
- `config.blocked_words` (`[]` by default).
|
|
134
|
-
- `config.blocked_words_action` (`:reject` by default; supports `:reject` or `:scramble`).
|
|
135
|
-
|
|
136
|
-
#### Rendering and styling
|
|
99
|
+
## Routes You Actually Need
|
|
137
100
|
|
|
138
|
-
|
|
139
|
-
- `
|
|
140
|
-
- `
|
|
141
|
-
- `
|
|
142
|
-
- `
|
|
143
|
-
- `
|
|
144
|
-
- `
|
|
145
|
-
- `
|
|
146
|
-
- `
|
|
147
|
-
- `
|
|
148
|
-
- `
|
|
149
|
-
- `
|
|
101
|
+
From the mounted engine:
|
|
102
|
+
- `GET /chats`
|
|
103
|
+
- `GET /chats/:id`
|
|
104
|
+
- `POST /chats`
|
|
105
|
+
- `PATCH /chats/:id/accept`
|
|
106
|
+
- `PATCH /chats/:id/decline`
|
|
107
|
+
- `PATCH /chats/:id/leave`
|
|
108
|
+
- `PATCH /chats/:id/close`
|
|
109
|
+
- `PATCH /chats/:id/reopen`
|
|
110
|
+
- `POST /chats/:chat_id/chat_memberships`
|
|
111
|
+
- `POST /chats/:chat_id/chat_messages`
|
|
112
|
+
- `PATCH /chats/:chat_id/chat_messages/:id`
|
|
150
113
|
|
|
151
|
-
|
|
114
|
+
## Recommended Baseline Config
|
|
152
115
|
|
|
153
|
-
|
|
154
|
-
- `config.emit_message_events` (`false` by default).
|
|
155
|
-
- `config.emit_mention_events` (`false` by default).
|
|
156
|
-
- `config.emit_invitation_events` (`false` by default).
|
|
157
|
-
- `config.emit_chat_lifecycle_events` (`false` by default).
|
|
158
|
-
- `config.emit_moderation_events` (`false` by default; emits `ActiveSupport::Notifications` moderation events).
|
|
159
|
-
- `config.emit_blocked_words_events` (`false` by default; emits `ActiveSupport::Notifications` blocked-word moderation events).
|
|
160
|
-
|
|
161
|
-
<details>
|
|
162
|
-
<summary>Full default initializer</summary>
|
|
116
|
+
Keep this simple initially:
|
|
163
117
|
|
|
164
118
|
```ruby
|
|
165
119
|
TurboChat.configure do |config|
|
|
166
120
|
config.permission_adapter = TurboChat::Permission
|
|
121
|
+
|
|
167
122
|
config.max_chat_participants = 10
|
|
168
123
|
config.max_message_length = 1000
|
|
169
124
|
config.message_history_limit = 200
|
|
125
|
+
config.active_chat_window = 5.minutes
|
|
126
|
+
|
|
170
127
|
config.enable_mentions = true
|
|
171
128
|
config.mention_filter_exclude_self = true
|
|
172
129
|
config.mention_filter_hide_roles = true
|
|
173
130
|
config.enable_emoji_aliases = true
|
|
174
|
-
|
|
131
|
+
|
|
175
132
|
config.blocked_words = []
|
|
176
|
-
config.blocked_words_action = :reject
|
|
177
|
-
|
|
178
|
-
config.
|
|
179
|
-
config.own_message_hex_color = nil
|
|
180
|
-
config.other_message_hex_color = nil
|
|
181
|
-
config.role_message_hex_colors = {}
|
|
133
|
+
config.blocked_words_action = :reject # or :scramble
|
|
134
|
+
|
|
135
|
+
config.render_message_html = false
|
|
182
136
|
config.show_timestamp = true
|
|
183
137
|
config.show_role = false
|
|
184
|
-
|
|
138
|
+
|
|
185
139
|
config.emit_typing_events = false
|
|
186
140
|
config.emit_message_events = false
|
|
187
141
|
config.emit_mention_events = false
|
|
@@ -189,553 +143,184 @@ TurboChat.configure do |config|
|
|
|
189
143
|
config.emit_chat_lifecycle_events = false
|
|
190
144
|
config.emit_moderation_events = false
|
|
191
145
|
config.emit_blocked_words_events = false
|
|
192
|
-
config.show_self_signals = false
|
|
193
|
-
config.replace_signals_on_message_submit = false
|
|
194
|
-
config.message_css_class_resolver = nil
|
|
195
|
-
config.render_message_html = false
|
|
196
|
-
config.message_html_tags = %w[a b br code em i li ol p pre strong ul]
|
|
197
|
-
config.message_html_attributes = %w[href target rel class]
|
|
198
|
-
config.timestamp_formatter = ->(timestamp, _chat_message) { I18n.l(timestamp.in_time_zone, format: :long) }
|
|
199
|
-
config.role_formatter = ->(role, _chat_message) { role.to_s.humanize }
|
|
200
|
-
end
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
</details>
|
|
204
|
-
|
|
205
|
-
## Core Concepts
|
|
206
|
-
|
|
207
|
-
### Chat Lifecycle
|
|
208
|
-
|
|
209
|
-
A chat is considered active when it has a regular message within the configured window (`config.active_chat_window`).
|
|
210
|
-
Signal rows do not count as activity. Closed chats (`closed_at` set) remain viewable but cannot receive new messages.
|
|
211
|
-
|
|
212
|
-
```ruby
|
|
213
|
-
TurboChat.configure do |config|
|
|
214
|
-
config.active_chat_window = 5.minutes
|
|
215
146
|
end
|
|
216
|
-
|
|
217
|
-
chat.active? # => true/false
|
|
218
|
-
chat.inactive? # => true/false
|
|
219
|
-
|
|
220
|
-
TurboChat::Chat.active
|
|
221
|
-
TurboChat::Chat.inactive
|
|
222
|
-
TurboChat::Chat.active(window: 10.minutes)
|
|
223
147
|
```
|
|
224
148
|
|
|
225
|
-
|
|
149
|
+
Opinionated defaults:
|
|
150
|
+
- Leave HTML rendering off unless required.
|
|
151
|
+
- Leave event emission off unless consumed.
|
|
152
|
+
- Keep permissions adapter default until you have a concrete policy gap.
|
|
226
153
|
|
|
227
|
-
|
|
154
|
+
## Roles and Permissions
|
|
228
155
|
|
|
229
|
-
-
|
|
230
|
-
-
|
|
231
|
-
-
|
|
156
|
+
Built-in roles:
|
|
157
|
+
- `member`
|
|
158
|
+
- `moderator`
|
|
159
|
+
- `admin`
|
|
232
160
|
|
|
233
|
-
|
|
161
|
+
Behavior summary:
|
|
162
|
+
- `member`: view/post and member mentions.
|
|
163
|
+
- `moderator`: invite, `@all`, role mentions, mute/timeout/ban, delete lower-rank messages.
|
|
164
|
+
- `admin`: moderator powers plus close/reopen chat.
|
|
234
165
|
|
|
235
|
-
|
|
166
|
+
Hard rules:
|
|
167
|
+
- Moderation is rank-based.
|
|
168
|
+
- No self-moderation.
|
|
169
|
+
- Closed chat blocks posting.
|
|
170
|
+
- Muted/timed-out members cannot post.
|
|
236
171
|
|
|
237
|
-
|
|
238
|
-
- `:mention_all` controls `@all`.
|
|
239
|
-
- `:mention_role` controls role mentions.
|
|
240
|
-
|
|
241
|
-
Emoji aliases are enabled by default for plain-text message rendering.
|
|
242
|
-
|
|
243
|
-
#### Add aliases incrementally
|
|
172
|
+
Custom role example:
|
|
244
173
|
|
|
245
174
|
```ruby
|
|
246
175
|
TurboChat.configure do |config|
|
|
247
|
-
config.
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
#### Override alias map
|
|
253
|
-
|
|
254
|
-
```ruby
|
|
255
|
-
TurboChat.configure do |config|
|
|
256
|
-
config.emoji_aliases = TurboChat::Configuration::DEFAULT_EMOJI_ALIASES.merge(
|
|
257
|
-
"shipit" => "🚢",
|
|
258
|
-
"party_parrot" => "🦜"
|
|
176
|
+
config.add_role(
|
|
177
|
+
:support_agent,
|
|
178
|
+
name: "Support Agent",
|
|
179
|
+
rank: 1,
|
|
180
|
+
permissions: %i[view_chat post_message delete_message]
|
|
259
181
|
)
|
|
260
182
|
end
|
|
261
183
|
```
|
|
262
184
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
Configure blocked words and choose whether to reject messages or scramble blocked words.
|
|
266
|
-
|
|
267
|
-
`scramble` now shuffles the blocked word's own characters (for example, `badword` -> `darbwod`).
|
|
268
|
-
|
|
269
|
-
```ruby
|
|
270
|
-
TurboChat.configure do |config|
|
|
271
|
-
config.blocked_words = %w[foo bar]
|
|
272
|
-
config.blocked_words_action = :reject
|
|
273
|
-
end
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
```ruby
|
|
277
|
-
TurboChat.configure do |config|
|
|
278
|
-
config.blocked_words = %w[foo bar]
|
|
279
|
-
config.blocked_words_action = :scramble
|
|
280
|
-
end
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
## UI Customization
|
|
284
|
-
|
|
285
|
-
### Styling and Custom Markup
|
|
286
|
-
|
|
287
|
-
#### Bubble colors
|
|
288
|
-
|
|
289
|
-
```ruby
|
|
290
|
-
TurboChat.configure do |config|
|
|
291
|
-
config.own_message_hex_color = "#c9f2ff"
|
|
292
|
-
config.other_message_hex_color = "#f6f8fb"
|
|
293
|
-
config.role_message_hex_colors = {
|
|
294
|
-
admin: "#ffe6e6",
|
|
295
|
-
moderator: { own: "#fff0c2", other: "#fff7de" },
|
|
296
|
-
support_agent: { default: "#e9f8ff" }
|
|
297
|
-
}
|
|
298
|
-
end
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
Role-specific colors override own/other defaults. Invalid hex values are ignored.
|
|
302
|
-
|
|
303
|
-
Viewer-targeted mentions can be color-customized:
|
|
304
|
-
|
|
305
|
-
```ruby
|
|
306
|
-
TurboChat.configure do |config|
|
|
307
|
-
config.mention_mark_hex_color = "#cf1322"
|
|
308
|
-
end
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
#### CSS class resolver (basic)
|
|
312
|
-
|
|
313
|
-
```ruby
|
|
314
|
-
TurboChat.configure do |config|
|
|
315
|
-
config.message_css_class_resolver = ->(_chat_message, own_message) {
|
|
316
|
-
own_message ? "msg-card msg-card--own" : "msg-card msg-card--other"
|
|
317
|
-
}
|
|
318
|
-
end
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
```html
|
|
322
|
-
<article id="chat_gem_chat_message_42" class="chat-bubble chat-bubble--own msg-card msg-card--own">
|
|
323
|
-
<header class="chat-meta">
|
|
324
|
-
<span class="chat-meta__author">you@example.com</span>
|
|
325
|
-
<time datetime="2026-02-18T16:40:00Z">February 18, 2026 4:40 PM</time>
|
|
326
|
-
</header>
|
|
327
|
-
<p class="chat-body">Hello world</p>
|
|
328
|
-
</article>
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
#### CSS class resolver (role-aware)
|
|
332
|
-
|
|
333
|
-
```ruby
|
|
334
|
-
TurboChat.configure do |config|
|
|
335
|
-
config.message_css_class_resolver = lambda { |chat_message, own_message|
|
|
336
|
-
classes = ["msg-card"]
|
|
337
|
-
classes << (own_message ? "msg-card--own" : "msg-card--other")
|
|
338
|
-
classes << "msg-card--role-#{chat_message.participant_membership_role}" if chat_message.participant_membership_role.present?
|
|
339
|
-
classes << "msg-card--long" if chat_message.body.to_s.length > 280
|
|
340
|
-
classes
|
|
341
|
-
}
|
|
342
|
-
end
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
#### Full markup override
|
|
346
|
-
|
|
347
|
-
`message_css_class_resolver` controls classes only. To change structure, override the message partial in your host app.
|
|
348
|
-
|
|
349
|
-
1. Create `app/views/chat_gem/chat_messages/_message.html.erb`.
|
|
350
|
-
2. Copy the engine partial and customize it.
|
|
351
|
-
3. Keep `id="<%= dom_id(chat_message) %>"` on the wrapper so Turbo updates/removals keep working.
|
|
352
|
-
|
|
353
|
-
```erb
|
|
354
|
-
<% own_message = own_chat_message?(chat_message) %>
|
|
355
|
-
<% show_timestamp = TurboChat.configuration.show_timestamp %>
|
|
356
|
-
<article id="<%= dom_id(chat_message) %>" class="<%= chat_message_css_classes(chat_message: chat_message, own_message: own_message) %>">
|
|
357
|
-
<div class="msg-card__header">
|
|
358
|
-
<span class="chat-meta__author"><%= chat_message.participant_display_name %></span>
|
|
359
|
-
<% if show_timestamp %>
|
|
360
|
-
<time datetime="<%= chat_message.created_at.iso8601 %>"><%= chat_message.formatted_timestamp %></time>
|
|
361
|
-
<% end %>
|
|
362
|
-
</div>
|
|
363
|
-
|
|
364
|
-
<div class="msg-card__body">
|
|
365
|
-
<%= render_chat_message_body(chat_message) %>
|
|
366
|
-
</div>
|
|
367
|
-
|
|
368
|
-
<div class="msg-card__footer">
|
|
369
|
-
<!-- custom badges/actions -->
|
|
370
|
-
</div>
|
|
371
|
-
</article>
|
|
372
|
-
```
|
|
373
|
-
|
|
374
|
-
### Rich HTML Message Rendering
|
|
375
|
-
|
|
376
|
-
#### Enable sanitized rendering
|
|
377
|
-
|
|
378
|
-
Enable sanitized HTML rendering for message bodies:
|
|
379
|
-
|
|
380
|
-
```ruby
|
|
381
|
-
TurboChat.configure do |config|
|
|
382
|
-
config.render_message_html = true
|
|
383
|
-
config.message_html_tags = %w[a b br code em i li ol p pre strong ul]
|
|
384
|
-
config.message_html_attributes = %w[href target rel class]
|
|
385
|
-
end
|
|
386
|
-
```
|
|
387
|
-
|
|
388
|
-
#### Extend the allowlist
|
|
389
|
-
|
|
390
|
-
Extend the allowlist as needed:
|
|
391
|
-
|
|
392
|
-
```ruby
|
|
393
|
-
TurboChat.configure do |config|
|
|
394
|
-
config.render_message_html = true
|
|
395
|
-
config.message_html_tags = TurboChat::Configuration::DEFAULT_MESSAGE_HTML_TAGS + %w[blockquote h4 mark]
|
|
396
|
-
config.message_html_attributes = TurboChat::Configuration::DEFAULT_MESSAGE_HTML_ATTRIBUTES + %w[title]
|
|
397
|
-
end
|
|
398
|
-
```
|
|
399
|
-
|
|
400
|
-
#### Simple Example
|
|
401
|
-
|
|
402
|
-
Given:
|
|
403
|
-
|
|
404
|
-
```html
|
|
405
|
-
<h4 title="notice">Update</h4><blockquote><mark>Done</mark></blockquote><u>underline</u>
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
Rendered/sanitized output:
|
|
185
|
+
## Invitations
|
|
409
186
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
187
|
+
- Invite creation: `POST /chats/:chat_id/chat_memberships`
|
|
188
|
+
- Accept: `PATCH /chats/:id/accept`
|
|
189
|
+
- Decline: `PATCH /chats/:id/decline`
|
|
413
190
|
|
|
414
|
-
|
|
191
|
+
Important constraints:
|
|
192
|
+
- Invite type must match inviter participant base class.
|
|
193
|
+
- Participant limits apply to active memberships.
|
|
194
|
+
- Re-invite reactivates existing memberships.
|
|
415
195
|
|
|
416
|
-
|
|
196
|
+
## Messages, Mentions, Signals
|
|
417
197
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
```
|
|
198
|
+
### Messages
|
|
199
|
+
- Realtime append/update/remove via Turbo Streams.
|
|
200
|
+
- Inline edit for your own messages (permission-gated).
|
|
201
|
+
- History capped by `message_history_limit`.
|
|
423
202
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
203
|
+
### Mentions
|
|
204
|
+
- `@username`, `@all`, `@ROLE`.
|
|
205
|
+
- Server-side mention permission validation.
|
|
206
|
+
- Autocomplete defaults: excludes self, hides roles.
|
|
428
207
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
});
|
|
432
|
-
```
|
|
208
|
+
### Signals
|
|
209
|
+
Automatic typing loop is built in.
|
|
433
210
|
|
|
434
|
-
|
|
211
|
+
Manual APIs:
|
|
435
212
|
|
|
436
213
|
```ruby
|
|
437
|
-
TurboChat.
|
|
438
|
-
|
|
439
|
-
|
|
214
|
+
TurboChat::Signals.start!(chat: chat, participant: current_user, signal_type: :thinking)
|
|
215
|
+
TurboChat::Signals.replace!(chat: chat, participant: current_user, signal_type: :planning)
|
|
216
|
+
TurboChat::Signals.clear!(chat: chat, participant: current_user)
|
|
440
217
|
```
|
|
441
218
|
|
|
442
|
-
```js
|
|
443
|
-
document.addEventListener("chat-gem:message-sent", function (event) {
|
|
444
|
-
// event.detail.chatId
|
|
445
|
-
});
|
|
446
|
-
```
|
|
447
|
-
|
|
448
|
-
#### Mention event
|
|
449
|
-
|
|
450
219
|
```ruby
|
|
451
|
-
TurboChat.
|
|
452
|
-
|
|
220
|
+
TurboChat::Signals.with(chat: chat, participant: current_user, signal_type: :thinking) do
|
|
221
|
+
# work
|
|
453
222
|
end
|
|
454
223
|
```
|
|
455
224
|
|
|
456
|
-
|
|
457
|
-
document.addEventListener("chat-gem:mention", function (event) {
|
|
458
|
-
// event.detail.chatId
|
|
459
|
-
// event.detail.messageId
|
|
460
|
-
// event.detail.mentions
|
|
461
|
-
// event.detail.targetsCurrentParticipant
|
|
462
|
-
// event.detail.targetedMentions
|
|
463
|
-
});
|
|
464
|
-
```
|
|
465
|
-
|
|
466
|
-
#### Invitation accepted event
|
|
225
|
+
## Moderation API
|
|
467
226
|
|
|
468
227
|
```ruby
|
|
469
|
-
TurboChat.
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
```js
|
|
478
|
-
document.addEventListener("chat-gem:invitation-accepted", function (event) {
|
|
479
|
-
// event.detail.chatId
|
|
480
|
-
// event.detail.chatTitle
|
|
481
|
-
// event.detail.chatMembershipId
|
|
482
|
-
});
|
|
228
|
+
TurboChat::Moderation.mute_member!(actor: moderator, membership: membership)
|
|
229
|
+
TurboChat::Moderation.unmute_member!(actor: moderator, membership: membership)
|
|
230
|
+
TurboChat::Moderation.timeout_member!(actor: moderator, membership: membership, until_time: 30.minutes.from_now)
|
|
231
|
+
TurboChat::Moderation.clear_timeout!(actor: moderator, membership: membership)
|
|
232
|
+
TurboChat::Moderation.ban_member!(actor: moderator, membership: membership)
|
|
233
|
+
TurboChat::Moderation.delete_message!(actor: moderator, message: message)
|
|
234
|
+
TurboChat::Moderation.close_chat!(actor: admin, chat: chat)
|
|
235
|
+
TurboChat::Moderation.reopen_chat!(actor: admin, chat: chat)
|
|
483
236
|
```
|
|
484
237
|
|
|
485
|
-
|
|
238
|
+
Raises:
|
|
239
|
+
- `TurboChat::Moderation::AuthorizationError`
|
|
240
|
+
- `TurboChat::Moderation::InvalidActionError`
|
|
486
241
|
|
|
487
|
-
|
|
488
|
-
TurboChat.configure do |config|
|
|
489
|
-
config.emit_chat_lifecycle_events = true
|
|
490
|
-
end
|
|
491
|
-
```
|
|
242
|
+
## Browser Events
|
|
492
243
|
|
|
493
|
-
|
|
244
|
+
Enable with config flags.
|
|
494
245
|
|
|
495
|
-
|
|
496
|
-
- `chat
|
|
497
|
-
- `chat
|
|
498
|
-
- `chat
|
|
499
|
-
- `chat
|
|
500
|
-
- `chat
|
|
246
|
+
Event names:
|
|
247
|
+
- `turbo-chat:typing-started`
|
|
248
|
+
- `turbo-chat:typing-ended`
|
|
249
|
+
- `turbo-chat:message-sent`
|
|
250
|
+
- `turbo-chat:mention`
|
|
251
|
+
- `turbo-chat:invitation-accepted`
|
|
252
|
+
- `turbo-chat:chat-invited`
|
|
253
|
+
- `turbo-chat:chat-joined`
|
|
254
|
+
- `turbo-chat:chat-declined`
|
|
255
|
+
- `turbo-chat:chat-left`
|
|
256
|
+
- `turbo-chat:chat-closed`
|
|
257
|
+
- `turbo-chat:chat-reopened`
|
|
501
258
|
|
|
502
259
|
```js
|
|
503
|
-
document.addEventListener("chat
|
|
504
|
-
|
|
505
|
-
// event.detail.chatId
|
|
506
|
-
// event.detail.chatTitle
|
|
507
|
-
// event.detail.chatMembershipId
|
|
260
|
+
document.addEventListener("turbo-chat:message-sent", function (event) {
|
|
261
|
+
console.log(event.detail.chatId);
|
|
508
262
|
});
|
|
509
|
-
|
|
510
|
-
document.addEventListener("chat-gem:chat-joined", function (event) {
|
|
511
|
-
// event.detail.action => "joined"
|
|
512
|
-
// event.detail.chatId
|
|
513
|
-
// event.detail.chatTitle
|
|
514
|
-
// event.detail.chatMembershipId
|
|
515
|
-
});
|
|
516
|
-
|
|
517
|
-
document.addEventListener("chat-gem:chat-declined", function (event) {
|
|
518
|
-
// event.detail.action => "declined"
|
|
519
|
-
// event.detail.chatId
|
|
520
|
-
// event.detail.chatTitle
|
|
521
|
-
// event.detail.chatMembershipId
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
document.addEventListener("chat-gem:chat-left", function (event) {
|
|
525
|
-
// event.detail.action => "left"
|
|
526
|
-
// event.detail.chatId
|
|
527
|
-
// event.detail.chatTitle
|
|
528
|
-
// event.detail.chatMembershipId
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
document.addEventListener("chat-gem:chat-closed", function (event) {
|
|
532
|
-
// event.detail.action => "closed"
|
|
533
|
-
// event.detail.chatId
|
|
534
|
-
// event.detail.chatTitle
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
document.addEventListener("chat-gem:chat-reopened", function (event) {
|
|
538
|
-
// event.detail.action => "reopened"
|
|
539
|
-
// event.detail.chatId
|
|
540
|
-
// event.detail.chatTitle
|
|
541
|
-
});
|
|
542
|
-
```
|
|
543
|
-
|
|
544
|
-
#### Moderation notifications (server-side)
|
|
545
|
-
|
|
546
|
-
```ruby
|
|
547
|
-
TurboChat.configure do |config|
|
|
548
|
-
config.emit_moderation_events = true
|
|
549
|
-
end
|
|
550
|
-
```
|
|
551
|
-
|
|
552
|
-
When enabled, TurboChat instruments `ActiveSupport::Notifications` events:
|
|
553
|
-
|
|
554
|
-
- `chat_gem.moderation.member_muted`
|
|
555
|
-
- `chat_gem.moderation.member_unmuted`
|
|
556
|
-
- `chat_gem.moderation.member_timed_out`
|
|
557
|
-
- `chat_gem.moderation.member_timeout_cleared`
|
|
558
|
-
- `chat_gem.moderation.member_banned`
|
|
559
|
-
- `chat_gem.moderation.message_deleted`
|
|
560
|
-
- `chat_gem.moderation.chat_closed`
|
|
561
|
-
- `chat_gem.moderation.chat_reopened`
|
|
562
|
-
|
|
563
|
-
```ruby
|
|
564
|
-
ActiveSupport::Notifications.subscribe("chat_gem.moderation.member_banned") do |_name, _start, _finish, _id, payload|
|
|
565
|
-
# payload includes chat_id, membership_id, participant_type, participant_id, actor_type, actor_id
|
|
566
|
-
end
|
|
567
263
|
```
|
|
568
264
|
|
|
569
|
-
|
|
265
|
+
Client namespace: `window.TurboChatUI`
|
|
570
266
|
|
|
571
|
-
|
|
572
|
-
TurboChat.configure do |config|
|
|
573
|
-
config.emit_blocked_words_events = true
|
|
574
|
-
end
|
|
575
|
-
```
|
|
267
|
+
## ActiveSupport::Notifications
|
|
576
268
|
|
|
577
|
-
|
|
269
|
+
Moderation (`emit_moderation_events = true`):
|
|
270
|
+
- `turbo_chat.moderation.member_muted`
|
|
271
|
+
- `turbo_chat.moderation.member_unmuted`
|
|
272
|
+
- `turbo_chat.moderation.member_timed_out`
|
|
273
|
+
- `turbo_chat.moderation.member_timeout_cleared`
|
|
274
|
+
- `turbo_chat.moderation.member_banned`
|
|
275
|
+
- `turbo_chat.moderation.message_deleted`
|
|
276
|
+
- `turbo_chat.moderation.chat_closed`
|
|
277
|
+
- `turbo_chat.moderation.chat_reopened`
|
|
578
278
|
|
|
579
|
-
|
|
580
|
-
- `
|
|
581
|
-
- `
|
|
582
|
-
|
|
583
|
-
```ruby
|
|
584
|
-
ActiveSupport::Notifications.subscribe("chat_gem.blocked_words.detected") do |_name, _start, _finish, _id, payload|
|
|
585
|
-
# payload includes chat_id, message_id, participant_type, participant_id, blocked_words, action
|
|
586
|
-
end
|
|
587
|
-
```
|
|
588
|
-
|
|
589
|
-
## Participants, Roles, and Moderation
|
|
590
|
-
|
|
591
|
-
Use `TurboChat::ChatMembership` to add participants to a chat.
|
|
592
|
-
Any model using `acts_as_chat_participant` works (users, bots, service accounts).
|
|
593
|
-
|
|
594
|
-
```ruby
|
|
595
|
-
chat = TurboChat::Chat.find(chat_id)
|
|
596
|
-
participant = User.find(user_id)
|
|
279
|
+
Blocked words (`emit_blocked_words_events = true`):
|
|
280
|
+
- `turbo_chat.blocked_words.detected`
|
|
281
|
+
- `turbo_chat.blocked_words.rejected`
|
|
282
|
+
- `turbo_chat.blocked_words.scrambled`
|
|
597
283
|
|
|
598
|
-
|
|
599
|
-
membership.role = :member
|
|
600
|
-
end
|
|
601
|
-
```
|
|
602
|
-
|
|
603
|
-
Invitations are pending until accepted by the invited participant.
|
|
604
|
-
`POST /chats/:id/chat_memberships` creates or reopens a pending invite (`invitation_accepted: false`).
|
|
605
|
-
Pending invites are listed on the chats index for the invited participant, where they can accept (`PATCH /chats/:id/accept`) or decline (`PATCH /chats/:id/decline`).
|
|
606
|
-
|
|
607
|
-
Configure participant limits:
|
|
608
|
-
|
|
609
|
-
```ruby
|
|
610
|
-
TurboChat.configure do |config|
|
|
611
|
-
config.max_chat_participants = 10
|
|
612
|
-
# Set to nil or 0 to disable the limit.
|
|
613
|
-
end
|
|
614
|
-
```
|
|
615
|
-
|
|
616
|
-
Built-in roles: `:member`, `:moderator`, `:admin`.
|
|
617
|
-
|
|
618
|
-
- `member`: view chat, post messages, mention members.
|
|
619
|
-
- `moderator`: member abilities plus invites, `@all`, `@ROLE`, mute/timeout/ban members, and delete member messages.
|
|
620
|
-
- `admin`: moderator abilities plus moderating moderators and closing/reopening chats.
|
|
621
|
-
|
|
622
|
-
Custom role registration:
|
|
623
|
-
|
|
624
|
-
```ruby
|
|
625
|
-
TurboChat.configure do |config|
|
|
626
|
-
config.add_role(
|
|
627
|
-
:support_agent,
|
|
628
|
-
name: "Support Agent",
|
|
629
|
-
rank: 1,
|
|
630
|
-
permissions: %i[view_chat post_message delete_message]
|
|
631
|
-
)
|
|
632
|
-
end
|
|
633
|
-
```
|
|
634
|
-
|
|
635
|
-
Assign a custom role:
|
|
636
|
-
|
|
637
|
-
```ruby
|
|
638
|
-
membership = TurboChat::ChatMembership.find_or_create_by!(chat: chat, participant: participant)
|
|
639
|
-
membership.role_key = :support_agent
|
|
640
|
-
membership.save!
|
|
641
|
-
```
|
|
642
|
-
|
|
643
|
-
Available permissions:
|
|
644
|
-
`:view_chat`, `:post_message`, `:mention_member`, `:mention_all`, `:mention_role`, `:invite_member`, `:mute_member`, `:timeout_member`, `:ban_member`, `:delete_message`, `:close_chat`, `:reopen_chat`
|
|
645
|
-
|
|
646
|
-
Higher `rank` can moderate lower `rank` (never self).
|
|
647
|
-
|
|
648
|
-
If a participant was removed (`removed_at` set), reactivate that membership:
|
|
649
|
-
|
|
650
|
-
```ruby
|
|
651
|
-
membership = TurboChat::ChatMembership.find_by!(chat: chat, participant: participant)
|
|
652
|
-
membership.update!(removed_at: nil, muted: false, timed_out_until: nil)
|
|
653
|
-
```
|
|
654
|
-
|
|
655
|
-
Use moderation service APIs for role-checked actions:
|
|
656
|
-
|
|
657
|
-
```ruby
|
|
658
|
-
chat = TurboChat::Chat.find(chat_id)
|
|
659
|
-
moderator = User.find(moderator_id)
|
|
660
|
-
member_membership = chat.chat_memberships.find_by!(participant_id: member_id, participant_type: "User")
|
|
661
|
-
|
|
662
|
-
TurboChat::Moderation.mute_member!(actor: moderator, membership: member_membership)
|
|
663
|
-
TurboChat::Moderation.timeout_member!(actor: moderator, membership: member_membership, until_time: 30.minutes.from_now)
|
|
664
|
-
TurboChat::Moderation.ban_member!(actor: moderator, membership: member_membership)
|
|
665
|
-
TurboChat::Moderation.delete_message!(actor: moderator, message: chat.chat_messages.find(message_id))
|
|
666
|
-
|
|
667
|
-
admin = User.find(admin_id)
|
|
668
|
-
TurboChat::Moderation.close_chat!(actor: admin, chat: chat)
|
|
669
|
-
TurboChat::Moderation.reopen_chat!(actor: admin, chat: chat)
|
|
670
|
-
```
|
|
671
|
-
|
|
672
|
-
## Programmatic Signals
|
|
673
|
-
|
|
674
|
-
Use signal helpers to show temporary participant states in normal chat flows (`typing`, `thinking`, `planning`).
|
|
675
|
-
|
|
676
|
-
### Start and clear a signal
|
|
677
|
-
|
|
678
|
-
```ruby
|
|
679
|
-
chat = TurboChat::Chat.find(chat_id)
|
|
680
|
-
participant = Current.user
|
|
284
|
+
## UI Customization
|
|
681
285
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
286
|
+
Do this in order:
|
|
287
|
+
1. Theme with config (`own_message_hex_color`, `role_message_hex_colors`, `mention_mark_hex_color`, formatters).
|
|
288
|
+
2. Add class-level customization via `message_css_class_resolver`.
|
|
289
|
+
3. Only then override markup partials.
|
|
686
290
|
|
|
687
|
-
|
|
291
|
+
Primary partial override point:
|
|
292
|
+
- `app/views/turbo_chat/chat_messages/_message.html.erb`
|
|
688
293
|
|
|
689
|
-
|
|
690
|
-
TurboChat::Signals.start!(chat: chat, participant: participant, signal_type: :thinking)
|
|
691
|
-
TurboChat::Signals.replace!(chat: chat, participant: participant, signal_type: :planning)
|
|
692
|
-
```
|
|
294
|
+
Keep `id="<%= dom_id(chat_message) %>"` on the message wrapper or Turbo replacements break.
|
|
693
295
|
|
|
694
|
-
|
|
296
|
+
## Internal Names
|
|
695
297
|
|
|
696
|
-
|
|
697
|
-
final_text = TurboChat::Signals.with(chat: chat, participant: participant, signal_type: :thinking) do
|
|
698
|
-
params[:body].to_s.strip
|
|
699
|
-
end
|
|
298
|
+
Use `TurboChat` in app code.
|
|
700
299
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
)
|
|
707
|
-
```
|
|
300
|
+
These are implementation identifiers used by the engine:
|
|
301
|
+
- Engine namespace in source: `TurboChat`
|
|
302
|
+
- Database table prefix: `turbo_chat_`
|
|
303
|
+
- Browser event prefix: `turbo-chat:*`
|
|
304
|
+
- `ActiveSupport::Notifications` prefix: `turbo_chat.*`
|
|
708
305
|
|
|
709
|
-
|
|
306
|
+
Mount with `as: "turbo_chat"` and use `turbo_chat.*` route helpers.
|
|
710
307
|
|
|
711
|
-
|
|
712
|
-
For an additional server-side safeguard, enable submit-time cleanup:
|
|
308
|
+
## Upgrade
|
|
713
309
|
|
|
714
|
-
```
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
end
|
|
310
|
+
```bash
|
|
311
|
+
bin/rails turbo_chat:install:migrations
|
|
312
|
+
bin/rails db:migrate
|
|
718
313
|
```
|
|
719
314
|
|
|
720
|
-
With this enabled, existing signals for that participant are cleared before the message record is created.
|
|
721
|
-
|
|
722
315
|
## Dependencies
|
|
723
316
|
|
|
724
|
-
Runtime dependencies:
|
|
725
|
-
|
|
726
317
|
- Ruby `>= 3.1`
|
|
727
318
|
- Rails `>= 7.0`, `< 8.0`
|
|
728
319
|
- `turbo-rails` `>= 1.4`, `< 3.0`
|
|
320
|
+
- PostgreSQL or SQLite
|
|
729
321
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
- PostgreSQL or SQLite (required for the partial unique index used by chat memberships).
|
|
733
|
-
|
|
734
|
-
Development dependencies in this repository:
|
|
322
|
+
## Development
|
|
735
323
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
## Maintainer
|
|
740
|
-
|
|
741
|
-
[haumer](https://github.com/haumer)
|
|
324
|
+
```bash
|
|
325
|
+
bundle exec rake test
|
|
326
|
+
```
|