livechat 0.7.1 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d8510b0623f63c828acb19577dc5383d2541a245e5cda9ee667b1dba3f3a5b19
4
- data.tar.gz: 3ae2d82e86cd025e3a750bbf583e3362273bc1b4c2b5888432b45e6445c754fc
3
+ metadata.gz: 275ee37e54a4e5a5b30abb69aea78e691d05c78e2b6e25124bb51b10b442881e
4
+ data.tar.gz: 7769b4a9dfe5851ca2d7f1bece6c3c4197c9573a4be6e87673df1f40ce5be8c8
5
5
  SHA512:
6
- metadata.gz: 1c6186e6fc58c2b3b9c4732e8f25dcc07e358698ac5c31ebf60cdb8d08bf27cd50ff27e3e67b5c28713c4f14fc9eb323e55620926132ab76ca08c36829dd4eb9
7
- data.tar.gz: fce349af0d40f1b1fd547b6cf442d8f81acec08ff425f5e105326149fce925c6c594161a3350b924ed42e947be079257776783effa2820eb732f6ea11886f56f
6
+ metadata.gz: '08038c4cea2d6942c3e5270dd610385e891178018436aa6feeb4191d0f08129937087662217d56ab298f2284bfb16463a093c5e872ede78d76e8fb1ca28ccb0f'
7
+ data.tar.gz: f788355751c85357edb0c7e79db9b9c5c6e775efe79e30c5f6b7249fe9a1fc2ad5adb247da7a4adea70b78a13a6e917b20d723dd935c3b3a3f706db48aa7befe
data/AGENTS.md ADDED
@@ -0,0 +1,173 @@
1
+ # AGENTS.md
2
+
3
+ Instructions for coding agents. Two audiences:
4
+
5
+ - **[Installing livechat into a Rails app](#installing-into-a-rails-app)** — you are working in a host app and were asked to add support chat, live chat, or an in-app inbox.
6
+ - **[Working on the gem itself](#working-on-the-gem-itself)** — you are working in this repository.
7
+
8
+ Requirements: Ruby >= 3.2, Rails >= 7.1. Active Storage only for file attachments. **No Redis and no Action Cable** — the transport is polling unless you opt in.
9
+
10
+ If you are in a host app and this file is not in front of you, it ships inside the gem: `cat "$(bundle show livechat)/AGENTS.md"`.
11
+
12
+ ---
13
+
14
+ ## Installing into a Rails app
15
+
16
+ ### 1. Install
17
+
18
+ ```bash
19
+ bundle add livechat
20
+ bin/rails generate livechat:install
21
+ bin/rails db:migrate
22
+ ```
23
+
24
+ The generator writes `config/initializers/livechat.rb`, one migration (`livechat_conversations`, `livechat_messages`), and `mount_livechat at: "/livechat"` into `config/routes.rb`. Read the initializer it wrote — every option is documented there in comments, and it is the source of truth over any summary of it, including this file.
25
+
26
+ Every `config.…` line below belongs inside the `Livechat.configure do |config|` block in that initializer. Uncomment and edit in place rather than appending a second `configure` block.
27
+
28
+ ### 2. Wire the three things the generator cannot
29
+
30
+ **a. The widget tag.** Nothing appears until this is on the page:
31
+
32
+ ```erb
33
+ <%# app/views/layouts/application.html.erb, before </body> %>
34
+ <%= livechat_tag %>
35
+ ```
36
+
37
+ The helper is injected into ActionView by the engine — no include, no import, no asset pipeline entry. It renders the launcher bubble bottom-right.
38
+
39
+ **b. `authorize_agent` — do this before deploying.** The inbox at `/livechat` defaults to **development only**. It fails closed, so shipping without this is not an open inbox — it is a 403 reading "Forbidden. Set Livechat.config.authorize_agent to grant access."
40
+
41
+ ```ruby
42
+ config.authorize_agent = ->(request) { request.env["warden"]&.user&.admin? }
43
+ ```
44
+
45
+ **c. Visitor identity**, if the app has users. Without it every visitor is a cookie-tracked guest, and nobody in the inbox has a name.
46
+
47
+ ```ruby
48
+ config.current_user = ->(request) { request.env["warden"]&.user }
49
+ config.visitor_label = ->(user) { user.name } # what the inbox shows
50
+ config.agent_label = ->(user) { user.name } # signed onto each reply
51
+ ```
52
+
53
+ > **`current_user`, `enabled` and `authorize_agent` receive the raw `request`, not a controller.** Writing `->(request) { current_user }` is the most common mistake here — that method does not exist in this scope. Resolve the user *from the request*: Warden env, a signed cookie, `Current.user` if middleware already set it. Note the different shapes: `visitor_label` and `agent_label` receive the **user**, while `agent_display_name` receives the already-stored **label string**.
54
+
55
+ Rails 8 built-in auth:
56
+
57
+ ```ruby
58
+ config.current_user = lambda do |request|
59
+ token = request.cookies["session_token"]
60
+ Session.find_signed(token)&.user if token
61
+ end
62
+ ```
63
+
64
+ ### 3. Verify
65
+
66
+ ```bash
67
+ bin/rails routes | grep livechat # engine mounted
68
+ bin/rails livechat:seed_demo # optional sample conversations, idempotent
69
+ ```
70
+
71
+ Then in the running app: load any page, confirm the bubble appears bottom-right, send a message, and answer it at `/livechat`.
72
+
73
+ ### Opening the widget
74
+
75
+ | Way | How |
76
+ | --- | --- |
77
+ | The launcher bubble | On by default. `config.show_launcher = false` to remove it |
78
+ | Your own element | `<%= livechat_button %>`, or any element with `data-livechat-open` |
79
+ | JavaScript | `window.Livechat.open()` |
80
+
81
+ A visitor has **one conversation**, not a queue of tickets — writing again reopens the same thread. Signed-in visitors keep it across devices (keyed by user id); guests are tracked by cookie.
82
+
83
+ ### Email notifications need two settings, not one
84
+
85
+ ```ruby
86
+ config.mailer_from = "support@example.com" # required, or nothing sends
87
+ config.agent_emails = ["team@example.com"] # array, or a callable returning one
88
+ ```
89
+
90
+ Setting `agent_emails` alone sends nothing: `mailer_from` is what switches email on (`Livechat.config.emails_enabled?` is `mailer_from.present?`). Notification is one email per unread stretch, not one per message.
91
+
92
+ ### Realtime is opt-in
93
+
94
+ Polling is the default transport, on purpose — a host with no Action Cable works untouched. Turning on push requires the host to actually mount a cable:
95
+
96
+ ```ruby
97
+ config.action_cable = true
98
+ config.action_cable_url = "/cable" # keep in sync with the mount in routes.rb
99
+ ```
100
+
101
+ Leave it off unless the app already has Action Cable working. Polling is not a degraded mode here.
102
+
103
+ ### Attachments
104
+
105
+ `config.attach_files` is on by default but **silently inert without Active Storage** in the host app (`rails active_storage:install`) — the widget keeps working, just without a paperclip. Caps: `max_attachments` (5), `max_attachment_size` (10 MB), `allowed_attachment_types` (nil = any). Files are served through the engine at `/livechat/attachments/:id`, gated per request — never a public blob URL. Do not build your own blob links.
106
+
107
+ ### Do not
108
+
109
+ - **Do not copy the widget JavaScript into `app/javascript`, or add a `<script>` tag for it.** `livechat_tag` renders what is needed, and the engine serves the code with a content fingerprint. There is no build step and nothing for esbuild/importmap/Tailwind to know about.
110
+ - **Do not build your own inbox.** Use the mounted one; `config.agent_layout = "admin/application"` renders it inside an existing admin shell.
111
+ - **Do not add an Action Cable mount to "make chat realtime"** unless you also set `config.action_cable = true`. Polling is the default and is not broken.
112
+ - **Do not set config outside the initializer.** `rate_limit` in particular is read when the controller class loads; assigning config per-request mutates it process-wide.
113
+ - **Do not expose attachments by blob URL** — the engine's gated route exists so a leaked signed URL cannot hand over a customer's file.
114
+
115
+ ### Configuration worth knowing
116
+
117
+ Everything is optional; a fresh install works with zero config. Full list with comments is in the generated initializer.
118
+
119
+ | Option | Default | Note |
120
+ | --- | --- | --- |
121
+ | `authorize_agent` | development only | **Who can read the inbox. Set before deploying.** |
122
+ | `enabled` | everyone | Per-request gate for the widget and its endpoints |
123
+ | `current_user` | `nil` | Receives the request; nil means guest-by-cookie |
124
+ | `visitor_label`, `agent_label` | name/email/to_s | Receive the user |
125
+ | `agent_display_name` | the label unchanged | Receives the label; return "Support team" to keep agents anonymous |
126
+ | `app_name`, `greeting`, `reply_time_text`, `launcher_label` | localized defaults | Widget copy |
127
+ | `avatar_url`, `accent_color` | `nil` | Header avatar (URL or callable) and brand hex |
128
+ | `show_launcher` | `true` | `false` = open only from your own elements |
129
+ | `mailer_from` | `nil` | **Required for any email at all** |
130
+ | `agent_emails` | `nil` | Array or callable |
131
+ | `attach_files` | `true` | Needs Active Storage; inert without it |
132
+ | `storage_service` | app default | A `storage.yml` key for a dedicated bucket |
133
+ | `max_attachments`, `max_attachment_size` | `5`, `10.megabytes` | Enforced server-side |
134
+ | `allowed_attachment_types` | `nil` | Content-type allowlist |
135
+ | `action_cable`, `action_cable_url` | `false`, `"/cable"` | Opt-in push |
136
+ | `rate_limit` | `{ to: 30, within: 60 }` | Rails 7.2+; ignored on 7.1. `nil` disables |
137
+ | `mount_path` | `"/livechat"` | Keep in sync with `mount_livechat at:` |
138
+ | `on_visitor_message`, `on_agent_message` | no-ops | Run inline after save — Slack, Noticed, push |
139
+
140
+ Turbo Drive and strict nonce-based CSP work out of the box. 26 locales ship with the gem, RTL included.
141
+
142
+ ### Common failure modes
143
+
144
+ | Symptom | Cause |
145
+ | --- | --- |
146
+ | `/livechat` returns 403 "Set Livechat.config.authorize_agent to grant access" | Exactly what it says: still at the development-only default |
147
+ | No bubble on the page | `livechat_tag` missing from the rendered layout, `config.enabled` false, or `show_launcher = false` with no opener of your own |
148
+ | No notification emails | `mailer_from` not set — `agent_emails` alone does nothing |
149
+ | Messages only appear on refresh | Expected: polling is the default. `config.action_cable = true` (with `/cable` mounted) for push |
150
+ | No attachment button | Active Storage not installed, or `attach_files = false` |
151
+ | `undefined local variable current_user` in the initializer | A gate lambda treated its argument as a controller. It is a `request` |
152
+
153
+ ---
154
+
155
+ ## Working on the gem itself
156
+
157
+ ```bash
158
+ bundle exec rake test # minitest, dummy app under test/dummy
159
+ bundle exec rubocop # must be clean
160
+ BUNDLE_GEMFILE=gemfiles/rails_7.1.gemfile bundle exec rake test # 7.1, 7.2, 8.0, 8.1 in gemfiles/
161
+ ```
162
+
163
+ Layout: `app/` controllers, models, inbox views, mailer · `lib/livechat/` config, widget JS/CSS, seeds, engine, channels · `lib/generators/livechat/install/` the one generator · `config/locales/` 26 locales · `test/` minitest, `test/dummy` the host app.
164
+
165
+ Conventions this codebase holds to — follow them rather than the first thing that works:
166
+
167
+ - **Polling is the baseline, Action Cable is opt-in.** Nothing may require a cable to be mounted. The channel lives under `lib/` and is required only when `ActionCable` is defined, so eager-loading an app without it cannot fail.
168
+ - **Active Storage is optional at runtime.** Attachment code checks for it rather than assuming it; an app without Active Storage gets a working widget, not an exception.
169
+ - **The widget is plain ES5-style JS served by the engine**, no build step, no framework, config read from a JSON block so a Turbo visit re-reads the current page's settings.
170
+ - **Visitor scoping is never by conversation id.** The widget's endpoints resolve the thread from the signed-in id or the guest cookie, so no id in a request can address someone else's conversation. Keep it that way.
171
+ - **The dummy app pins `config.active_job.queue_adapter = :test`.** Do not remove it or let it drift back to the `:async` default. Attaching a file enqueues Active Storage's analysis job, and `:async` runs it on a background thread that checks out its own connection — writes no test transaction covers, landing in the middle of whatever runs next. That is a suite that fails order-dependently in a test which never created a row, and it is miserable to trace back.
172
+ - Every user-facing change bumps `lib/livechat/version.rb` and adds a `CHANGELOG.md` entry that says what it costs, not only what it adds.
173
+ - Commit messages are prose that explains the tradeoff — read `git log` before writing one.
data/CHANGELOG.md CHANGED
@@ -1,5 +1,50 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.0
4
+
5
+ - **`config.agent_layout` now works on its own.** The inbox's stylesheet and
6
+ script were declared in the gem's layout, so replacing that layout dropped
7
+ both: unstyled inbox with its thread polling and keyboard submit dead. They
8
+ move into the views, so every layout gets them with nothing asked of the host.
9
+ - **The dashboard stylesheet no longer claims selectors it does not own.** It
10
+ styled bare `*`, `body` and `a`, and its `.container`, `.card` and `.tabs` are
11
+ names other frameworks use too. Component rules now nest inside a
12
+ `.lvc-dashboard` wrapper the views render, and every custom property is
13
+ `--lvc-` prefixed — that collision ran both ways, so a host defining `--bg`
14
+ recoloured the inbox just as easily.
15
+ - **Added `config.base_controller_class`.** Name the controller your own admin
16
+ inherits from and the inbox adopts its layout, helpers, authentication and
17
+ request context. It reparents the inbox only — the widget, visitor API and
18
+ attachment proxy stay on the engine's public controller, so it can never
19
+ demand a staff session from a visitor starting a chat. Default is unchanged.
20
+ - **Migrations follow the host's `primary_key_type`,** including the engine's own
21
+ `messages -> conversations` foreign key, which has to match or a bigint column
22
+ ends up pointing at a uuid table. A uuid-keyed app has a uuid
23
+ `active_storage_attachments.record_id`, so bigint tables here could never hold
24
+ a message attachment: `attach` raised `NotNullViolation`.
25
+ - **Dropped the `id: /\d+/` constraints** on the attachment and conversation
26
+ routes, which were what forced the tables to be bigint. Ordering already
27
+ disambiguates: every fixed-name route is declared first.
28
+ - **`t.references :conversation` no longer creates its own index.**
29
+ `(conversation_id)` is a leftmost prefix of the `(conversation_id, id)` index
30
+ the migration already adds, so it answered no query the wider one could not.
31
+ Existing installs keep theirs until they drop it:
32
+ `remove_index :livechat_messages, :conversation_id`.
33
+ - A `BackboneTest` now fails the build on any of the above regressing.
34
+
35
+ ## 0.7.2
36
+
37
+ - Adds `AGENTS.md`: install and integration instructions written for coding
38
+ agents — the request-shaped config lambdas, the two settings email needs, why
39
+ polling is the default and Action Cable is opt-in, and the mistakes agents
40
+ actually make. It ships inside the gem, so
41
+ `cat "$(bundle show livechat)/AGENTS.md"` works from a host app.
42
+ - The dummy app pins `queue_adapter = :test` for the test suite. Attaching a
43
+ file enqueues Active Storage's analysis job, and the default `:async` adapter
44
+ runs it on a background thread with its own database connection — writes no
45
+ test transaction covers, which is how a suite starts failing order-dependently
46
+ in a test that never created a row. No effect on the gem itself.
47
+
3
48
  ## 0.7.1
4
49
 
5
50
  - Removed the translucent border and background from the customer-facing
data/README.md CHANGED
@@ -52,6 +52,11 @@ demo messages instead of duplicating conversations.
52
52
 
53
53
  Ruby >= 3.2 · Rails >= 7.1 · Active Storage only if you want file attachments.
54
54
 
55
+ Installing with a coding agent? Point it at [AGENTS.md](AGENTS.md) — the same
56
+ steps in the order an agent needs them, plus the gates it tends to get wrong and
57
+ the things it should not do. It ships inside the gem, so
58
+ `cat "$(bundle show livechat)/AGENTS.md"` works from any app that bundles it.
59
+
55
60
  ## What you get
56
61
 
57
62
  | | |
@@ -121,6 +126,7 @@ Everything is optional — a fresh install works with zero config. In
121
126
  | Option | Default | What it does |
122
127
  | --- | --- | --- |
123
128
  | `authorize_agent` | development only | **Who can read the inbox.** Override before deploying |
129
+ | `base_controller_class` | `ActionController::Base` | Controller the inbox inherits — name your admin's and it adopts its layout, helpers and auth |
124
130
  | `enabled` | everyone | Who sees the widget. `false` hides it and rejects posts |
125
131
  | `current_user` | `nil` | Identify the visitor. Receives the request |
126
132
  | `app_name` | Rails app name | Shown in the widget header |
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Livechat
4
+ # Who is asking — visitor or agent — and the gates that answer it.
5
+ #
6
+ # A concern rather than inherited behaviour because the engine has two
7
+ # controller roots: the widget's endpoints hang off ActionController::Base, and
8
+ # the inbox hangs off whatever the host set as `base_controller_class`.
9
+ module RequestContext
10
+ extend ActiveSupport::Concern
11
+
12
+ private
13
+
14
+ def livechat_agent_layout
15
+ Livechat.config.agent_layout
16
+ end
17
+
18
+ def current_visitor
19
+ return @current_visitor if defined?(@current_visitor)
20
+
21
+ @current_visitor = Livechat.config.current_user.call(request)
22
+ end
23
+
24
+ def current_visitor_id
25
+ current_visitor.respond_to?(:id) ? current_visitor.id.to_s : nil
26
+ end
27
+
28
+ def require_enabled
29
+ head :forbidden unless Livechat.enabled?(request)
30
+ end
31
+
32
+ # Server-side gate for the inbox. Default: development only.
33
+ def require_agent
34
+ return if Livechat.agent?(request)
35
+
36
+ render plain: 'Forbidden. Set Livechat.config.authorize_agent to grant access.',
37
+ status: :forbidden
38
+ end
39
+
40
+ # Replies are attributed to whoever config.current_user resolves. When an
41
+ # inbox is protected by something user-less (HTTP basic, a VPN), replies
42
+ # still work — anonymously, as "Support".
43
+ def current_agent_id
44
+ current_visitor_id || '0'
45
+ end
46
+
47
+ def current_agent_label
48
+ label = (Livechat.config.agent_label.call(current_visitor).presence if current_visitor)
49
+ label || I18n.t(:team, scope: :livechat, default: 'Support')
50
+ end
51
+
52
+ def render_rate_limited
53
+ message = I18n.t('livechat.error_rate_limited',
54
+ default: 'Too many messages. Please wait a moment and try again.')
55
+ render json: { errors: [message] }, status: :too_many_requests
56
+ end
57
+
58
+ # Guests get a permanent random token — their key to the conversation
59
+ # across visits. Signed-in visitors are keyed by id and only need the
60
+ # cookie to carry a guest history into their account.
61
+ def visitor_token
62
+ cookies[:livechat_vid]
63
+ end
64
+
65
+ def ensure_visitor_token
66
+ visitor_token.presence || begin
67
+ token = SecureRandom.base58(24)
68
+ cookies.permanent[:livechat_vid] = { value: token, httponly: true, same_site: :lax }
69
+ token
70
+ end
71
+ end
72
+
73
+ # A guest who signed in keeps their thread: adopt cookie-token
74
+ # conversations into the account, then the cookie no longer matters.
75
+ def claim_guest_conversations
76
+ return unless current_visitor_id && visitor_token
77
+
78
+ Conversation.claim!(
79
+ visitor_token: visitor_token,
80
+ visitor_id: current_visitor_id,
81
+ visitor_label: Livechat.config.visitor_label.call(current_visitor).presence
82
+ )
83
+ end
84
+ end
85
+ end
@@ -1,80 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Livechat
4
+ # Root of the engine's PUBLIC surface: widget.js, the visitor API and the
5
+ # attachment proxy (which serves visitors as well as agents). These stay on a
6
+ # plain ActionController::Base deliberately — a visitor starting a chat must
7
+ # not be routed through a host's admin controller, which would demand a staff
8
+ # session for the widget.
9
+ #
10
+ # The inbox's root is DashboardController, and that is where
11
+ # `config.base_controller_class` applies.
4
12
  class ApplicationController < ActionController::Base
5
- protect_from_forgery with: :exception
6
-
7
- private
8
-
9
- def livechat_agent_layout
10
- Livechat.config.agent_layout
11
- end
12
-
13
- def current_visitor
14
- return @current_visitor if defined?(@current_visitor)
15
-
16
- @current_visitor = Livechat.config.current_user.call(request)
17
- end
18
-
19
- def current_visitor_id
20
- current_visitor.respond_to?(:id) ? current_visitor.id.to_s : nil
21
- end
22
-
23
- def require_enabled
24
- head :forbidden unless Livechat.enabled?(request)
25
- end
26
-
27
- # Server-side gate for the inbox. Default: development only.
28
- def require_agent
29
- return if Livechat.agent?(request)
13
+ include RequestContext
30
14
 
31
- render plain: 'Forbidden. Set Livechat.config.authorize_agent to grant access.',
32
- status: :forbidden
33
- end
34
-
35
- # Replies are attributed to whoever config.current_user resolves. When an
36
- # inbox is protected by something user-less (HTTP basic, a VPN), replies
37
- # still work — anonymously, as "Support".
38
- def current_agent_id
39
- current_visitor_id || '0'
40
- end
41
-
42
- def current_agent_label
43
- label = (Livechat.config.agent_label.call(current_visitor).presence if current_visitor)
44
- label || I18n.t(:team, scope: :livechat, default: 'Support')
45
- end
46
-
47
- def render_rate_limited
48
- message = I18n.t('livechat.error_rate_limited',
49
- default: 'Too many messages. Please wait a moment and try again.')
50
- render json: { errors: [message] }, status: :too_many_requests
51
- end
52
-
53
- # Guests get a permanent random token — their key to the conversation
54
- # across visits. Signed-in visitors are keyed by id and only need the
55
- # cookie to carry a guest history into their account.
56
- def visitor_token
57
- cookies[:livechat_vid]
58
- end
59
-
60
- def ensure_visitor_token
61
- visitor_token.presence || begin
62
- token = SecureRandom.base58(24)
63
- cookies.permanent[:livechat_vid] = { value: token, httponly: true, same_site: :lax }
64
- token
65
- end
66
- end
67
-
68
- # A guest who signed in keeps their thread: adopt cookie-token
69
- # conversations into the account, then the cookie no longer matters.
70
- def claim_guest_conversations
71
- return unless current_visitor_id && visitor_token
72
-
73
- Conversation.claim!(
74
- visitor_token: visitor_token,
75
- visitor_id: current_visitor_id,
76
- visitor_label: Livechat.config.visitor_label.call(current_visitor).presence
77
- )
78
- end
15
+ protect_from_forgery with: :exception
79
16
  end
80
17
  end
@@ -4,10 +4,8 @@ module Livechat
4
4
  # The inbox. Every action is gated by config.authorize_agent; any
5
5
  # authorized teammate can read and answer any thread — it's a shared line,
6
6
  # not an assignment queue.
7
- class ConversationsController < ApplicationController
8
- before_action :require_agent
7
+ class ConversationsController < DashboardController
9
8
  before_action :set_conversation, except: %i[index index_poll]
10
- layout :livechat_agent_layout
11
9
 
12
10
  PER_PAGE = 50
13
11
 
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Livechat
4
+ # Root of the AGENT surface: the inbox and agent replies.
5
+ #
6
+ # Inherits from `config.base_controller_class` — by default a plain
7
+ # ActionController::Base, which is why `authorize_agent` exists. Point it at
8
+ # the controller your own admin already inherits from and the inbox picks up
9
+ # that stack wholesale: your layout, your helpers, your authentication, and
10
+ # whatever request context your before_actions establish.
11
+ #
12
+ # Only the inbox hangs off it. The widget's endpoints stay on
13
+ # ApplicationController, so wiring an admin base controller here can never
14
+ # demand a staff session from a visitor starting a chat.
15
+ class DashboardController < Livechat.base_controller
16
+ include RequestContext
17
+
18
+ # A host base controller brings its own layout, and declaring one here would
19
+ # override it. So the gem only claims the layout when it owns the decision:
20
+ # no host base controller, or a host that named an `agent_layout` explicitly.
21
+ layout :livechat_agent_layout unless superclass != ActionController::Base &&
22
+ Livechat.config.agent_layout == Configuration::DEFAULT_AGENT_LAYOUT
23
+
24
+ before_action :require_agent
25
+
26
+ # A host base controller has configured CSRF already; declaring it twice
27
+ # would run the check twice.
28
+ protect_from_forgery with: :exception if superclass == ActionController::Base
29
+ end
30
+ end
@@ -1,9 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Livechat
4
- class MessagesController < ApplicationController
5
- before_action :require_agent
6
-
4
+ class MessagesController < DashboardController
7
5
  def create
8
6
  conversation = Conversation.find(params[:conversation_id])
9
7
  message = conversation.post_agent_message!(
@@ -5,19 +5,18 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <%= csrf_meta_tags %>
7
7
  <%= csp_meta_tag %>
8
- <%= stylesheet_link_tag "#{dashboard_stylesheet_path}?v=#{Livechat::Widget.dashboard_stylesheet_fingerprint}",
9
- 'data-turbo-track': 'reload' %>
10
- <%# Same-origin script instead of inline handlers, so thread polling and
11
- keyboard submit work under strict script-src CSPs. %>
12
- <%= javascript_include_tag "#{dashboard_script_path}?v=#{Livechat::Widget.dashboard_fingerprint}",
13
- defer: true, nonce: true %>
8
+ <%# The inbox's stylesheet and script are declared by the views
9
+ (livechat/shared/_dashboard), not here, so they survive a host replacing
10
+ this layout via config.agent_layout or config.base_controller_class. %>
14
11
  </head>
15
- <body class="<%= yield(:body_class) %>">
16
- <div class="container">
17
- <% if flash[:alert].present? %>
18
- <p class="flash alert"><%= flash[:alert] %></p>
19
- <% end %>
20
- <%= yield %>
12
+ <body class="lvc-page <%= yield(:body_class) %>">
13
+ <div class="lvc-dashboard">
14
+ <div class="container">
15
+ <% if flash[:alert].present? %>
16
+ <p class="flash alert"><%= flash[:alert] %></p>
17
+ <% end %>
18
+ <%= yield %>
19
+ </div>
21
20
  </div>
22
21
  </body>
23
22
  </html>