turbo_chat 0.1.15 → 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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -0
  3. data/README.md +181 -183
  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 +4 -24
  11. data/app/assets/javascripts/turbo_chat/scroll_proxy.js +379 -0
  12. data/app/assets/javascripts/turbo_chat/shared.js +8 -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 +36 -2
  23. data/app/controllers/turbo_chat/chat_messages_controller.rb +4 -8
  24. data/app/controllers/turbo_chat/chats_controller/event_payload_support.rb +2 -2
  25. data/app/controllers/turbo_chat/chats_controller/invitation_support.rb +1 -1
  26. data/app/controllers/turbo_chat/chats_controller.rb +10 -12
  27. data/app/helpers/turbo_chat/application_helper/config_support.rb +42 -32
  28. data/app/helpers/turbo_chat/application_helper/mention_support/permission_support.rb +2 -2
  29. data/app/helpers/turbo_chat/application_helper/mention_support.rb +3 -3
  30. data/app/helpers/turbo_chat/application_helper/message_rendering.rb +33 -13
  31. data/app/helpers/turbo_chat/application_helper/participant_support.rb +2 -2
  32. data/app/models/turbo_chat/chat.rb +47 -20
  33. data/app/models/turbo_chat/chat_membership.rb +1 -1
  34. data/app/models/turbo_chat/chat_message/blocked_words_moderation.rb +9 -25
  35. data/app/models/turbo_chat/chat_message/body_length_validation.rb +1 -1
  36. data/app/models/turbo_chat/chat_message/broadcasting.rb +2 -6
  37. data/app/models/turbo_chat/chat_message/formatting.rb +3 -3
  38. data/app/models/turbo_chat/chat_message/mention_validation.rb +3 -3
  39. data/app/models/turbo_chat/chat_message/signals.rb +1 -1
  40. data/app/models/turbo_chat/chat_message.rb +3 -8
  41. data/app/views/turbo_chat/chat_messages/_form.html.erb +9 -9
  42. data/app/views/turbo_chat/chat_messages/_message.html.erb +2 -2
  43. data/app/views/turbo_chat/chat_messages/_signals.html.erb +11 -13
  44. data/app/views/turbo_chat/chat_messages/_system.html.erb +1 -1
  45. data/app/views/turbo_chat/chats/_invite_form.html.erb +1 -1
  46. data/app/views/turbo_chat/chats/_member_entries.html.erb +15 -1
  47. data/app/views/turbo_chat/chats/index.html.erb +1 -1
  48. data/app/views/turbo_chat/chats/new.html.erb +4 -7
  49. data/app/views/turbo_chat/chats/show.html.erb +29 -27
  50. data/config/routes.rb +6 -1
  51. data/db/migrate/20260302000015_add_kind_index_to_turbo_chat_chat_messages.rb +6 -0
  52. data/db/migrate/20260325000016_add_chat_mode_to_turbo_chat_chats.rb +6 -0
  53. data/lib/generators/turbo_chat/install/templates/turbo_chat.rb +8 -0
  54. data/lib/turbo_chat/configuration/defaults.rb +21 -0
  55. data/lib/turbo_chat/configuration.rb +105 -0
  56. data/lib/turbo_chat/messages.rb +1 -1
  57. data/lib/turbo_chat/moderation/chat_actions.rb +2 -2
  58. data/lib/turbo_chat/moderation/member_actions.rb +2 -1
  59. data/lib/turbo_chat/moderation/support.rb +5 -9
  60. data/lib/turbo_chat/permission/support.rb +9 -4
  61. data/lib/turbo_chat/permission.rb +1 -5
  62. data/lib/turbo_chat/signals.rb +1 -1
  63. data/lib/turbo_chat/version.rb +1 -1
  64. metadata +14 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 62e1eb06f88659efb5df1cc8a72f33e669b2a969dd734abc8a977aa4331fe925
4
- data.tar.gz: 5c40f2095d91a51a69cafd0086553ceab8ecf69894696bc8de2f86498170e52a
3
+ metadata.gz: 2a123f536b09e56d7dbbe582048c7d5eceff7d043126c947647c567b6004b972
4
+ data.tar.gz: d568733e0d68d5e74c67bf0ab7f0a3ccc6433aa14797575311fe1d9250490835
5
5
  SHA512:
6
- metadata.gz: a0ebcdf8245e4c726f65071d9da1bb673cbef17ae2327c0e3708e86097bedf90a51a9deaef21b9e28b19aef5552f0071b164a21e571a0e332834775dab54db78
7
- data.tar.gz: 7e9ede1f0c631f03642b8c9da45a5ec4a15b0880edc85ae3f6ea51b2aa4aaf59e25d91786b2ab3e9a30e4c4ea890c47deee143910978ebcb74793d20cfa203ed
6
+ metadata.gz: 1ef3331843b81f06f0f5e77988a03ee6d7039b6afecdf4a744259cf0c704d6e0ebf4cec5e9ad37a61e060fea9c3620f1962dc7e0129e987615d3ddc79b65a81d
7
+ data.tar.gz: 155857795d96fdea994d64ac88fdbf4021900c9066a0343dcd36648c823f7bc30f6798db94a2a3b1bd6b1e11f90e6203cf09641fd719cad19f358b95c978f478
data/CHANGELOG.md CHANGED
@@ -2,6 +2,39 @@
2
2
 
3
3
  All notable changes to `turbo_chat` will be documented in this file.
4
4
 
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
20
+
21
+ ### Added
22
+ - Membership lookup cache (`Chat#membership_lookup`) for efficient per-message role resolution, eliminating N+1 queries when rendering participant roles in message lists.
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.
27
+
28
+ ### Changed
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.
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.
37
+
5
38
  ## [0.1.15] - 2026-02-28
6
39
 
7
40
  ### Added
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,182 +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
- ## Message Ingest API
125
+ ### Chat Modes
153
126
 
154
- Post messages as a specific participant, including external sources like WhatsApp:
127
+ Chats default to `chat_mode: :standard`.
155
128
 
156
129
  ```ruby
157
- TurboChat::Messages.send_message_as(
158
- current_user,
159
- chat,
160
- body: "Internal note",
161
- source: :app
162
- )
130
+ chat = TurboChat::Chat.create!(title: "Assistant", chat_mode: :assistant)
163
131
  ```
164
132
 
165
- External ingest with idempotency (`chat_id + source + external_id`):
166
-
167
- ```ruby
168
- TurboChat::Messages.ingest_external!(
169
- chat: chat,
170
- participant: current_user,
171
- body: "Hello from WhatsApp",
172
- source: :whatsapp,
173
- external_id: webhook_payload.fetch("message_id"),
174
- sent_at: webhook_payload["sent_at"]
175
- )
176
- ```
133
+ Built-in assistant-mode defaults make the chat simpler for 1:1 assistant use cases:
177
134
 
178
- `external_id` is required for `ingest_external!` so duplicate webhook deliveries can resolve to the same stored message.
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
179
141
 
180
- Source labels shown in message badges are configurable:
142
+ You can override assistant-mode defaults without changing standard chats:
181
143
 
182
144
  ```ruby
183
145
  TurboChat.configure do |config|
184
- config.chat_message.message_source_labels = {
185
- "app" => "In App",
186
- "whatsapp" => "WhatsApp",
187
- "sms_gateway" => "SMS"
188
- }
146
+ config.mode(:assistant).show_members = true
147
+ config.mode(:assistant).enable_mentions = true
189
148
  end
190
149
  ```
191
150
 
192
- Chat UI layout style is configurable:
151
+ ### Rate Limiting
193
152
 
194
- ```ruby
195
- TurboChat.configure do |config|
196
- config.style.chat_style = "chat_style_unbounded" # or "chat_style_bounded"
197
- end
198
- ```
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).
199
154
 
200
- Timeline insert position is configurable:
155
+ ## Roles and Permissions
201
156
 
202
- ```ruby
203
- TurboChat.configure do |config|
204
- config.chat_message.message_insert_position = "append_end" # or "append_start"
205
- end
206
- ```
157
+ ### Built-in roles
207
158
 
208
- Composer input can be disabled globally:
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 |
164
+
165
+ Higher-rank roles can act on lower-rank members but not on peers or above.
166
+
167
+ ### Custom roles
209
168
 
210
169
  ```ruby
211
170
  TurboChat.configure do |config|
212
- 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
+ )
213
177
  end
214
178
  ```
