turbo_chat 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -1
  3. data/README.md +178 -190
  4. data/app/assets/config/turbo_chat_manifest.js +3 -0
  5. data/app/assets/javascripts/turbo_chat/application.js +3 -0
  6. data/app/assets/javascripts/turbo_chat/invite_picker.js +19 -392
  7. data/app/assets/javascripts/turbo_chat/member_sync.js +426 -0
  8. data/app/assets/javascripts/turbo_chat/mentions.js +366 -0
  9. data/app/assets/javascripts/turbo_chat/messages.js +18 -370
  10. data/app/assets/javascripts/turbo_chat/realtime.js +3 -10
  11. data/app/assets/javascripts/turbo_chat/scroll_proxy.js +379 -0
  12. data/app/assets/javascripts/turbo_chat/shared.js +7 -383
  13. data/app/assets/stylesheets/turbo_chat/application.css +9 -1646
  14. data/app/assets/stylesheets/turbo_chat/base.css +84 -0
  15. data/app/assets/stylesheets/turbo_chat/components.css +193 -0
  16. data/app/assets/stylesheets/turbo_chat/composer.css +241 -0
  17. data/app/assets/stylesheets/turbo_chat/layout.css +307 -0
  18. data/app/assets/stylesheets/turbo_chat/members.css +264 -0
  19. data/app/assets/stylesheets/turbo_chat/menus.css +172 -0
  20. data/app/assets/stylesheets/turbo_chat/messages.css +430 -0
  21. data/app/controllers/turbo_chat/application_controller.rb +3 -7
  22. data/app/controllers/turbo_chat/chat_memberships_controller.rb +35 -1
  23. data/app/controllers/turbo_chat/chat_messages_controller.rb +4 -8
  24. data/app/controllers/turbo_chat/chats_controller.rb +10 -12
  25. data/app/helpers/turbo_chat/application_helper/config_support.rb +42 -32
  26. data/app/helpers/turbo_chat/application_helper/mention_support.rb +3 -3
  27. data/app/helpers/turbo_chat/application_helper/message_rendering.rb +24 -13
  28. data/app/models/turbo_chat/chat.rb +43 -20
  29. data/app/models/turbo_chat/chat_membership.rb +1 -1
  30. data/app/models/turbo_chat/chat_message/blocked_words_moderation.rb +9 -25
  31. data/app/models/turbo_chat/chat_message/body_length_validation.rb +1 -1
  32. data/app/models/turbo_chat/chat_message/broadcasting.rb +2 -6
  33. data/app/models/turbo_chat/chat_message/formatting.rb +3 -7
  34. data/app/models/turbo_chat/chat_message/mention_validation.rb +1 -1
  35. data/app/models/turbo_chat/chat_message/signals.rb +1 -1
  36. data/app/models/turbo_chat/chat_message.rb +3 -8
  37. data/app/views/turbo_chat/chat_messages/_form.html.erb +9 -9
  38. data/app/views/turbo_chat/chat_messages/_message.html.erb +2 -2
  39. data/app/views/turbo_chat/chat_messages/_signals.html.erb +11 -13
  40. data/app/views/turbo_chat/chat_messages/_system.html.erb +1 -1
  41. data/app/views/turbo_chat/chats/_invite_form.html.erb +1 -1
  42. data/app/views/turbo_chat/chats/_member_entries.html.erb +15 -1
  43. data/app/views/turbo_chat/chats/index.html.erb +1 -1
  44. data/app/views/turbo_chat/chats/new.html.erb +4 -7
  45. data/app/views/turbo_chat/chats/show.html.erb +29 -27
  46. data/config/routes.rb +6 -1
  47. data/db/migrate/20260325000016_add_chat_mode_to_turbo_chat_chats.rb +6 -0
  48. data/lib/generators/turbo_chat/install/templates/turbo_chat.rb +8 -0
  49. data/lib/turbo_chat/configuration/defaults.rb +21 -0
  50. data/lib/turbo_chat/configuration.rb +105 -0
  51. data/lib/turbo_chat/moderation/chat_actions.rb +2 -2
  52. data/lib/turbo_chat/moderation/member_actions.rb +2 -1
  53. data/lib/turbo_chat/moderation/support.rb +5 -9
  54. data/lib/turbo_chat/permission/support.rb +6 -2
  55. data/lib/turbo_chat/permission.rb +1 -5
  56. data/lib/turbo_chat/signals.rb +1 -1
  57. data/lib/turbo_chat/version.rb +1 -1
  58. metadata +13 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c3fc14508fac1357a1da08a00d2cd3b905271a1fd5a18dc0ccc71e8f6a8572c7
