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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -1
- data/README.md +178 -190
- data/app/assets/config/turbo_chat_manifest.js +3 -0
- data/app/assets/javascripts/turbo_chat/application.js +3 -0
- data/app/assets/javascripts/turbo_chat/invite_picker.js +19 -392
- data/app/assets/javascripts/turbo_chat/member_sync.js +426 -0
- data/app/assets/javascripts/turbo_chat/mentions.js +366 -0
- data/app/assets/javascripts/turbo_chat/messages.js +18 -370
- data/app/assets/javascripts/turbo_chat/realtime.js +3 -10
- data/app/assets/javascripts/turbo_chat/scroll_proxy.js +379 -0
- data/app/assets/javascripts/turbo_chat/shared.js +7 -383
- data/app/assets/stylesheets/turbo_chat/application.css +9 -1646
- data/app/assets/stylesheets/turbo_chat/base.css +84 -0
- data/app/assets/stylesheets/turbo_chat/components.css +193 -0
- data/app/assets/stylesheets/turbo_chat/composer.css +241 -0
- data/app/assets/stylesheets/turbo_chat/layout.css +307 -0
- data/app/assets/stylesheets/turbo_chat/members.css +264 -0
- data/app/assets/stylesheets/turbo_chat/menus.css +172 -0
- data/app/assets/stylesheets/turbo_chat/messages.css +430 -0
- data/app/controllers/turbo_chat/application_controller.rb +3 -7
- data/app/controllers/turbo_chat/chat_memberships_controller.rb +35 -1
- data/app/controllers/turbo_chat/chat_messages_controller.rb +4 -8
- data/app/controllers/turbo_chat/chats_controller.rb +10 -12
- data/app/helpers/turbo_chat/application_helper/config_support.rb +42 -32
- data/app/helpers/turbo_chat/application_helper/mention_support.rb +3 -3
- data/app/helpers/turbo_chat/application_helper/message_rendering.rb +24 -13
- data/app/models/turbo_chat/chat.rb +43 -20
- data/app/models/turbo_chat/chat_membership.rb +1 -1
- data/app/models/turbo_chat/chat_message/blocked_words_moderation.rb +9 -25
- data/app/models/turbo_chat/chat_message/body_length_validation.rb +1 -1
- data/app/models/turbo_chat/chat_message/broadcasting.rb +2 -6
- data/app/models/turbo_chat/chat_message/formatting.rb +3 -7
- data/app/models/turbo_chat/chat_message/mention_validation.rb +1 -1
- data/app/models/turbo_chat/chat_message/signals.rb +1 -1
- data/app/models/turbo_chat/chat_message.rb +3 -8
- data/app/views/turbo_chat/chat_messages/_form.html.erb +9 -9
- data/app/views/turbo_chat/chat_messages/_message.html.erb +2 -2
- data/app/views/turbo_chat/chat_messages/_signals.html.erb +11 -13
- data/app/views/turbo_chat/chat_messages/_system.html.erb +1 -1
- data/app/views/turbo_chat/chats/_invite_form.html.erb +1 -1
- data/app/views/turbo_chat/chats/_member_entries.html.erb +15 -1
- data/app/views/turbo_chat/chats/index.html.erb +1 -1
- data/app/views/turbo_chat/chats/new.html.erb +4 -7
- data/app/views/turbo_chat/chats/show.html.erb +29 -27
- data/config/routes.rb +6 -1
- data/db/migrate/20260325000016_add_chat_mode_to_turbo_chat_chats.rb +6 -0
- data/lib/generators/turbo_chat/install/templates/turbo_chat.rb +8 -0
- data/lib/turbo_chat/configuration/defaults.rb +21 -0
- data/lib/turbo_chat/configuration.rb +105 -0
- data/lib/turbo_chat/moderation/chat_actions.rb +2 -2
- data/lib/turbo_chat/moderation/member_actions.rb +2 -1
- data/lib/turbo_chat/moderation/support.rb +5 -9
- data/lib/turbo_chat/permission/support.rb +6 -2
- data/lib/turbo_chat/permission.rb +1 -5
- data/lib/turbo_chat/signals.rb +1 -1
- data/lib/turbo_chat/version.rb +1 -1
- metadata +13 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2a123f536b09e56d7dbbe582048c7d5eceff7d043126c947647c567b6004b972
|
|
4
|
+
data.tar.gz: d568733e0d68d5e74c67bf0ab7f0a3ccc6433aa14797575311fe1d9250490835
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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,
|
|
12
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
+
### 2. Resolve the current participant
|
|
68
67
|
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
100
|
+
Config is organized into scoped namespaces:
|
|
110
101
|
|
|
111
102
|
```ruby
|
|
112
103
|
TurboChat.configure do |config|
|
|
113
|
-
|
|
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
|
|
111
|
+
config.chat_message.blocked_words_action = :reject # or :scramble
|
|
124
112
|
|
|
125
|
-
|
|
126
|
-
config.style.
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
config.
|
|
134
|
-
config.
|
|
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 (
|
|
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
|
-
###
|
|
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
|
-
|
|
127
|
+
Chats default to `chat_mode: :standard`.
|
|
159
128
|
|
|
160
129
|
```ruby
|
|
161
|
-
TurboChat::
|
|
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
|
-
|
|
133
|
+
Built-in assistant-mode defaults make the chat simpler for 1:1 assistant use cases:
|
|
170
134
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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.
|
|
189
|
-
|
|
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
|
-
|
|
151
|
+
### Rate Limiting
|
|
197
152
|
|
|
198
|
-
|
|
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
|
-
|
|
155
|
+
## Roles and Permissions
|
|
205
156
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
205
|
+
TurboChat::ChatMembership.create!(
|
|
206
|
+
chat: chat,
|
|
207
|
+
participant: invitee,
|
|
208
|
+
role: :member,
|
|
209
|
+
invitation_accepted: false
|
|
210
|
+
)
|
|
241
211
|
```
|
|
242
212
|
|
|
243
|
-
|
|
213
|
+
Accept/decline routes are built in: `PATCH /chats/:id/accept` and `PATCH /chats/:id/decline`.
|
|
244
214
|
|
|
245
|
-
|
|
215
|
+
## Message Ingest API
|
|
216
|
+
|
|
217
|
+
Post messages as a specific participant:
|
|
246
218
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
228
|
+
External ingest with idempotency (`chat_id + source + external_id`):
|
|
252
229
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
290
|
-
TurboChat::Signals.start!(chat: chat, participant:
|
|
291
|
-
TurboChat::Signals.
|
|
292
|
-
TurboChat::Signals.
|
|
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
|
-
|
|
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.
|
|
303
|
-
|
|
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
|
-
|
|
287
|
+
Signal lifetime is configurable via `config.signals.signal_ttl_seconds` (default: 60).
|
|
314
288
|
|
|
315
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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\.
|
|
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
|
|
378
|
-
- **Blocked word filtering.** Word-boundary matching can be bypassed with Unicode homoglyphs, zero-width characters, or leetspeak
|
|
379
|
-
- **Invitable participants cap.** The invite picker returns at most 100 non-member participants.
|
|
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
|