livechat 0.3.4 → 0.3.6
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 +16 -0
- data/app/controllers/livechat/conversations_controller.rb +29 -3
- data/app/views/livechat/conversations/show.html.erb +3 -5
- data/config/locales/livechat.ar.yml +0 -1
- data/config/locales/livechat.bg.yml +0 -1
- data/config/locales/livechat.bn.yml +0 -1
- data/config/locales/livechat.de.yml +0 -1
- data/config/locales/livechat.el.yml +0 -1
- data/config/locales/livechat.en.yml +0 -1
- data/config/locales/livechat.es.yml +0 -1
- data/config/locales/livechat.fr.yml +0 -1
- data/config/locales/livechat.hi.yml +0 -1
- data/config/locales/livechat.hr.yml +0 -1
- data/config/locales/livechat.id.yml +0 -1
- data/config/locales/livechat.it.yml +0 -1
- data/config/locales/livechat.ja.yml +0 -1
- data/config/locales/livechat.ko.yml +0 -1
- data/config/locales/livechat.lb.yml +0 -1
- data/config/locales/livechat.nl.yml +0 -1
- data/config/locales/livechat.pl.yml +0 -1
- data/config/locales/livechat.pt.yml +0 -1
- data/config/locales/livechat.ro.yml +0 -1
- data/config/locales/livechat.ru.yml +0 -1
- data/config/locales/livechat.th.yml +0 -1
- data/config/locales/livechat.tr.yml +0 -1
- data/config/locales/livechat.uk.yml +0 -1
- data/config/locales/livechat.ur.yml +0 -1
- data/config/locales/livechat.vi.yml +0 -1
- data/config/locales/livechat.zh-CN.yml +0 -1
- data/lib/livechat/dashboard.js +40 -8
- data/lib/livechat/version.rb +1 -1
- data/lib/livechat/widget.js +10 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 68b007c7a72520ae1b1732ec24961ed5ca88b166fbdc30aac082b2bf711346bd
|
|
4
|
+
data.tar.gz: 3d3dbc292f4115eb19aa22d295e5f873707db12df684de803c72dd912eeb82ac
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f53035e00b636c3abf5cda271f3e2468cd65c469210998c8b43c6f2a60b3598ccb59d40d8193c8321fc1c516b4cee2e2941867f8320f5ded10fc12ec8091df48
|
|
7
|
+
data.tar.gz: 4b13d777fdd2ba4d9b2277ad9af643adc57715f221d471542ff3d84278bc802fa3fb4dd182798e34129e061ba8581b3f81feb9bca679346e40c54a43fb54a77a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.6
|
|
4
|
+
|
|
5
|
+
- The opening greeting is now styled as a support message bubble (with a
|
|
6
|
+
generic "Support" label) instead of a muted line, so the panel reads like
|
|
7
|
+
a warmly-opened conversation. Deliberately generic — no fake individual,
|
|
8
|
+
avatar, or presence — and still client-only (never stored, never in the
|
|
9
|
+
agent inbox).
|
|
10
|
+
|
|
11
|
+
## 0.3.5
|
|
12
|
+
|
|
13
|
+
- The inbox thread now appends new messages live instead of showing a
|
|
14
|
+
"New messages — refresh" link — no page reload, so an agent's half-written
|
|
15
|
+
reply is never lost, and scroll position is kept unless you're at the
|
|
16
|
+
bottom. Visitor messages are marked read as they stream in. Still polling
|
|
17
|
+
(no Action Cable), still draft-safe.
|
|
18
|
+
|
|
3
19
|
## 0.3.4
|
|
4
20
|
|
|
5
21
|
- Docs: show how to wire `current_user` with Rails 8's built-in authentication
|
|
@@ -46,10 +46,20 @@ module Livechat
|
|
|
46
46
|
@conversation.mark_read_for_agent!
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
# Polled by dashboard.js
|
|
49
|
+
# Polled by dashboard.js on the open thread: returns messages newer than
|
|
50
|
+
# ?after=<id> so they can be appended live — no reload, so an agent's
|
|
51
|
+
# half-written reply is never lost. Marks the visitor's messages read,
|
|
52
|
+
# since the agent is looking right at them.
|
|
50
53
|
def poll
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
messages = @conversation.messages.chronological
|
|
55
|
+
messages = messages.after_id(params[:after]) if params[:after].present?
|
|
56
|
+
messages = messages.to_a
|
|
57
|
+
@conversation.mark_read_for_agent! if messages.any?(&:visitor?)
|
|
58
|
+
|
|
59
|
+
render json: {
|
|
60
|
+
status: @conversation.status,
|
|
61
|
+
messages: messages.map { |message| thread_message_json(message) }
|
|
62
|
+
}
|
|
53
63
|
end
|
|
54
64
|
|
|
55
65
|
def resolve
|
|
@@ -68,6 +78,22 @@ module Livechat
|
|
|
68
78
|
@conversation = Conversation.find(params[:id])
|
|
69
79
|
end
|
|
70
80
|
|
|
81
|
+
# The fields dashboard.js needs to render a message bubble, matching the
|
|
82
|
+
# thread partial: system events carry a ready localized line; visitor and
|
|
83
|
+
# agent messages carry a display name (for the author header) and body.
|
|
84
|
+
def thread_message_json(message)
|
|
85
|
+
if message.system?
|
|
86
|
+
{ id: message.id, author: 'system',
|
|
87
|
+
text: t("livechat.events.#{message.event}", agent: message.agent_label,
|
|
88
|
+
default: "%{agent} #{message.event} the conversation"),
|
|
89
|
+
at: message.created_at.to_fs(:short) }
|
|
90
|
+
else
|
|
91
|
+
{ id: message.id, author: message.author_type,
|
|
92
|
+
name: message.agent? ? message.agent_label : @conversation.display_name,
|
|
93
|
+
body: message.body, at: message.created_at.to_fs(:short) }
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
71
97
|
# Case-insensitive match on who the visitor is or anything anyone wrote.
|
|
72
98
|
# LOWER(...) LIKE is deliberately plain SQL — portable across SQLite,
|
|
73
99
|
# PostgreSQL and MySQL alike (feedback_engine's proven approach).
|
|
@@ -30,9 +30,11 @@
|
|
|
30
30
|
</p>
|
|
31
31
|
|
|
32
32
|
<div class="card">
|
|
33
|
+
<% last = @messages.last %>
|
|
33
34
|
<div class="thread" id="thread"
|
|
34
35
|
data-poll-url="<%= poll_conversation_path(@conversation) %>"
|
|
35
|
-
data-latest="<%=
|
|
36
|
+
data-latest="<%= last&.id.to_i %>"
|
|
37
|
+
data-last-author="<%= (last && !last.system?) ? "#{last.author_type}:#{last.agent_label}" : '' %>">
|
|
36
38
|
<% previous_author = nil %>
|
|
37
39
|
<% @messages.each do |message| %>
|
|
38
40
|
<% if message.system? %>
|
|
@@ -56,10 +58,6 @@
|
|
|
56
58
|
<% end %>
|
|
57
59
|
</div>
|
|
58
60
|
|
|
59
|
-
<p id="new-messages" class="system" hidden>
|
|
60
|
-
<a href=""><%= t('livechat.dashboard.new_messages', default: 'New messages — refresh') %></a>
|
|
61
|
-
</p>
|
|
62
|
-
|
|
63
61
|
<%= form_with url: conversation_messages_path(@conversation), method: :post, class: 'reply' do |form| %>
|
|
64
62
|
<%= form.text_area :body, rows: 2, autofocus: true, required: true,
|
|
65
63
|
placeholder: t('livechat.dashboard.reply_placeholder', default: 'Write your reply…') %>
|
data/lib/livechat/dashboard.js
CHANGED
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
thread.scrollTop = thread.scrollHeight;
|
|
57
57
|
|
|
58
58
|
var replyBox = document.querySelector(".reply textarea");
|
|
59
|
-
var banner = document.getElementById("new-messages");
|
|
60
59
|
var latest = parseInt(thread.getAttribute("data-latest"), 10) || 0;
|
|
60
|
+
var lastAuthorKey = thread.getAttribute("data-last-author") || null;
|
|
61
61
|
var url = thread.getAttribute("data-poll-url");
|
|
62
62
|
|
|
63
63
|
if (replyBox) {
|
|
@@ -69,17 +69,49 @@
|
|
|
69
69
|
});
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
// New messages are appended in place — never a reload — so a half-written
|
|
73
|
+
// reply is never lost and scroll position is kept unless you're at the
|
|
74
|
+
// bottom. Author headers group exactly like the server-rendered thread.
|
|
75
|
+
function append(message) {
|
|
76
|
+
if (message.id <= latest) return;
|
|
77
|
+
latest = message.id;
|
|
78
|
+
|
|
79
|
+
if (message.author === "system") {
|
|
80
|
+
var line = document.createElement("p");
|
|
81
|
+
line.className = "system";
|
|
82
|
+
line.id = "message-" + message.id;
|
|
83
|
+
line.textContent = message.text + " · " + message.at;
|
|
84
|
+
thread.appendChild(line);
|
|
85
|
+
lastAuthorKey = null;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
var key = message.author + ":" + (message.name || "");
|
|
90
|
+
if (key !== lastAuthorKey) {
|
|
91
|
+
var who = document.createElement("p");
|
|
92
|
+
who.className = "who " + message.author;
|
|
93
|
+
who.textContent = message.name + " · " + message.at;
|
|
94
|
+
thread.appendChild(who);
|
|
95
|
+
lastAuthorKey = key;
|
|
96
|
+
}
|
|
97
|
+
var bubble = document.createElement("div");
|
|
98
|
+
bubble.className = "msg " + message.author;
|
|
99
|
+
bubble.id = "message-" + message.id;
|
|
100
|
+
bubble.textContent = message.body;
|
|
101
|
+
thread.appendChild(bubble);
|
|
102
|
+
}
|
|
103
|
+
|
|
72
104
|
setInterval(function () {
|
|
73
105
|
if (document.hidden) return;
|
|
74
|
-
|
|
106
|
+
// Only auto-scroll if the agent is already reading the latest — don't
|
|
107
|
+
// yank them down while they've scrolled up through history.
|
|
108
|
+
var atBottom = thread.scrollHeight - thread.scrollTop - thread.clientHeight < 60;
|
|
109
|
+
fetch(url + "?after=" + latest, { headers: { Accept: "application/json" }, credentials: "same-origin" })
|
|
75
110
|
.then(function (response) { return response.ok ? response.json() : null; })
|
|
76
111
|
.then(function (data) {
|
|
77
|
-
if (!data || data.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
} else if (banner) {
|
|
81
|
-
banner.hidden = false;
|
|
82
|
-
}
|
|
112
|
+
if (!data || !data.messages || !data.messages.length) return;
|
|
113
|
+
data.messages.forEach(append);
|
|
114
|
+
if (atBottom) thread.scrollTop = thread.scrollHeight;
|
|
83
115
|
})
|
|
84
116
|
.catch(function () { /* transient network error — next tick retries */ });
|
|
85
117
|
}, POLL_MS);
|
data/lib/livechat/version.rb
CHANGED
data/lib/livechat/widget.js
CHANGED
|
@@ -207,8 +207,17 @@
|
|
|
207
207
|
listEl.id = "lvc-list";
|
|
208
208
|
listEl.setAttribute("role", "log");
|
|
209
209
|
listEl.setAttribute("aria-live", "polite");
|
|
210
|
+
// The opening greeting, styled as a support message so the panel reads
|
|
211
|
+
// like a conversation that's already been warmly opened. Deliberately a
|
|
212
|
+
// generic team label — never a fake individual, avatar, or presence
|
|
213
|
+
// (livechat is honest async support). It's client-only: never stored,
|
|
214
|
+
// never shown in the agent's inbox.
|
|
215
|
+
var greetingWho = document.createElement("div");
|
|
216
|
+
greetingWho.className = "lvc-who lvc-who-agent";
|
|
217
|
+
greetingWho.textContent = config.labels.team;
|
|
218
|
+
listEl.appendChild(greetingWho);
|
|
210
219
|
var greeting = document.createElement("div");
|
|
211
|
-
greeting.className = "lvc-
|
|
220
|
+
greeting.className = "lvc-msg lvc-agent";
|
|
212
221
|
greeting.textContent = config.labels.greeting;
|
|
213
222
|
listEl.appendChild(greeting);
|
|
214
223
|
panel.appendChild(listEl);
|
|
@@ -611,7 +620,6 @@
|
|
|
611
620
|
"background:var(--lvc-bg);display:flex;flex-direction:column;gap:4px}" +
|
|
612
621
|
// Class rules carry the #lvc-root prefix so they outrank the
|
|
613
622
|
// id-level `#lvc-root *` reset above.
|
|
614
|
-
"#lvc-root .lvc-greeting{color:var(--lvc-muted);font-size:13px;margin-bottom:8px}" +
|
|
615
623
|
"#lvc-root .lvc-who{font-size:11px;color:var(--lvc-muted);margin:8px 4px 2px}" +
|
|
616
624
|
"#lvc-root .lvc-who-visitor{text-align:right}" +
|
|
617
625
|
"#lvc-root[dir=rtl] .lvc-who-visitor{text-align:left}" +
|