4
- data.tar.gz: dc7b1fb742ebe03541e5ab140c312d1b2ed6f27efeb0cfbd3adca266bd9f7b28
3
+ metadata.gz: 2a123f536b09e56d7dbbe582048c7d5eceff7d043126c947647c567b6004b972
4
+ data.tar.gz: d568733e0d68d5e74c67bf0ab7f0a3ccc6433aa14797575311fe1d9250490835
5
5
  SHA512:
6
- metadata.gz: a452a6b3ba2b13ab03382150ca9f5780b13994781011a764f54a518939a095a46dec586319b9853f5e03bc4b7b0e70fc62af9749935540fcc5525534c2c910c6
7
- data.tar.gz: be545c2e1dc54a1df52fa83e8785ab36ea48c138ce40338c12ebd122e5311d771c861688b1347fe3f3b1f77c646c5f1fa7e1207fd482293c5ef89858a6b39cc4
6
+ metadata.gz: 1ef3331843b81f06f0f5e77988a03ee6d7039b6afecdf4a744259cf0c704d6e0ebf4cec5e9ad37a61e060fea9c3620f1962dc7e0129e987615d3ddc79b65a81d
7
+ data.tar.gz: 155857795d96fdea994d64ac88fdbf4021900c9066a0343dcd36648c823f7bc30f6798db94a2a3b1bd6b1e11f90e6203cf09641fd719cad19f358b95c978f478
data/CHANGELOG.md CHANGED
@@ -2,15 +2,38 @@
2
2
 
3
3
  All notable changes to `turbo_chat` will be documented in this file.
4
4
 
5
- ## [0.2.0] - 2026-03-02
5
+ ## [0.3.0] - 2026-03-25
6
+
7
+ ### Added
8
+ - `chat_mode` on chats with `standard` as the default and `assistant` as a built-in alternate mode.
9
+ - Mode-aware configuration overrides via `config.mode(:assistant)` so assistant chats can simplify UI and behavior without affecting standard chats.
10
+ - Built-in assistant-mode defaults for 1:1 assistant conversations, including a two-participant limit and simplified members/invite/mention/moderation defaults.
11
+ - README and installer guidance for chat modes and assistant-mode overrides.
12
+
13
+ ### Changed
14
+ - Chat UI, rendering helpers, validations, moderation events, and message/signal settings now resolve configuration against the current chat when mode-specific overrides are present.
15
+
16
+ ### Fixed
17
+ - Inviting an already-active participant or a participant with a pending invitation no longer reuses the membership in a way that can silently demote it back to a pending state.
18
+
19
+ ## [0.2.0] - 2026-03-03
6
20
 
7
21
  ### Added
8
22
  - Membership lookup cache (`Chat#membership_lookup`) for efficient per-message role resolution, eliminating N+1 queries when rendering participant roles in message lists.
9
23
  - Memoized `actor_membership` in Permission to avoid redundant database queries within a single request.
24
+ - Moderation UI: mute/unmute and remove buttons in the members panel with permission-gated visibility (`can_mute_member?`, `can_ban_member?`).
25
+ - `mute` and `ban` member routes and controller actions backed by `TurboChat::Moderation`.
26
+ - Narrower `chat-shell--narrow` variant for the new-chat page.
10
27
 
11
28
  ### Changed
