livechat 0.7.2 → 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 +4 -4
- data/CHANGELOG.md +32 -0
- data/README.md +1 -0
- data/app/controllers/concerns/livechat/request_context.rb +85 -0
- data/app/controllers/livechat/application_controller.rb +10 -73
- data/app/controllers/livechat/conversations_controller.rb +1 -3
- data/app/controllers/livechat/dashboard_controller.rb +30 -0
- data/app/controllers/livechat/messages_controller.rb +1 -3
- data/app/views/layouts/livechat/application.html.erb +11 -12
- data/app/views/livechat/conversations/index.html.erb +91 -89
- data/app/views/livechat/conversations/show.html.erb +6 -4
- data/app/views/livechat/shared/_dashboard.html.erb +18 -0
- data/config/routes.rb +9 -4
- data/lib/generators/livechat/install/install_generator.rb +2 -6
- data/lib/generators/livechat/install/templates/create_livechat_tables.rb.tt +5 -3
- data/lib/generators/livechat/install/templates/initializer.rb +8 -0
- data/lib/generators/livechat/migration_helpers.rb +43 -0
- data/lib/livechat/configuration.rb +21 -1
- data/lib/livechat/dashboard.css +224 -231
- data/lib/livechat/version.rb +1 -1
- data/lib/livechat.rb +7 -0
- metadata +5 -1
|
@@ -15,6 +15,14 @@ Livechat.configure do |config|
|
|
|
15
15
|
#
|
|
16
16
|
# Render the inbox inside your app's admin layout. Default: the gem's
|
|
17
17
|
# standalone inbox layout.
|
|
18
|
+
# Two ways to put the inbox inside an admin you already have.
|
|
19
|
+
#
|
|
20
|
+
# The whole stack — your layout, helpers, authentication and any request
|
|
21
|
+
# context your before_actions set up. Only the inbox inherits it; the
|
|
22
|
+
# widget's endpoints stay public, so this cannot gate a visitor's chat.
|
|
23
|
+
# config.base_controller_class = "Admin::BaseController"
|
|
24
|
+
#
|
|
25
|
+
# Or the layout alone:
|
|
18
26
|
# config.agent_layout = "admin/application"
|
|
19
27
|
|
|
20
28
|
# Resolve the current user (optional). Return an object responding to #id,
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Livechat
|
|
4
|
+
module Generators
|
|
5
|
+
# Shared bits every migration-writing generator needs.
|
|
6
|
+
module MigrationHelpers
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def migration_version
|
|
10
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Follow the host's own key type instead of forcing bigint. An app that
|
|
14
|
+
# keys its models with uuids has a uuid
|
|
15
|
+
# `active_storage_attachments.record_id`, so a bigint table here could
|
|
16
|
+
# never take a message attachment: the foreign key has nowhere to point
|
|
17
|
+
# and `attach` raises NotNullViolation.
|
|
18
|
+
#
|
|
19
|
+
# Same lookup Rails' own Active Storage, Action Text and Action Mailbox
|
|
20
|
+
# migrations do, so a host that set it once gets consistent tables from
|
|
21
|
+
# all of them.
|
|
22
|
+
#
|
|
23
|
+
# Rendered as options rather than bare values, because a template is
|
|
24
|
+
# expanded at generate time: emitting the method name would put
|
|
25
|
+
# `id: primary_key_type` in the migration, where nothing defines it.
|
|
26
|
+
def primary_key_type_option
|
|
27
|
+
primary_key_type ? ", id: :#{primary_key_type}" : ''
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The engine's own foreign key (messages -> conversations) has to match
|
|
31
|
+
# whatever the referenced table's primary key became, or the reference
|
|
32
|
+
# column is a bigint pointing at a uuid.
|
|
33
|
+
def foreign_key_type_option
|
|
34
|
+
primary_key_type ? ", type: :#{primary_key_type}" : ''
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def primary_key_type
|
|
38
|
+
config = Rails.configuration.generators
|
|
39
|
+
config.options[config.orm][:primary_key_type]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -5,6 +5,10 @@ module Livechat
|
|
|
5
5
|
# works with zero configuration; the hooks below let an app decide who can
|
|
6
6
|
# chat, who answers, and how everyone is named.
|
|
7
7
|
class Configuration
|
|
8
|
+
# The gem's own agent layout. Compared against, so DashboardController can
|
|
9
|
+
# tell "the host left this alone" from "the host chose this".
|
|
10
|
+
DEFAULT_AGENT_LAYOUT = 'livechat/application'
|
|
11
|
+
|
|
8
12
|
# Shown in the widget header and in notification emails. nil resolves to
|
|
9
13
|
# the Rails application name.
|
|
10
14
|
attr_accessor :app_name
|
|
@@ -22,6 +26,21 @@ module Livechat
|
|
|
22
26
|
# inside your app's admin shell, e.g. "admin/application".
|
|
23
27
|
attr_accessor :agent_layout
|
|
24
28
|
|
|
29
|
+
# The controller the INBOX inherits from, as a String so it resolves lazily
|
|
30
|
+
# rather than at config time. Default: a plain 'ActionController::Base',
|
|
31
|
+
# where `authorize_agent` is the only gate.
|
|
32
|
+
#
|
|
33
|
+
# Name the controller your own admin already inherits from and the inbox
|
|
34
|
+
# adopts that whole stack — layout, helpers, authentication, and any request
|
|
35
|
+
# context your before_actions set up. `agent_layout` covers only the layout,
|
|
36
|
+
# which leaves a host layout calling its own helpers to raise NameError under
|
|
37
|
+
# the engine's isolated namespace.
|
|
38
|
+
#
|
|
39
|
+
# Only the inbox uses it. The widget's endpoints stay on the engine's own
|
|
40
|
+
# public controller, so an admin base controller here can never demand a
|
|
41
|
+
# staff session from a visitor starting a chat.
|
|
42
|
+
attr_accessor :base_controller_class
|
|
43
|
+
|
|
25
44
|
# Resolve the current user (optional). Return an object responding to
|
|
26
45
|
# #id, or nil. Receives the request. Signed-in users keep one conversation
|
|
27
46
|
# across devices; guests are tracked with a cookie.
|
|
@@ -109,7 +128,8 @@ module Livechat
|
|
|
109
128
|
@app_name = nil
|
|
110
129
|
@enabled = ->(_request) { true }
|
|
111
130
|
@authorize_agent = ->(_request) { Rails.env.development? }
|
|
112
|
-
@agent_layout =
|
|
131
|
+
@agent_layout = DEFAULT_AGENT_LAYOUT
|
|
132
|
+
@base_controller_class = 'ActionController::Base'
|
|
113
133
|
@current_user = ->(_request) {}
|
|
114
134
|
@visitor_label = ->(user) { user.try(:name).presence || user.try(:email).presence || user.to_s }
|
|
115
135
|
@agent_label = ->(user) { user.try(:name).presence || user.try(:email).presence || user.to_s }
|
data/lib/livechat/dashboard.css
CHANGED
|
@@ -1,233 +1,226 @@
|
|
|
1
|
-
|
|
1
|
+
/* Dashboard styles.
|
|
2
|
+
*
|
|
3
|
+
* Three layers, and the split is what lets a host replace the layout:
|
|
4
|
+
*
|
|
5
|
+
* :root custom properties, all `--lvc-` prefixed so they can
|
|
6
|
+
* neither overwrite a host's nor be overwritten by them.
|
|
7
|
+
* .lvc-page the page frame (was bare html/body). Only the gem's own
|
|
8
|
+
* layout puts this class on <body>, so inside a host admin
|
|
9
|
+
* these rules simply do not apply.
|
|
10
|
+
* .lvc-dashboard everything else, nested. Names like .container, .card
|
|
11
|
+
* and .tabs are confined to the dashboard's own markup, so
|
|
12
|
+
* loading this file inside a host cannot restyle its chrome.
|
|
13
|
+
*
|
|
14
|
+
* Nothing here styles a bare `body`, `a`, `table` or `*` at the top level.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
:root {
|
|
2
19
|
color-scheme: light dark;
|
|
3
|
-
--bg: #f6f7f9; --surface: #fff; --text: #1c2024; --muted: #6b7280;
|
|
4
|
-
--border: #e5e7eb; --accent: #2563eb; --accent-text: #fff;
|
|
5
|
-
--open: #16a34a; --resolved: #6b7280; --danger: #dc2626;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
--bg: #111418; --surface: #1a1f26; --text: #e6e8ea; --muted: #9aa2ab;
|
|
10
|
-
--border: #2a313a; --accent: #3b82f6;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
.breadcrumb { margin-bottom: 12px; font-size: 13px; }
|
|
47
|
-
.flash { margin: 0 0 16px; padding: 10px 14px; border-radius: 10px; border: 1px solid var(--border);
|
|
48
|
-
background: var(--surface); }
|
|
49
|
-
.flash.alert { color: var(--danger); border-color: var(--danger); }
|
|
50
|
-
.head-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
|
|
51
|
-
.head-row h1 { margin: 0; flex: 1; }
|
|
52
|
-
.head-row form { display: inline; }
|
|
53
|
-
.status-toggle-form { flex: 0 0 auto; }
|
|
54
|
-
.status-toggle { position: relative; display: grid; grid-template-columns: 1fr 1fr; gap: 0; width: 176px;
|
|
55
|
-
min-height: 36px; padding: 3px; border-radius: 999px; border: 1px solid var(--border);
|
|
56
|
-
background: var(--bg); color: var(--muted); overflow: hidden; }
|
|
57
|
-
.status-toggle::before { content: ""; position: absolute; z-index: 0; top: 3px; bottom: 3px;
|
|
58
|
-
width: calc(50% - 3px); border-radius: 999px; background: var(--surface);
|
|
59
|
-
box-shadow: 0 1px 2px rgba(15, 23, 42, .12); transition: left .16s ease; }
|
|
60
|
-
.status-toggle.is-open::before { left: 3px; }
|
|
61
|
-
.status-toggle.is-resolved::before { left: 50%; }
|
|
62
|
-
.status-toggle span { position: relative; z-index: 1; display: flex; align-items: center; justify-content: center;
|
|
63
|
-
min-width: 0; padding: 0 10px; font-size: 13px; font-weight: 700; white-space: nowrap; }
|
|
64
|
-
.status-toggle.is-open span:first-child { color: var(--open); }
|
|
65
|
-
.status-toggle.is-resolved span:last-child { color: var(--text); }
|
|
66
|
-
.status-toggle:hover { text-decoration: none; color: var(--text); }
|
|
67
|
-
button, .button { padding: 8px 14px; border: 1px solid var(--border); border-radius: 8px; cursor: pointer;
|
|
68
|
-
background: var(--surface); color: var(--text); font: inherit; }
|
|
69
|
-
button.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-text); }
|
|
70
|
-
.case-id { color: var(--muted); font-weight: 400; font-size: 15px; }
|
|
71
|
-
.avatars { display: inline-flex; }
|
|
72
|
-
.avatars .avatar { width: 26px; height: 26px; border-radius: 50%; display: inline-flex;
|
|
73
|
-
align-items: center; justify-content: center; font-size: 10px; font-weight: 700;
|
|
74
|
-
color: #fff; border: 2px solid var(--surface); cursor: default; }
|
|
75
|
-
.avatars .avatar + .avatar { margin-left: -8px; }
|
|
76
|
-
.avatar.color-0 { background: #2563eb; } .avatar.color-1 { background: #16a34a; }
|
|
77
|
-
.avatar.color-2 { background: #d97706; } .avatar.color-3 { background: #dc2626; }
|
|
78
|
-
.avatar.color-4 { background: #7c3aed; } .avatar.color-5 { background: #0891b2; }
|
|
79
|
-
.context-line { margin: -6px 0 16px; overflow-wrap: anywhere; }
|
|
80
|
-
.thread { display: flex; flex-direction: column; gap: 4px; padding: 16px; }
|
|
81
|
-
.thread .who { font-size: 12px; color: var(--muted); margin: 10px 4px 2px; }
|
|
82
|
-
.thread .who.visitor { text-align: left; }
|
|
83
|
-
.thread .who.agent { text-align: right; }
|
|
84
|
-
.thread .msg { max-width: 78%; padding: 8px 12px; border-radius: 14px; white-space: pre-wrap;
|
|
85
|
-
overflow-wrap: anywhere; }
|
|
86
|
-
.thread .msg.visitor { align-self: flex-start; background: var(--bg); border: 1px solid var(--border);
|
|
87
|
-
border-bottom-left-radius: 4px; }
|
|
88
|
-
.thread .msg.agent { align-self: flex-end; background: var(--accent); color: var(--accent-text);
|
|
89
|
-
border-bottom-right-radius: 4px; }
|
|
90
|
-
.thread .system { align-self: center; color: var(--muted); font-size: 12px; margin: 8px 0; }
|
|
91
|
-
.thread .typing { align-self: flex-start; color: var(--muted); font-size: 12px;
|
|
92
|
-
font-style: italic; margin: 8px 4px 2px; }
|
|
93
|
-
.thread .msg .atts { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; }
|
|
94
|
-
.thread .msg .att-img img { max-width: 100%; max-height: 240px; border-radius: 8px; display: block; }
|
|
95
|
-
.thread .msg .att-audio { display: block; width: 280px; max-width: 100%; height: 36px; }
|
|
96
|
-
.thread .msg .att-file { display: inline-block; padding: 6px 10px; border-radius: 8px;
|
|
97
|
-
background: rgba(0,0,0,.06); color: inherit; font-size: 13px;
|
|
98
|
-
overflow-wrap: anywhere; }
|
|
99
|
-
.thread .msg.agent .att-file { background: rgba(255,255,255,.2); }
|
|
100
|
-
#new-messages { margin: 12px 0 0; }
|
|
101
|
-
#new-messages a { font-weight: 600; }
|
|
102
|
-
/* Unified composer box (matches the visitor widget): the reply on top, a
|
|
103
|
-
toolbar row below (tools left, send right), all inside one bordered box
|
|
104
|
-
that lights up on focus. */
|
|
105
|
-
.reply { display: flex; flex-direction: column; gap: 6px; margin: 10px 12px; padding: 8px 10px;
|
|
106
|
-
border: 1px solid var(--border); border-radius: 14px; background: var(--bg); }
|
|
107
|
-
.reply:focus-within { border-color: var(--accent); }
|
|
108
|
-
.reply textarea { -webkit-appearance: none; appearance: none; border: none; background: none;
|
|
109
|
-
resize: none; outline: none; font: inherit; color: var(--text);
|
|
110
|
-
padding: 4px 4px 0; min-height: 40px; max-height: 160px; overflow-y: auto; }
|
|
111
|
-
.reply input[type=file].reply-file { font-size: 12px; color: var(--muted); }
|
|
112
|
-
.reply-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
113
|
-
.reply-tools { display: flex; align-items: center; gap: 2px; }
|
|
114
|
-
.reply .attach { width: 32px; height: 32px; padding: 0; border: none; background: none;
|
|
115
|
-
color: var(--muted); border-radius: 8px; cursor: pointer;
|
|
116
|
-
display: flex; align-items: center; justify-content: center; }
|
|
117
|
-
.reply .attach:hover { background: var(--border); color: var(--text); }
|
|
118
|
-
.reply .attach.recording-on { color: var(--danger); }
|
|
119
|
-
.reply.is-recording .attach { display: none; }
|
|
120
|
-
.reply .chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
121
|
-
.reply .chip { display: inline-flex; align-items: center; gap: 6px; max-width: 100%;
|
|
122
|
-
padding: 4px 6px 4px 10px; border: 1px solid var(--border); border-radius: 999px;
|
|
123
|
-
background: var(--surface); font-size: 12px; }
|
|
124
|
-
.reply .chip-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 220px; }
|
|
125
|
-
.reply .chip-x { padding: 0 2px; border: none; background: none; color: var(--muted);
|
|
126
|
-
font-size: 16px; line-height: 1; cursor: pointer; }
|
|
127
|
-
.reply .chip-x:hover { color: var(--text); }
|
|
128
|
-
.reply .recording { display: inline-flex; align-items: center; gap: 8px; height: 32px;
|
|
129
|
-
padding: 0 4px 0 8px; border: 1px solid var(--border); border-radius: 999px;
|
|
130
|
-
background: var(--surface); color: var(--muted); font-size: 12px;
|
|
131
|
-
white-space: nowrap; }
|
|
132
|
-
.reply .recording[hidden] { display: none; }
|
|
133
|
-
.reply .recording-status { display: inline-grid; grid-template-columns: 8px 24px 34px;
|
|
134
|
-
align-items: center; gap: 6px; font-variant-numeric: tabular-nums; }
|
|
135
|
-
.reply .recording-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--danger);
|
|
136
|
-
box-shadow: 0 0 0 3px rgba(220,38,38,.12); }
|
|
137
|
-
.reply .recording-rec { font-size: 10px; font-weight: 700; letter-spacing: .04em; color: var(--danger); }
|
|
138
|
-
.reply .recording-actions { display: flex; align-items: center; gap: 4px; }
|
|
139
|
-
.reply .recording-actions button { width: 24px; height: 24px; padding: 0; border: none;
|
|
140
|
-
border-radius: 50%; background: none; color: var(--muted);
|
|
141
|
-
display: flex; align-items: center; justify-content: center; }
|
|
142
|
-
.reply .recording-actions button:hover { background: var(--border); color: var(--text); }
|
|
143
|
-
.reply .recording-actions .stop-recording { background: var(--danger); color: #fff; }
|
|
144
|
-
.reply .recording-actions .stop-recording:hover { background: #b91c1c; color: #fff; }
|
|
145
|
-
.reply .send { width: 32px; min-width: 32px; height: 32px; padding: 0; border: none; border-radius: 50%;
|
|
146
|
-
background: var(--accent); color: var(--accent-text); cursor: pointer;
|
|
147
|
-
display: flex; align-items: center; justify-content: center; }
|
|
148
|
-
/* Desktop inbox: a durable conversation list on the left and the active
|
|
149
|
-
thread on the right, with each pane owning its own scroll. */
|
|
150
|
-
.lvc-inbox, .lvc-inbox .container { height: 100vh; height: 100dvh; }
|
|
151
|
-
.lvc-inbox { overflow: hidden; }
|
|
152
|
-
.lvc-inbox .container { max-width: 1280px; display: flex; flex-direction: column; padding-bottom: 16px; }
|
|
153
|
-
.lvc-inbox .container > h1 { flex: 0 0 auto; margin-bottom: 14px; }
|
|
154
|
-
.inbox-shell { flex: 1 1 auto; min-height: 0; display: grid; grid-template-columns: minmax(320px, 380px) 1fr;
|
|
155
|
-
gap: 16px; }
|
|
156
|
-
.inbox-sidebar { min-width: 0; min-height: 0; display: flex; flex-direction: column;
|
|
157
|
-
background: var(--surface); border: 1px solid var(--border);
|
|
158
|
-
border-radius: 8px; overflow: hidden; }
|
|
159
|
-
.inbox-sidebar .tabs { flex: 0 0 auto; margin: 12px 12px 0; padding: 0; background: transparent;
|
|
160
|
-
border: 0; border-radius: 0; gap: 6px; flex-wrap: nowrap; }
|
|
161
|
-
.inbox-sidebar .tabs a { flex: 1 1 0; position: relative; display: flex; align-items: center;
|
|
162
|
-
justify-content: center; gap: 6px; min-height: 34px; padding: 5px 12px 8px;
|
|
163
|
-
border-radius: 8px; color: var(--muted); font-weight: 600; }
|
|
164
|
-
.inbox-sidebar .tabs a.active { color: var(--text); border: 0; background: transparent; box-shadow: none; margin: 0; }
|
|
165
|
-
.inbox-sidebar .tabs a.active::after { content: ""; position: absolute; left: 24px; right: 24px; bottom: 0;
|
|
166
|
-
height: 2px; border-radius: 999px; background: var(--accent); }
|
|
167
|
-
.inbox-sidebar .tabs a:not(.active):hover { text-decoration: none; color: var(--text); background: var(--bg); }
|
|
168
|
-
.inbox-sidebar .tabs .count { margin-left: 0; background: color-mix(in srgb, var(--muted) 14%, transparent); }
|
|
169
|
-
.inbox-sidebar .tabs a.active .count { background: var(--border); }
|
|
170
|
-
.inbox-sidebar .filters { flex: 0 0 auto; margin: 0; padding: 12px; border-bottom: 1px solid var(--border); }
|
|
171
|
-
.inbox-sidebar .filters input[type=search] { max-width: none; }
|
|
172
|
-
.conversation-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
|
|
173
|
-
.conversation-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; padding: 13px 14px;
|
|
174
|
-
color: var(--text); border-bottom: 1px solid var(--border); }
|
|
175
|
-
.conversation-row:hover { text-decoration: none; background: var(--bg); }
|
|
176
|
-
.conversation-row.active { background: color-mix(in srgb, var(--accent) 9%, var(--surface));
|
|
177
|
-
box-shadow: inset 3px 0 0 var(--accent); }
|
|
178
|
-
.conversation-row.unread .conversation-name, .conversation-row.unread .conversation-preview { font-weight: 700; }
|
|
179
|
-
.conversation-main { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
|
|
180
|
-
.conversation-topline, .conversation-meta { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
|
|
181
|
-
.conversation-name, .conversation-preview, .conversation-meta span { overflow: hidden; text-overflow: ellipsis;
|
|
182
|
-
white-space: nowrap; }
|
|
183
|
-
.conversation-name { font-weight: 600; }
|
|
184
|
-
.conversation-time { margin-left: auto; white-space: nowrap; }
|
|
185
|
-
.conversation-preview { display: block; }
|
|
186
|
-
.conversation-side { display: flex; flex-direction: column; align-items: flex-end; justify-content: space-between;
|
|
187
|
-
gap: 8px; padding-top: 1px; }
|
|
188
|
-
.conversation-side .avatars { min-height: 26px; }
|
|
189
|
-
.inbox-sidebar .pager { flex: 0 0 auto; margin: 0; padding: 10px 12px; border-top: 1px solid var(--border); }
|
|
190
|
-
.inbox-thread { min-width: 0; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
|
|
191
|
-
.inbox-thread .conversation-panel { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column;
|
|
192
|
-
padding: 0; }
|
|
193
|
-
.conversation-panel .head-row, .conversation-panel .context-line { flex: 0 0 auto; }
|
|
194
|
-
.conversation-panel .card { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column;
|
|
195
|
-
border-radius: 8px; }
|
|
196
|
-
.conversation-panel .thread { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
|
|
197
|
-
.conversation-panel .reply { flex: 0 0 auto; }
|
|
198
|
-
.empty-state { margin: auto; max-width: 320px; padding: 24px; text-align: center; }
|
|
199
|
-
.empty-state h2 { margin: 0 0 6px; font-size: 18px; }
|
|
200
|
-
.empty-state p { margin: 0; }
|
|
201
|
-
.mobile-back { display: none; }
|
|
202
|
-
/* Conversation page: fill the viewport and scroll the THREAD, not the
|
|
203
|
-
page — so a reply never jumps the page, and the composer stays put. */
|
|
204
|
-
.lvc-convo, .lvc-convo .container { height: 100vh; height: 100dvh; }
|
|
205
|
-
.lvc-convo { overflow: hidden; }
|
|
206
|
-
.lvc-convo .container { display: flex; flex-direction: column; padding-bottom: 16px; }
|
|
207
|
-
.lvc-convo .breadcrumb, .lvc-convo .head-row, .lvc-convo .context-line,
|
|
20
|
+
--lvc-bg: #f6f7f9; --lvc-surface: #fff; --lvc-text: #1c2024; --lvc-muted: #6b7280;
|
|
21
|
+
--lvc-border: #e5e7eb; --lvc-accent: #2563eb; --lvc-accent-text: #fff;
|
|
22
|
+
--lvc-open: #16a34a; --lvc-resolved: #6b7280; --lvc-danger: #dc2626;
|
|
23
|
+
}
|
|
24
|
+
@media (prefers-color-scheme: dark) {
|
|
25
|
+
:root {
|
|
26
|
+
--lvc-bg: #111418; --lvc-surface: #1a1f26; --lvc-text: #e6e8ea; --lvc-muted: #9aa2ab;
|
|
27
|
+
--lvc-border: #2a313a; --lvc-accent: #3b82f6;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
@media (max-width: 760px) {
|
|
31
|
+
.lvc-page { font-size: 14px; }
|
|
32
|
+
.lvc-inbox { overflow: auto; }
|
|
33
|
+
.lvc-inbox, .lvc-inbox .container { min-height: 100vh; height: auto; }
|
|
34
|
+
.lvc-inbox .container { padding-bottom: 10px; }
|
|
35
|
+
.lvc-dashboard {
|
|
36
|
+
& .container { padding: 14px 10px 24px; }
|
|
37
|
+
& .inbox-shell { display: block; min-height: calc(100vh - 62px); }
|
|
38
|
+
& .inbox-sidebar,
|
|
39
|
+
& .inbox-thread { min-height: calc(100vh - 62px); }
|
|
40
|
+
& .inbox-shell.has-selected .inbox-sidebar { display: none; }
|
|
41
|
+
& .inbox-shell:not(.has-selected) .inbox-thread { display: none; }
|
|
42
|
+
& .conversation-list { overflow: visible; }
|
|
43
|
+
& .conversation-row { padding: 12px; }
|
|
44
|
+
& .conversation-meta { display: none; }
|
|
45
|
+
& .conversation-panel { min-height: calc(100vh - 64px); }
|
|
46
|
+
& .inbox-thread .conversation-panel { padding: 0; }
|
|
47
|
+
& .mobile-back { display: block; flex: 0 0 auto; margin: 10px 10px 0; font-size: 13px; }
|
|
48
|
+
& .conversation-panel .head-row h1 { font-size: 18px; }
|
|
49
|
+
& .status-toggle { width: 152px; min-height: 34px; }
|
|
50
|
+
& .status-toggle span { padding: 0 7px; font-size: 12px; }
|
|
51
|
+
& .thread .msg { max-width: 88%; }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* Page frame — the gem's own layout only. */
|
|
56
|
+
.lvc-page { margin: 0; background: var(--lvc-bg); color: var(--lvc-text); font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; }
|
|
57
|
+
.lvc-inbox { overflow: hidden; }
|
|
58
|
+
.lvc-inbox .container { max-width: 1280px; display: flex; flex-direction: column; padding-bottom: 16px; }
|
|
59
|
+
.lvc-inbox .container > h1 { flex: 0 0 auto; margin-bottom: 14px; }
|
|
60
|
+
.lvc-convo { overflow: hidden; }
|
|
61
|
+
.lvc-convo .container { display: flex; flex-direction: column; padding-bottom: 16px; }
|
|
62
|
+
.lvc-convo .breadcrumb, .lvc-convo .head-row, .lvc-convo .context-line,
|
|
208
63
|
.lvc-convo .flash { flex: 0 0 auto; }
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
64
|
+
.lvc-convo .conversation-panel { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
|
|
65
|
+
.lvc-convo .card { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
|
|
66
|
+
.lvc-convo .thread { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
|
|
67
|
+
.lvc-convo .reply { flex: 0 0 auto; }
|
|
68
|
+
|
|
69
|
+
/* Dashboard components — any layout. */
|
|
70
|
+
.lvc-dashboard {
|
|
71
|
+
& * { box-sizing: border-box; }
|
|
72
|
+
& a { color: var(--lvc-accent); text-decoration: none; }
|
|
73
|
+
& a:hover { text-decoration: underline; }
|
|
74
|
+
& .container { max-width: 860px; margin: 0 auto; padding: 24px 16px 64px; }
|
|
75
|
+
& h1 { font-size: 22px; margin: 0 0 16px; }
|
|
76
|
+
& .tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--lvc-border); margin-bottom: 16px; flex-wrap: wrap; }
|
|
77
|
+
& .tabs a { padding: 8px 14px; border-radius: 8px 8px 0 0; color: var(--lvc-muted); }
|
|
78
|
+
& .tabs a.active { color: var(--lvc-text); font-weight: 600; border: 1px solid var(--lvc-border); border-bottom: 2px solid var(--lvc-surface); background: var(--lvc-surface); margin-bottom: -1px; }
|
|
79
|
+
& .tabs a:hover { text-decoration: none; color: var(--lvc-text); }
|
|
80
|
+
& .count { display: inline-block; min-width: 20px; padding: 0 6px; margin-left: 4px; border-radius: 999px; background: var(--lvc-border); font-size: 12px; text-align: center; }
|
|
81
|
+
& .filters { margin-bottom: 16px; display: flex; gap: 8px; }
|
|
82
|
+
& .filters input[type=search] { flex: 1; max-width: 320px; padding: 6px 10px; border: 1px solid var(--lvc-border); border-radius: 8px; background: var(--lvc-surface); color: var(--lvc-text); font: inherit; }
|
|
83
|
+
& .card { background: var(--lvc-surface); border: 1px solid var(--lvc-border); border-radius: 12px; overflow: hidden; }
|
|
84
|
+
& .card.pad { padding: 16px; }
|
|
85
|
+
& table { width: 100%; border-collapse: collapse; }
|
|
86
|
+
& th,
|
|
87
|
+
& td { padding: 10px 14px; text-align: left; vertical-align: top; }
|
|
88
|
+
& th { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--lvc-muted); border-bottom: 1px solid var(--lvc-border); }
|
|
89
|
+
& tr + tr td { border-top: 1px solid var(--lvc-border); }
|
|
90
|
+
& tr.unread td { font-weight: 600; }
|
|
91
|
+
& .badge { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 12px; font-weight: 600; color: #fff; white-space: nowrap; }
|
|
92
|
+
& .badge.status-open { background: var(--lvc-open); }
|
|
93
|
+
& .badge.status-resolved { background: var(--lvc-resolved); }
|
|
94
|
+
& .badge.unread-count { background: var(--lvc-danger); }
|
|
95
|
+
& .muted { color: var(--lvc-muted); font-size: 13px; }
|
|
96
|
+
& .pager { margin-top: 16px; display: flex; gap: 16px; }
|
|
97
|
+
& .empty { padding: 48px 16px; text-align: center; color: var(--lvc-muted); }
|
|
98
|
+
& .breadcrumb { margin-bottom: 12px; font-size: 13px; }
|
|
99
|
+
& .flash { margin: 0 0 16px; padding: 10px 14px; border-radius: 10px; border: 1px solid var(--lvc-border); background: var(--lvc-surface); }
|
|
100
|
+
& .flash.alert { color: var(--lvc-danger); border-color: var(--lvc-danger); }
|
|
101
|
+
& .head-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
|
|
102
|
+
& .head-row h1 { margin: 0; flex: 1; }
|
|
103
|
+
& .head-row form { display: inline; }
|
|
104
|
+
& .status-toggle-form { flex: 0 0 auto; }
|
|
105
|
+
& .status-toggle { position: relative; display: grid; grid-template-columns: 1fr 1fr; gap: 0; width: 176px; min-height: 36px; padding: 3px; border-radius: 999px; border: 1px solid var(--lvc-border); background: var(--lvc-bg); color: var(--lvc-muted); overflow: hidden; }
|
|
106
|
+
& .status-toggle::before { content: ""; position: absolute; z-index: 0; top: 3px; bottom: 3px; width: calc(50% - 3px); border-radius: 999px; background: var(--lvc-surface); box-shadow: 0 1px 2px rgba(15, 23, 42, .12); transition: left .16s ease; }
|
|
107
|
+
& .status-toggle.is-open::before { left: 3px; }
|
|
108
|
+
& .status-toggle.is-resolved::before { left: 50%; }
|
|
109
|
+
& .status-toggle span { position: relative; z-index: 1; display: flex; align-items: center; justify-content: center; min-width: 0; padding: 0 10px; font-size: 13px; font-weight: 700; white-space: nowrap; }
|
|
110
|
+
& .status-toggle.is-open span:first-child { color: var(--lvc-open); }
|
|
111
|
+
& .status-toggle.is-resolved span:last-child { color: var(--lvc-text); }
|
|
112
|
+
& .status-toggle:hover { text-decoration: none; color: var(--lvc-text); }
|
|
113
|
+
& button,
|
|
114
|
+
& .button { padding: 8px 14px; border: 1px solid var(--lvc-border); border-radius: 8px; cursor: pointer; background: var(--lvc-surface); color: var(--lvc-text); font: inherit; }
|
|
115
|
+
& button.primary { background: var(--lvc-accent); border-color: var(--lvc-accent); color: var(--lvc-accent-text); }
|
|
116
|
+
& .case-id { color: var(--lvc-muted); font-weight: 400; font-size: 15px; }
|
|
117
|
+
& .avatars { display: inline-flex; }
|
|
118
|
+
& .avatars .avatar { width: 26px; height: 26px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 700; color: #fff; border: 2px solid var(--lvc-surface); cursor: default; }
|
|
119
|
+
& .avatars .avatar + .avatar { margin-left: -8px; }
|
|
120
|
+
& .avatar.color-0 { background: #2563eb; }
|
|
121
|
+
& .avatar.color-1 { background: #16a34a; }
|
|
122
|
+
& .avatar.color-2 { background: #d97706; }
|
|
123
|
+
& .avatar.color-3 { background: #dc2626; }
|
|
124
|
+
& .avatar.color-4 { background: #7c3aed; }
|
|
125
|
+
& .avatar.color-5 { background: #0891b2; }
|
|
126
|
+
& .context-line { margin: -6px 0 16px; overflow-wrap: anywhere; }
|
|
127
|
+
& .thread { display: flex; flex-direction: column; gap: 4px; padding: 16px; }
|
|
128
|
+
& .thread .who { font-size: 12px; color: var(--lvc-muted); margin: 10px 4px 2px; }
|
|
129
|
+
& .thread .who.visitor { text-align: left; }
|
|
130
|
+
& .thread .who.agent { text-align: right; }
|
|
131
|
+
& .thread .msg { max-width: 78%; padding: 8px 12px; border-radius: 14px; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
132
|
+
& .thread .msg.visitor { align-self: flex-start; background: var(--lvc-bg); border: 1px solid var(--lvc-border); border-bottom-left-radius: 4px; }
|
|
133
|
+
& .thread .msg.agent { align-self: flex-end; background: var(--lvc-accent); color: var(--lvc-accent-text); border-bottom-right-radius: 4px; }
|
|
134
|
+
& .thread .system { align-self: center; color: var(--lvc-muted); font-size: 12px; margin: 8px 0; }
|
|
135
|
+
& .thread .typing { align-self: flex-start; color: var(--lvc-muted); font-size: 12px; font-style: italic; margin: 8px 4px 2px; }
|
|
136
|
+
& .thread .msg .atts { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; }
|
|
137
|
+
& .thread .msg .att-img img { max-width: 100%; max-height: 240px; border-radius: 8px; display: block; }
|
|
138
|
+
& .thread .msg .att-audio { display: block; width: 280px; max-width: 100%; height: 36px; }
|
|
139
|
+
& .thread .msg .att-file { display: inline-block; padding: 6px 10px; border-radius: 8px; background: rgba(0,0,0,.06); color: inherit; font-size: 13px; overflow-wrap: anywhere; }
|
|
140
|
+
& .thread .msg.agent .att-file { background: rgba(255,255,255,.2); }
|
|
141
|
+
& #new-messages { margin: 12px 0 0; }
|
|
142
|
+
& #new-messages a { font-weight: 600; }
|
|
143
|
+
& /* Unified composer box (matches the visitor widget): the reply on top,
|
|
144
|
+
& a
|
|
145
|
+
toolbar row below (tools left,
|
|
146
|
+
& send right),
|
|
147
|
+
& all inside one bordered box
|
|
148
|
+
that lights up on focus. */
|
|
149
|
+
.reply { display: flex; flex-direction: column; gap: 6px; margin: 10px 12px; padding: 8px 10px; border: 1px solid var(--lvc-border); border-radius: 14px; background: var(--lvc-bg); }
|
|
150
|
+
& .reply:focus-within { border-color: var(--lvc-accent); }
|
|
151
|
+
& .reply textarea { -webkit-appearance: none; appearance: none; border: none; background: none; resize: none; outline: none; font: inherit; color: var(--lvc-text); padding: 4px 4px 0; min-height: 40px; max-height: 160px; overflow-y: auto; }
|
|
152
|
+
& .reply input[type=file].reply-file { font-size: 12px; color: var(--lvc-muted); }
|
|
153
|
+
& .reply-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
154
|
+
& .reply-tools { display: flex; align-items: center; gap: 2px; }
|
|
155
|
+
& .reply .attach { width: 32px; height: 32px; padding: 0; border: none; background: none; color: var(--lvc-muted); border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center; }
|
|
156
|
+
& .reply .attach:hover { background: var(--lvc-border); color: var(--lvc-text); }
|
|
157
|
+
& .reply .attach.recording-on { color: var(--lvc-danger); }
|
|
158
|
+
& .reply.is-recording .attach { display: none; }
|
|
159
|
+
& .reply .chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
160
|
+
& .reply .chip { display: inline-flex; align-items: center; gap: 6px; max-width: 100%; padding: 4px 6px 4px 10px; border: 1px solid var(--lvc-border); border-radius: 999px; background: var(--lvc-surface); font-size: 12px; }
|
|
161
|
+
& .reply .chip-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 220px; }
|
|
162
|
+
& .reply .chip-x { padding: 0 2px; border: none; background: none; color: var(--lvc-muted); font-size: 16px; line-height: 1; cursor: pointer; }
|
|
163
|
+
& .reply .chip-x:hover { color: var(--lvc-text); }
|
|
164
|
+
& .reply .recording { display: inline-flex; align-items: center; gap: 8px; height: 32px; padding: 0 4px 0 8px; border: 1px solid var(--lvc-border); border-radius: 999px; background: var(--lvc-surface); color: var(--lvc-muted); font-size: 12px; white-space: nowrap; }
|
|
165
|
+
& .reply .recording[hidden] { display: none; }
|
|
166
|
+
& .reply .recording-status { display: inline-grid; grid-template-columns: 8px 24px 34px; align-items: center; gap: 6px; font-variant-numeric: tabular-nums; }
|
|
167
|
+
& .reply .recording-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--lvc-danger); box-shadow: 0 0 0 3px rgba(220,38,38,.12); }
|
|
168
|
+
& .reply .recording-rec { font-size: 10px; font-weight: 700; letter-spacing: .04em; color: var(--lvc-danger); }
|
|
169
|
+
& .reply .recording-actions { display: flex; align-items: center; gap: 4px; }
|
|
170
|
+
& .reply .recording-actions button { width: 24px; height: 24px; padding: 0; border: none; border-radius: 50%; background: none; color: var(--lvc-muted); display: flex; align-items: center; justify-content: center; }
|
|
171
|
+
& .reply .recording-actions button:hover { background: var(--lvc-border); color: var(--lvc-text); }
|
|
172
|
+
& .reply .recording-actions .stop-recording { background: var(--lvc-danger); color: #fff; }
|
|
173
|
+
& .reply .recording-actions .stop-recording:hover { background: #b91c1c; color: #fff; }
|
|
174
|
+
& .reply .send { width: 32px; min-width: 32px; height: 32px; padding: 0; border: none; border-radius: 50%; background: var(--lvc-accent); color: var(--lvc-accent-text); cursor: pointer; display: flex; align-items: center; justify-content: center; }
|
|
175
|
+
& /* Desktop inbox: a durable conversation list on the left and the active
|
|
176
|
+
thread on the right,
|
|
177
|
+
& with each pane owning its own scroll. */
|
|
178
|
+
.lvc-inbox,
|
|
179
|
+
& .lvc-inbox .container { height: 100vh; height: 100dvh; }
|
|
180
|
+
& .inbox-shell { flex: 1 1 auto; min-height: 0; display: grid; grid-template-columns: minmax(320px, 380px) 1fr; gap: 16px; }
|
|
181
|
+
& .inbox-sidebar { min-width: 0; min-height: 0; display: flex; flex-direction: column; background: var(--lvc-surface); border: 1px solid var(--lvc-border); border-radius: 8px; overflow: hidden; }
|
|
182
|
+
& .inbox-sidebar .tabs { flex: 0 0 auto; margin: 12px 12px 0; padding: 0; background: transparent; border: 0; border-radius: 0; gap: 6px; flex-wrap: nowrap; }
|
|
183
|
+
& .inbox-sidebar .tabs a { flex: 1 1 0; position: relative; display: flex; align-items: center; justify-content: center; gap: 6px; min-height: 34px; padding: 5px 12px 8px; border-radius: 8px; color: var(--lvc-muted); font-weight: 600; }
|
|
184
|
+
& .inbox-sidebar .tabs a.active { color: var(--lvc-text); border: 0; background: transparent; box-shadow: none; margin: 0; }
|
|
185
|
+
& .inbox-sidebar .tabs a.active::after { content: ""; position: absolute; left: 24px; right: 24px; bottom: 0; height: 2px; border-radius: 999px; background: var(--lvc-accent); }
|
|
186
|
+
& .inbox-sidebar .tabs a:not(.active):hover { text-decoration: none; color: var(--lvc-text); background: var(--lvc-bg); }
|
|
187
|
+
& .inbox-sidebar .tabs .count { margin-left: 0; background: color-mix(in srgb, var(--lvc-muted) 14%, transparent); }
|
|
188
|
+
& .inbox-sidebar .tabs a.active .count { background: var(--lvc-border); }
|
|
189
|
+
& .inbox-sidebar .filters { flex: 0 0 auto; margin: 0; padding: 12px; border-bottom: 1px solid var(--lvc-border); }
|
|
190
|
+
& .inbox-sidebar .filters input[type=search] { max-width: none; }
|
|
191
|
+
& .conversation-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
|
|
192
|
+
& .conversation-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; padding: 13px 14px; color: var(--lvc-text); border-bottom: 1px solid var(--lvc-border); }
|
|
193
|
+
& .conversation-row:hover { text-decoration: none; background: var(--lvc-bg); }
|
|
194
|
+
& .conversation-row.active { background: color-mix(in srgb, var(--lvc-accent) 9%, var(--lvc-surface)); box-shadow: inset 3px 0 0 var(--lvc-accent); }
|
|
195
|
+
& .conversation-row.unread .conversation-name,
|
|
196
|
+
& .conversation-row.unread .conversation-preview { font-weight: 700; }
|
|
197
|
+
& .conversation-main { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
|
|
198
|
+
& .conversation-topline,
|
|
199
|
+
& .conversation-meta { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
|
|
200
|
+
& .conversation-name,
|
|
201
|
+
& .conversation-preview,
|
|
202
|
+
& .conversation-meta span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
203
|
+
& .conversation-name { font-weight: 600; }
|
|
204
|
+
& .conversation-time { margin-left: auto; white-space: nowrap; }
|
|
205
|
+
& .conversation-preview { display: block; }
|
|
206
|
+
& .conversation-side { display: flex; flex-direction: column; align-items: flex-end; justify-content: space-between; gap: 8px; padding-top: 1px; }
|
|
207
|
+
& .conversation-side .avatars { min-height: 26px; }
|
|
208
|
+
& .inbox-sidebar .pager { flex: 0 0 auto; margin: 0; padding: 10px 12px; border-top: 1px solid var(--lvc-border); }
|
|
209
|
+
& .inbox-thread { min-width: 0; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
|
|
210
|
+
& .inbox-thread .conversation-panel { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; padding: 0; }
|
|
211
|
+
& .conversation-panel .head-row,
|
|
212
|
+
& .conversation-panel .context-line { flex: 0 0 auto; }
|
|
213
|
+
& .conversation-panel .card { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; border-radius: 8px; }
|
|
214
|
+
& .conversation-panel .thread { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
|
|
215
|
+
& .conversation-panel .reply { flex: 0 0 auto; }
|
|
216
|
+
& .empty-state { margin: auto; max-width: 320px; padding: 24px; text-align: center; }
|
|
217
|
+
& .empty-state h2 { margin: 0 0 6px; font-size: 18px; }
|
|
218
|
+
& .empty-state p { margin: 0; }
|
|
219
|
+
& .mobile-back { display: none; }
|
|
220
|
+
& /* Conversation page: fill the viewport and scroll the THREAD,
|
|
221
|
+
& not the
|
|
222
|
+
page — so a reply never jumps the page,
|
|
223
|
+
& and the composer stays put. */
|
|
224
|
+
.lvc-convo,
|
|
225
|
+
& .lvc-convo .container { height: 100vh; height: 100dvh; }
|
|
226
|
+
}
|
data/lib/livechat/version.rb
CHANGED
data/lib/livechat.rb
CHANGED
|
@@ -29,6 +29,13 @@ module Livechat
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
# Can this request work the inbox? Checked by every inbox action.
|
|
32
|
+
# The class Livechat::DashboardController inherits from. Resolved on every
|
|
33
|
+
# call rather than memoized, so a host that reassigns base_controller_class
|
|
34
|
+
# in a reloadable initializer is not pinned to a stale, unloaded constant.
|
|
35
|
+
def base_controller
|
|
36
|
+
config.base_controller_class.to_s.constantize
|
|
37
|
+
end
|
|
38
|
+
|
|
32
39
|
def agent?(request)
|
|
33
40
|
!!config.authorize_agent.call(request)
|
|
34
41
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: livechat
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yaroslav Shmarov
|
|
@@ -43,9 +43,11 @@ files:
|
|
|
43
43
|
- MIT-LICENSE
|
|
44
44
|
- README.md
|
|
45
45
|
- Rakefile
|
|
46
|
+
- app/controllers/concerns/livechat/request_context.rb
|
|
46
47
|
- app/controllers/livechat/application_controller.rb
|
|
47
48
|
- app/controllers/livechat/attachments_controller.rb
|
|
48
49
|
- app/controllers/livechat/conversations_controller.rb
|
|
50
|
+
- app/controllers/livechat/dashboard_controller.rb
|
|
49
51
|
- app/controllers/livechat/messages_controller.rb
|
|
50
52
|
- app/controllers/livechat/visitor_controller.rb
|
|
51
53
|
- app/controllers/livechat/widgets_controller.rb
|
|
@@ -61,6 +63,7 @@ files:
|
|
|
61
63
|
- app/views/livechat/conversations/show.html.erb
|
|
62
64
|
- app/views/livechat/mailer/new_agent_reply.text.erb
|
|
63
65
|
- app/views/livechat/mailer/new_visitor_message.text.erb
|
|
66
|
+
- app/views/livechat/shared/_dashboard.html.erb
|
|
64
67
|
- config/locales/livechat.ar.yml
|
|
65
68
|
- config/locales/livechat.bg.yml
|
|
66
69
|
- config/locales/livechat.bn.yml
|
|
@@ -91,6 +94,7 @@ files:
|
|
|
91
94
|
- lib/generators/livechat/install/install_generator.rb
|
|
92
95
|
- lib/generators/livechat/install/templates/create_livechat_tables.rb.tt
|
|
93
96
|
- lib/generators/livechat/install/templates/initializer.rb
|
|
97
|
+
- lib/generators/livechat/migration_helpers.rb
|
|
94
98
|
- lib/livechat.rb
|
|
95
99
|
- lib/livechat/channels.rb
|
|
96
100
|
- lib/livechat/configuration.rb
|