215
179
 
216
- 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:
217
183
 
218
184
  ```ruby
185
+ class CustomPermission < TurboChat::Permission
186
+ def can_post_message?
187
+ super && !participant.suspended?
188
+ end
189
+ end
190
+
219
191
  TurboChat.configure do |config|
220
- config.chat.show_header_title = false
221
- config.chat.show_header_status = false
222
- config.chat.show_header_close_action = false
223
- config.chat.show_header_leave_action = false
224
- config.chat.show_header_back_action = false
192
+ config.chat.permission_adapter = CustomPermission
225
193
  end
226
194
  ```
227
195
 
228
- 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:
229
203
 
230
204
  ```ruby
231
- TurboChat.configure do |config|
232
- config.chat.show_members = true
233
- config.chat.show_members_list = true
234
- config.chat.show_members_invite_controls = true
235
- config.chat.show_invite_fallback_when_members_hidden = true
236
- end
205
+ TurboChat::ChatMembership.create!(
206
+ chat: chat,
207
+ participant: invitee,
208
+ role: :member,
209
+ invitation_accepted: false
210
+ )
237
211
  ```
238
212
 
239
- ## Roles
213
+ Accept/decline routes are built in: `PATCH /chats/:id/accept` and `PATCH /chats/:id/decline`.
240
214
 
241
- Built-in roles:
215
+ ## Message Ingest API
242
216
 
243
- - `member`
244
- - `moderator`
245
- - `admin`
217
+ Post messages as a specific participant:
246
218
 
247
- Role behavior:
219
+ ```ruby
220
+ TurboChat::Messages.send_message_as(
221
+ current_user,
222
+ chat,
223
+ body: "Internal note",
224
+ source: :app
225
+ )
226
+ ```
248
227
 
249
- - `member`: can view/post and mention members.
250
- - `moderator`: can invite, mention `@all`/roles, mute/timeout/ban, and delete lower-rank messages.
251
- - `admin`: can do moderator actions plus close/reopen chats.
228
+ External ingest with idempotency (`chat_id + source + external_id`):
252
229
 
253
- Custom role example:
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
+ ```
240
+
241
+ Duplicate webhook deliveries with the same `external_id` resolve to the existing message.
242
+
243
+ Source labels shown in message badges are configurable:
254
244
 
255
245
  ```ruby
256
- TurboChat.configure do |config|
257
- config.add_role(
258
- :support_agent,
259
- name: "Support Agent",
260
- rank: 1,
261
- permissions: %i[view_chat post_message delete_message]
262
- )
263
- end
246
+ config.chat_message.message_source_labels = {
247
+ "app" => "In App",
248
+ "whatsapp" => "WhatsApp",
249
+ "sms_gateway" => "SMS"
250
+ }
264
251
  ```
265
252
 
266
253
  ## Moderation API
267
254
 
268
255
  ```ruby
269
256
  TurboChat::Moderation.mute_member!(actor: moderator, membership: membership)
257
+ TurboChat::Moderation.unmute_member!(actor: moderator, membership: membership)
270
258
  TurboChat::Moderation.timeout_member!(actor: moderator, membership: membership, until_time: 30.minutes.from_now)
271
259
  TurboChat::Moderation.ban_member!(actor: moderator, membership: membership)
272
260
  TurboChat::Moderation.delete_message!(actor: moderator, message: message)
@@ -274,93 +262,97 @@ TurboChat::Moderation.close_chat!(actor: admin, chat: chat)
274
262
  TurboChat::Moderation.reopen_chat!(actor: admin, chat: chat)
275
263
  ```
276
264
 
277
- 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`).
278
266
 
279
- - `TurboChat::Moderation::AuthorizationError`
280
- - `TurboChat::Moderation::InvalidActionError`
267
+ Raises `TurboChat::Moderation::AuthorizationError` or `TurboChat::Moderation::InvalidActionError`.
281
268
 
282
269
  ## Signals API
283
270
 
284
271
  ```ruby
285
- TurboChat::Signals.start!(chat: chat, participant: current_user, signal_type: :typing)
286
- TurboChat::Signals.start!(chat: chat, participant: current_user, signal_type: :custom, signal_text: "Hello")
287
- TurboChat::Signals.custom!(chat: chat, participant: current_user, signal_text: "Reviewing your request")
288
- 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)
289
277
  ```