12
29
  - Narrowed broad `rescue StandardError` clauses to specific exception types (`NoMethodError`, `TypeError`) across controllers, helpers, models, and permission modules for better error visibility during development.
13
30
  - Removed duplicated `containerBottomPadding` function from `realtime.js`; now imports the shared version from `shared.js`.
31
+ - New-chat page uses compact inline grid form instead of full-width field and button.
32
+ - Members invite dropdown no longer clipped by panel overflow when expanded.
33
+ - Removed redundant "Conversation active/closed" kicker from chat header.
34
+ - Invite submit button disabled until a participant is selected from the dropdown.
35
+ - Gear button hidden (instead of disabled) for own member entry in the members panel.
36
+ - Manage-member gear toggle now visible for moderators with mute/ban permissions, not only admins with grant permissions.
14
37
 
15
38
  ## [0.1.15] - 2026-02-28
16
39
 
data/README.md CHANGED
@@ -6,10 +6,12 @@ Status: actively maintained.
6
6
 
7
7
  ## What You Get
8
8
 
9
- - Turbo Stream chat UI.
10
- - Role-based permissions.
11
- - Mentions, invitations, moderation, and typing signals.
12
- - A Rails-first path to ship chat quickly.
9
+ - Turbo Stream chat UI with two layout styles (bounded card or full-viewport).
10
+ - Role-based permissions with a swappable adapter.
11
+ - Mentions, invitations, inline message editing, and typing signals.
12
+ - Moderation: mute, timeout, ban, and message deletion.
13
+ - System messages for membership lifecycle events.
14
+ - Browser and server-side event hooks.
13
15
 
14
16
  ## Basic Setup
15
17
 
@@ -59,20 +61,11 @@ class User < ApplicationRecord
59
61
  end
60
62
  ```
61
63
 
62
- ### 2. Resolve the current participant
63
-
64
- You can define `current_chat_participant` in your host `ApplicationController`.
65
- If you already expose `current_user` and it returns a model using `acts_as_chat_participant`, TurboChat works without adding this method.
64
+ This adds `has_many :chat_memberships` and the associations TurboChat needs to resolve participants in chats.
66
65
 
67
- Recommended hook:
66
+ ### 2. Resolve the current participant
68
67
 
69
- ```ruby
70
- class ApplicationController < ActionController::Base
71
- def current_chat_participant
72
- current_user
73
- end
74
- end
75
- ```
68
+ If your app exposes `current_user` and it returns a model using `acts_as_chat_participant`, TurboChat works out of the box.
76
69
 
77
70
  Resolution order:
78
71
 
@@ -81,7 +74,7 @@ Resolution order:
81
74
  3. `current_user` (if available)
82
75
  4. Raise `NotImplementedError`
83
76
 
84
- Optional resolver for non-`current_user` auth:
77
+ For non-`current_user` auth:
85
78
 
86
79
  ```ruby
87
80
  TurboChat.configure do |config|
@@ -91,186 +84,177 @@ end
91
84
 
92
85
  ## First Working Example
93
86
 
94
- Create a chat and add an admin membership:
95
-
96
87
  ```ruby
97
88
  chat = TurboChat::Chat.create!(title: "Support")
98
89
  TurboChat::ChatMembership.create!(chat: chat, participant: current_user, role: :admin)
99
90
  ```
100
91
 
101
- Link to it:
102
-
103
92
  ```erb
104
93
  <%= link_to "Open chat", turbo_chat.chat_path(chat) %>
105
94
  ```
106
95
 
107
- ## Essential Configuration
96
+ ## Configuration
97
+
98
+ The installer generates a full initializer at `config/initializers/turbo_chat.rb` with every option documented. Start with the defaults and change only what you need.
108
99
 
109
- Start with a minimal initializer and only expand when needed:
100
+ Config is organized into scoped namespaces:
110
101
 
111
102
  ```ruby
112
103
  TurboChat.configure do |config|
113
- config.chat.permission_adapter = TurboChat::Permission
114
-
104
+ # Limits
115
105
  config.chat.max_chat_participants = 10
116
106
  config.chat_message.max_message_length = 1000
117
- config.chat_message.message_history_limit = 200
118
107
 
108
+ # Features
119
109
  config.chat_message.enable_mentions = true
120
- config.chat_message.enable_emoji_aliases = true
121
-
122
110
  config.chat_message.blocked_words = []
123
- config.chat_message.blocked_words_action = :reject # or :scramble
111
+ config.chat_message.blocked_words_action = :reject # or :scramble
124
112
 
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"
113
+ # Layout
114
+ config.style.chat_style = "chat_style_bounded" # or "chat_style_unbounded"
130
115
  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
116
+
117
+ # Events (all opt-in, all false by default)
118
+ config.events.emit_typing_events = true
119
+ config.events.emit_message_events = true
147
120
  end
148
121
  ```
149
122
 
150
- Flat aliases (for example `config.max_message_length`) still work for backward compatibility.
123
+ Available scopes: `config.chat`, `config.chat_message`, `config.style`, `config.events`, `config.moderation`, `config.signals`. Flat aliases (e.g. `config.max_message_length`) still work for backward compatibility.
151
124
 