290
278
 
291
- ## Event Emissions
292
-
293
- All event emissions are opt-in.
294
-
295
- Enable only what you consume:
279
+ Block form that auto-clears when the work finishes:
296
280
 
297
281
  ```ruby
298
- TurboChat.configure do |config|
299
- config.events.emit_typing_events = true
300
- config.events.emit_message_events = true
301
- config.events.emit_mention_events = true
302
- config.events.emit_invitation_events = true
303
- config.events.emit_chat_lifecycle_events = true
304
- config.moderation.emit_moderation_events = true
305
- config.moderation.emit_blocked_words_events = true
282
+ TurboChat::Signals.with(chat: chat, participant: user, signal_type: :thinking) do
283
+ # long-running operation
306
284
  end
307
285
  ```
308
286
 
309
- Browser events (`CustomEvent`):
287
+ Signal lifetime is configurable via `config.signals.signal_ttl_seconds` (default: 60).
288
+
289
+ ## Events
310
290
 
311
- - `emit_typing_events`: `turbo-chat:typing-started`, `turbo-chat:typing-ended`
312
- - `emit_message_events`: `turbo-chat:message-sent`
313
- - `emit_mention_events`: `turbo-chat:mention`
314
- - `emit_invitation_events`: `turbo-chat:invitation-accepted`
315
- - `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`
291
+ All event emissions are opt-in.
316
292
 
317
- Minimal browser listener:
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
+ ```
318
303
 
319
304
  ```js
320
- [
321
- "turbo-chat:typing-started",
322
- "turbo-chat:typing-ended",
323
- "turbo-chat:message-sent",
324
- "turbo-chat:mention",
325
- "turbo-chat:invitation-accepted",
326
- "turbo-chat:chat-invited",
327
- "turbo-chat:chat-joined",
328
- "turbo-chat:chat-declined",
329
- "turbo-chat:chat-left",
330
- "turbo-chat:chat-closed",
331
- "turbo-chat:chat-reopened"
332
- ].forEach(function (eventName) {
333
- document.addEventListener(eventName, function (event) {
334
- console.log(eventName, event.detail);
335
- });
305
+ document.addEventListener("turbo-chat:message-sent", function (event) {
306
+ console.log(event.detail); // { chatId: "123" }
336
307
  });
337
308
  ```
338
309
 
339
- Server-side notifications (`ActiveSupport::Notifications`):
340
-
341
- - `emit_moderation_events`:
342
- `turbo_chat.moderation.member_muted`,
343
- `turbo_chat.moderation.member_unmuted`,
344
- `turbo_chat.moderation.member_timed_out`,
345
- `turbo_chat.moderation.member_timeout_cleared`,
346
- `turbo_chat.moderation.member_banned`,
347
- `turbo_chat.moderation.message_deleted`,
348
- `turbo_chat.moderation.chat_closed`,
349
- `turbo_chat.moderation.chat_reopened`
350
- - `emit_blocked_words_events`:
351
- `turbo_chat.blocked_words.detected`,
352
- `turbo_chat.blocked_words.rejected`,
353
- `turbo_chat.blocked_words.scrambled`
310
+ ### Server-side events (`ActiveSupport::Notifications`)
354
311
 
355
- 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
+ ```
356
316
 
357
317
  ```ruby
358
- ActiveSupport::Notifications.subscribe(/turbo_chat\.(moderation|blocked_words)\./) do |*args|
318
+ ActiveSupport::Notifications.subscribe(/turbo_chat\.moderation\./) do |*args|
359
319
  event = ActiveSupport::Notifications::Event.new(*args)
360
320
  Rails.logger.info("[TurboChat] #{event.name} #{event.payload.inspect}")
361
321
  end
362
322
  ```
363
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
+
364
356
  ## Upgrade
365
357
 
366
358
  ```bash
@@ -368,6 +360,12 @@ bin/rails turbo_chat:install:migrations
368
360
  bin/rails db:migrate
369
361
  ```
370
362
 
363
+ ## Known Limitations
364
+
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.
368
+
371
369
  ## Dependencies
372
370
 
373
371
  - Ruby `>= 3.1`
@@ -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