152
- ### Rate Limiting
153
-
154
- TurboChat does not include built-in rate limiting. For production deployments, add request throttling for message creation in your host application using middleware such as [rack-attack](https://github.com/rack/rack-attack).
155
-
156
- ## Message Ingest API
125
+ ### Chat Modes
157
126
 
158
- Post messages as a specific participant, including external sources like WhatsApp:
127
+ Chats default to `chat_mode: :standard`.
159
128
 
160
129
  ```ruby
161
- TurboChat::Messages.send_message_as(
162
- current_user,
163
- chat,
164
- body: "Internal note",
165
- source: :app
166
- )
130
+ chat = TurboChat::Chat.create!(title: "Assistant", chat_mode: :assistant)
167
131
  ```
168
132
 
169
- External ingest with idempotency (`chat_id + source + external_id`):
133
+ Built-in assistant-mode defaults make the chat simpler for 1:1 assistant use cases:
170
134
 
171
- ```ruby
172
- TurboChat::Messages.ingest_external!(
173
- chat: chat,
174
- participant: current_user,
175
- body: "Hello from WhatsApp",
176
- source: :whatsapp,
177
- external_id: webhook_payload.fetch("message_id"),
178
- sent_at: webhook_payload["sent_at"]
179
- )
180
- ```
135
+ - `max_chat_participants = 2`
136
+ - members panel and invite UI hidden
137
+ - mentions disabled
138
+ - system messages disabled
139
+ - chat close action hidden
140
+ - invitation, mention, lifecycle, and moderation events disabled
181
141
 
182
- `external_id` is required for `ingest_external!` so duplicate webhook deliveries can resolve to the same stored message.
183
-
184
- Source labels shown in message badges are configurable:
142
+ You can override assistant-mode defaults without changing standard chats:
185
143
 
186
144
  ```ruby
187
145
  TurboChat.configure do |config|
188
- config.chat_message.message_source_labels = {
189
- "app" => "In App",
190
- "whatsapp" => "WhatsApp",
191
- "sms_gateway" => "SMS"
192
- }
146
+ config.mode(:assistant).show_members = true
147
+ config.mode(:assistant).enable_mentions = true
193
148
  end
194
149
  ```
195
150
 
196
- Chat UI layout style is configurable:
151
+ ### Rate Limiting
197
152
 
198
- ```ruby
199
- TurboChat.configure do |config|
200
- config.style.chat_style = "chat_style_unbounded" # or "chat_style_bounded"
201
- end
202
- ```
153
+ TurboChat does not include built-in rate limiting. For production, add request throttling using middleware such as [rack-attack](https://github.com/rack/rack-attack).
203
154
 
204
- Timeline insert position is configurable:
155
+ ## Roles and Permissions
205
156
 
206
- ```ruby
207
- TurboChat.configure do |config|
208
- config.chat_message.message_insert_position = "append_end" # or "append_start"
209
- end
210
- ```
157
+ ### Built-in roles
158
+
159
+ | Role | Rank | Key capabilities |
160
+ |------|------|-----------------|
161
+ | `member` | 0 | View, post, mention members, edit own messages |
162
+ | `moderator` | 1 | + invite, `@all`/`@ROLE` mentions, mute/timeout/ban, delete messages |
163
+ | `admin` | 2 | + close/reopen chats, change member roles |
211
164
 
212
- Composer input can be disabled globally:
165
+ Higher-rank roles can act on lower-rank members but not on peers or above.
166
+
167
+ ### Custom roles
213
168
 
214
169
  ```ruby
215
170
  TurboChat.configure do |config|
216
- config.chat.disable_input = true
171
+ config.add_role(
172
+ :support_agent,
173
+ name: "Support Agent",
174
+ rank: 1,
175
+ permissions: %i[view_chat post_message delete_message]
176
+ )
217
177
  end
218
178
  ```
219
179
 
220
- Chat header elements can also be disabled globally:
180
+ ### Permission adapter
181
+
182
+ All access checks go through `config.chat.permission_adapter` (default: `TurboChat::Permission`). To customize, create a class that implements the same public interface:
221
183
 
222
184
  ```ruby
185
+ class CustomPermission < TurboChat::Permission
186
+ def can_post_message?
187
+ super && !participant.suspended?
188
+ end
189
+ end
190
+
223
191
  TurboChat.configure do |config|
224
- config.chat.show_header_title = false
225
- config.chat.show_header_status = false
226
- config.chat.show_header_close_action = false
227
- config.chat.show_header_leave_action = false
228
- config.chat.show_header_back_action = false
192
+ config.chat.permission_adapter = CustomPermission
229
193
  end
230
194
  ```
231
195
 
232
- Members area visibility can be tuned independently:
196
+ The adapter must respond to: `can_view_chat?`, `can_create_chat?`, `can_post_message?`, `can_invite_member?`, `can_grant_member_permissions?`, `can_mute_member?`, `can_timeout_member?`, `can_ban_member?`, `can_delete_message?`, `can_edit_message?`, `can_close_chat?`, `can_reopen_chat?`.
197
+
198
+ ## Invitations
199
+
200
+ Admins and moderators can invite participants through the members panel. The invited participant sees pending invitations on the chat index with Accept/Decline buttons.
201
+
202
+ Programmatic invitation:
233
203
 
234
204
  ```ruby
235
- TurboChat.configure do |config|
236
- config.chat.show_members = true
237
- config.chat.show_members_list = true
238
- config.chat.show_members_invite_controls = true
239
- config.chat.show_invite_fallback_when_members_hidden = true
240
- end
205
+ TurboChat::ChatMembership.create!(
206
+ chat: chat,
207
+ participant: invitee,
208
+ role: :member,
209
+ invitation_accepted: false
210
+ )
241
211
  ```
242
212
 
243
- ## Roles
213
+ Accept/decline routes are built in: `PATCH /chats/:id/accept` and `PATCH /chats/:id/decline`.
244
214
 
245
- Built-in roles:
215
+ ## Message Ingest API
216
+
217
+ Post messages as a specific participant:
246
218
 
247
- - `member`
248
- - `moderator`
249
- - `admin`
219
+ ```ruby
220
+ TurboChat::Messages.send_message_as(
221
+ current_user,
222
+ chat,
223
+ body: "Internal note",
224
+ source: :app
225
+ )
226
+ ```
250
227
 
251
- Role behavior:
228
+ External ingest with idempotency (`chat_id + source + external_id`):
252
229
 
253
- - `member`: can view/post and mention members.
254
- - `moderator`: can invite, mention `@all`/roles, mute/timeout/ban, and delete lower-rank messages.
255
- - `admin`: can do moderator actions plus close/reopen chats.
230
+ ```ruby
231
+ TurboChat::Messages.ingest_external!(
232
+ chat: chat,
233
+ participant: current_user,
234
+ body: "Hello from WhatsApp",
235
+ source: :whatsapp,
236
+ external_id: webhook_payload.fetch("message_id"),
237
+ sent_at: webhook_payload["sent_at"]
238
+ )
239
+ ```
256
240
 
257
- Custom role example:
241
+ Duplicate webhook deliveries with the same `external_id` resolve to the existing message.
242
+
243
+ Source labels shown in message badges are configurable:
258
244
 
259
245
  ```ruby
260
- TurboChat.configure do |config|
261
- config.add_role(
262
- :support_agent,
263
- name: "Support Agent",
264
- rank: 1,
265
- permissions: %i[view_chat post_message delete_message]
266
- )
267
- end
246
+ config.chat_message.message_source_labels = {
247
+ "app" => "In App",
248
+ "whatsapp" => "WhatsApp",
249
+ "sms_gateway" => "SMS"
250
+ }
268
251
  ```
269
252
 
270
253
  ## Moderation API
271
254
 
272
255
  ```ruby
273
256
  TurboChat::Moderation.mute_member!(actor: moderator, membership: membership)
257
+ TurboChat::Moderation.unmute_member!(actor: moderator, membership: membership)
274
258
  TurboChat::Moderation.timeout_member!(actor: moderator, membership: membership, until_time: 30.minutes.from_now)
275
259
  TurboChat::Moderation.ban_member!(actor: moderator, membership: membership)
276
260
  TurboChat::Moderation.delete_message!(actor: moderator, message: message)
@@ -278,93 +262,97 @@ TurboChat::Moderation.close_chat!(actor: admin, chat: chat)
278
262
  TurboChat::Moderation.reopen_chat!(actor: admin, chat: chat)
279
263
  ```
280
264
 
281
- Raises:
265
+ All actions are permission-checked and generate system messages in the chat timeline. Mute, timeout, ban, and role changes are visible to all participants as system messages (disable with `config.chat.system_messages = false`).
282
266
 
283
- - `TurboChat::Moderation::AuthorizationError`
284
- - `TurboChat::Moderation::InvalidActionError`
267
+ Raises `TurboChat::Moderation::AuthorizationError` or `TurboChat::Moderation::InvalidActionError`.
285
268
 
286
269
  ## Signals API
287
270
 
288
271
  ```ruby
289
- TurboChat::Signals.start!(chat: chat, participant: current_user, signal_type: :typing)
290
- TurboChat::Signals.start!(chat: chat, participant: current_user, signal_type: :custom, signal_text: "Hello")
291
- TurboChat::Signals.custom!(chat: chat, participant: current_user, signal_text: "Reviewing your request")
292
- TurboChat::Signals.clear!(chat: chat, participant: current_user)
272
+ TurboChat::Signals.start!(chat: chat, participant: user, signal_type: :typing)
273
+ TurboChat::Signals.start!(chat: chat, participant: user, signal_type: :thinking)
274
+ TurboChat::Signals.start!(chat: chat, participant: user, signal_type: :planning)
275
+ TurboChat::Signals.custom!(chat: chat, participant: user, signal_text: "Reviewing your request")
276
+ TurboChat::Signals.clear!(chat: chat, participant: user)
293
277
  ```
294
278
 
295
- ## Event Emissions
296
-
297
- All event emissions are opt-in.
298
-
299
- Enable only what you consume:
279
+ Block form that auto-clears when the work finishes:
300
280
 
301
281
  ```ruby
302
- TurboChat.configure do |config|
303
- config.events.emit_typing_events = true
304
- config.events.emit_message_events = true
305
- config.events.emit_mention_events = true
306
- config.events.emit_invitation_events = true
307
- config.events.emit_chat_lifecycle_events = true
308
- config.moderation.emit_moderation_events = true
309
- config.moderation.emit_blocked_words_events = true
282
+ TurboChat::Signals.with(chat: chat, participant: user, signal_type: :thinking) do
283
+ # long-running operation
310
284
  end
311
285
  ```
312
286
 
313
- Browser events (`CustomEvent`):
287
+ Signal lifetime is configurable via `config.signals.signal_ttl_seconds` (default: 60).
314
288
 
315
- - `emit_typing_events`: `turbo-chat:typing-started`, `turbo-chat:typing-ended`
316
- - `emit_message_events`: `turbo-chat:message-sent`
317
- - `emit_mention_events`: `turbo-chat:mention`
318
- - `emit_invitation_events`: `turbo-chat:invitation-accepted`
319
- - `emit_chat_lifecycle_events`: `turbo-chat:chat-invited`, `turbo-chat:chat-joined`, `turbo-chat:chat-declined`, `turbo-chat:chat-left`, `turbo-chat:chat-closed`, `turbo-chat:chat-reopened`
289
+ ## Events
320
290
 
321
- Minimal browser listener:
291
+ All event emissions are opt-in.
292
+
293
+ ### Browser events (`CustomEvent` on `document`)
294
+
295
+ ```ruby
296
+ config.events.emit_typing_events = true # turbo-chat:typing-started, turbo-chat:typing-ended
297
+ config.events.emit_message_events = true # turbo-chat:message-sent
298
+ config.events.emit_mention_events = true # turbo-chat:mention
299
+ config.events.emit_invitation_events = true # turbo-chat:invitation-accepted
300
+ config.events.emit_chat_lifecycle_events = true # turbo-chat:chat-invited, chat-joined, chat-declined,
301
+ # chat-left, chat-closed, chat-reopened
302
+ ```
322
303
 
323
304
  ```js
324
- [
325
- "turbo-chat:typing-started",
326
- "turbo-chat:typing-ended",
327
- "turbo-chat:message-sent",
328
- "turbo-chat:mention",
329
- "turbo-chat:invitation-accepted",
330
- "turbo-chat:chat-invited",
331
- "turbo-chat:chat-joined",
332
- "turbo-chat:chat-declined",
333
- "turbo-chat:chat-left",
334
- "turbo-chat:chat-closed",
335
- "turbo-chat:chat-reopened"
336
- ].forEach(function (eventName) {
337
- document.addEventListener(eventName, function (event) {
338
- console.log(eventName, event.detail);
339
- });
305
+ document.addEventListener("turbo-chat:message-sent", function (event) {
306
+ console.log(event.detail); // { chatId: "123" }
340
307
  });
341
308
  ```
342
309
 
343
- Server-side notifications (`ActiveSupport::Notifications`):
344
-
345
- - `emit_moderation_events`:
346
- `turbo_chat.moderation.member_muted`,
347
- `turbo_chat.moderation.member_unmuted`,
348
- `turbo_chat.moderation.member_timed_out`,
349
- `turbo_chat.moderation.member_timeout_cleared`,
350
- `turbo_chat.moderation.member_banned`,
351
- `turbo_chat.moderation.message_deleted`,
352
- `turbo_chat.moderation.chat_closed`,
353
- `turbo_chat.moderation.chat_reopened`
354
- - `emit_blocked_words_events`:
355
- `turbo_chat.blocked_words.detected`,
356
- `turbo_chat.blocked_words.rejected`,
357
- `turbo_chat.blocked_words.scrambled`
310
+ ### Server-side events (`ActiveSupport::Notifications`)
358
311
 
359
- Minimal Rails listener:
312
+ ```ruby
313
+ config.moderation.emit_moderation_events = true # turbo_chat.moderation.member_muted, member_banned, ...
314
+ config.moderation.emit_blocked_words_events = true # turbo_chat.blocked_words.detected, rejected, scrambled
315
+ ```
360
316
 
361
317
  ```ruby
362
- ActiveSupport::Notifications.subscribe(/turbo_chat\.(moderation|blocked_words)\./) do |*args|
318
+ ActiveSupport::Notifications.subscribe(/turbo_chat\.moderation\./) do |*args|
363
319
  event = ActiveSupport::Notifications::Event.new(*args)
364
320
  Rails.logger.info("[TurboChat] #{event.name} #{event.payload.inspect}")
365
321
  end
366
322
  ```
367
323
 
324
+ ## Theming
325
+
326
+ TurboChat ships a default theme built on CSS custom properties. Override them in your app stylesheet:
327
+
328
+ ```css
329
+ :root {
330
+ --chat-text: #12263f;
331
+ --chat-muted: #5f738a;
332
+ --chat-border: #c9d5e3;
333
+ --chat-surface: #ffffff;
334
+ --chat-surface-soft: #f6f9fd;
335
+ --chat-primary: #1f6edc;
336
+ --chat-primary-dark: #1452ac;
337
+ --chat-primary-soft: #e7f1ff;
338
+ --chat-danger: #b42318;
339
+ --chat-danger-soft: #fff1f3;
340
+ --chat-success: #1f7a40;
341
+ --chat-success-soft: #ecfdf3;
342
+ --chat-mention-highlight-color: #b42318;
343
+ --chat-shadow: 0 20px 48px rgba(16, 36, 58, 0.12);
344
+ }
345
+ ```
346
+
347
+ Additional style config:
348
+
349
+ ```ruby
350
+ config.style.own_message_hex_color = "#e7f1ff"
351
+ config.style.other_message_hex_color = "#ffffff"
352
+ config.style.mention_mark_hex_color = "#b42318"
353
+ config.style.composer_placeholder_text = "Type a message..."
354
+ ```
355
+
368
356
  ## Upgrade
369
357
 
370
358
  ```bash
@@ -374,9 +362,9 @@ bin/rails db:migrate
374
362
 
375
363
  ## Known Limitations
376
364
 
377
- - **Stream subscriptions after member removal.** Turbo Stream subscriptions persist until the page is reloaded. A removed member may continue to see new messages until they navigate away. This is not a security issue because stream names are cryptographically signed, but it can be surprising.
378
- - **Blocked word filtering.** Word-boundary matching can be bypassed with Unicode homoglyphs, zero-width characters, or leetspeak substitutions. Treat it as a first line of defense, not a guarantee.
379
- - **Invitable participants cap.** The invite picker returns at most 100 non-member participants. Applications with larger user bases should provide a custom search endpoint.
365
+ - **Stream subscriptions after member removal.** Turbo Stream subscriptions persist until page reload. A removed member may see new messages until they navigate away. Stream names are cryptographically signed so this is not a security issue.
366
+ - **Blocked word filtering.** Word-boundary matching can be bypassed with Unicode homoglyphs, zero-width characters, or leetspeak. Treat it as a first line of defense.
367
+ - **Invitable participants cap.** The invite picker returns at most 100 non-member participants. Larger user bases should provide a custom search endpoint.
380
368
 
381
369
  ## Dependencies
382
370
 
@@ -1,7 +1,10 @@
1
1
  //= link turbo_chat/application.css
2
2
  //= link turbo_chat/application.js
3
3
  //= link turbo_chat/shared.js
4
+ //= link turbo_chat/mentions.js
5
+ //= link turbo_chat/scroll_proxy.js
4
6
  //= link turbo_chat/messages.js
5
7
  //= link turbo_chat/realtime.js
6
8
  //= link turbo_chat/invite_picker.js
9
+ //= link turbo_chat/member_sync.js
7
10
  //= link turbo_chat/lifecycle_events.js
@@ -1,5 +1,8 @@
1
1
  //= require turbo_chat/shared
2
+ //= require turbo_chat/scroll_proxy
3
+ //= require turbo_chat/mentions
2
4
  //= require turbo_chat/messages
3
5
  //= require turbo_chat/realtime
4
6
  //= require turbo_chat/invite_picker
7
+ //= require turbo_chat/member_sync
5
8
  //= require turbo_chat/lifecycle_events