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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aa83a99e1d1ebe0259347b286862ec7cf7cfffbfe47ea4a624baff36b3ca066e
4
- data.tar.gz: 0d017808858c6dfbce423e283b061476a74b3075b42bb70c3f10c54fb74b5622
3
+ metadata.gz: 68b007c7a72520ae1b1732ec24961ed5ca88b166fbdc30aac082b2bf711346bd
4
+ data.tar.gz: 3d3dbc292f4115eb19aa22d295e5f873707db12df684de803c72dd912eeb82ac
5
5
  SHA512:
6
- metadata.gz: 97dc8a43bfb5ab7fecd0dabdfaf5f126e3346b97e73f56aa96129331046e74355bbc201a7fc0ab010c1ef0e48a73d15ec7aaadac0dd185c4310480271486984c
7
- data.tar.gz: e1469a54e206b39b539d6dacc767568caad7a44da3540d5ff3b746fb0affb74e83224f3ca9f0ea87163f2796656d66b274aaa58332e1cd40ddfda9b441b1028d
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: has anything new arrived in this thread?
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
- render json: { latest: @conversation.messages.maximum(:id).to_i,
52
- status: @conversation.status }
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="<%= @messages.last&.id.to_i %>">
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…') %>
@@ -36,7 +36,6 @@ ar:
36
36
  reopen: "إعادة فتح"
37
37
  reply_placeholder: "اكتب ردك…"
38
38
  send: "إرسال"
39
- new_messages: "رسائل جديدة — تحديث"
40
39
  newer: "الأحدث"
41
40
  older: "الأقدم"
42
41
  mail:
@@ -36,7 +36,6 @@ bg:
36
36
  reopen: "Отвори отново"
37
37
  reply_placeholder: "Напишете отговора си…"
38
38
  send: "Изпрати"
39
- new_messages: "Нови съобщения — обновете"
40
39
  newer: "По-нови"
41
40
  older: "По-стари"
42
41
  mail:
@@ -36,7 +36,6 @@ bn:
36
36
  reopen: "আবার খুলুন"
37
37
  reply_placeholder: "আপনার উত্তর লিখুন…"
38
38
  send: "পাঠান"
39
- new_messages: "নতুন বার্তা — রিফ্রেশ করুন"
40
39
  newer: "নতুনতর"
41
40
  older: "পুরনো"
42
41
  mail:
@@ -36,7 +36,6 @@ de:
36
36
  reopen: "Wieder öffnen"
37
37
  reply_placeholder: "Antwort schreiben…"
38
38
  send: "Senden"
39
- new_messages: "Neue Nachrichten — aktualisieren"
40
39
  newer: "Neuere"
41
40
  older: "Ältere"
42
41
  mail:
@@ -36,7 +36,6 @@ el:
36
36
  reopen: "Άνοιγμα ξανά"
37
37
  reply_placeholder: "Γράψτε την απάντησή σας…"
38
38
  send: "Αποστολή"
39
- new_messages: "Νέα μηνύματα — ανανέωση"
40
39
  newer: "Νεότερα"
41
40
  older: "Παλαιότερα"
42
41
  mail:
@@ -36,7 +36,6 @@ en:
36
36
  reopen: "Reopen"
37
37
  reply_placeholder: "Write your reply…"
38
38
  send: "Send"
39
- new_messages: "New messages — refresh"
40
39
  newer: "Newer"
41
40
  older: "Older"
42
41
  mail:
@@ -36,7 +36,6 @@ es:
36
36
  reopen: "Reabrir"
37
37
  reply_placeholder: "Escribe tu respuesta…"
38
38
  send: "Enviar"
39
- new_messages: "Mensajes nuevos — actualizar"
40
39
  newer: "Más recientes"
41
40
  older: "Más antiguos"
42
41
  mail:
@@ -36,7 +36,6 @@ fr:
36
36
  reopen: "Rouvrir"
37
37
  reply_placeholder: "Écrivez votre réponse…"
38
38
  send: "Envoyer"
39
- new_messages: "Nouveaux messages — actualiser"
40
39
  newer: "Plus récents"
41
40
  older: "Plus anciens"
42
41
  mail:
@@ -36,7 +36,6 @@ hi:
36
36
  reopen: "फिर से खोलें"
37
37
  reply_placeholder: "अपना जवाब लिखें…"
38
38
  send: "भेजें"
39
- new_messages: "नए संदेश — रीफ़्रेश करें"
40
39
  newer: "नए"
41
40
  older: "पुराने"
42
41
  mail:
@@ -36,7 +36,6 @@ hr:
36
36
  reopen: "Ponovno otvori"
37
37
  reply_placeholder: "Napišite svoj odgovor…"
38
38
  send: "Pošalji"
39
- new_messages: "Nove poruke — osvježite"
40
39
  newer: "Novije"
41
40
  older: "Starije"
42
41
  mail:
@@ -36,7 +36,6 @@ id:
36
36
  reopen: "Buka kembali"
37
37
  reply_placeholder: "Tulis balasan Anda…"
38
38
  send: "Kirim"
39
- new_messages: "Pesan baru — muat ulang"
40
39
  newer: "Lebih baru"
41
40
  older: "Lebih lama"
42
41
  mail:
@@ -36,7 +36,6 @@ it:
36
36
  reopen: "Riapri"
37
37
  reply_placeholder: "Scrivi la tua risposta…"
38
38
  send: "Invia"
39
- new_messages: "Nuovi messaggi — aggiorna"
40
39
  newer: "Più recenti"
41
40
  older: "Più vecchi"
42
41
  mail:
@@ -36,7 +36,6 @@ ja:
36
36
  reopen: "再開"
37
37
  reply_placeholder: "返信を入力…"
38
38
  send: "送信"
39
- new_messages: "新着メッセージ — 更新"
40
39
  newer: "新しい"
41
40
  older: "古い"
42
41
  mail:
@@ -36,7 +36,6 @@ ko:
36
36
  reopen: "다시 열기"
37
37
  reply_placeholder: "답장을 입력하세요…"
38
38
  send: "보내기"
39
- new_messages: "새 메시지 — 새로고침"
40
39
  newer: "최신"
41
40
  older: "이전"
42
41
  mail:
@@ -36,7 +36,6 @@ lb:
36
36
  reopen: "Nees opmaachen"
37
37
  reply_placeholder: "Schreift Är Äntwert…"
38
38
  send: "Schécken"
39
- new_messages: "Nei Noriichten — aktualiséieren"
40
39
  newer: "Méi nei"
41
40
  older: "Méi al"
42
41
  mail:
@@ -36,7 +36,6 @@ nl:
36
36
  reopen: "Heropenen"
37
37
  reply_placeholder: "Schrijf je antwoord…"
38
38
  send: "Versturen"
39
- new_messages: "Nieuwe berichten — vernieuwen"
40
39
  newer: "Nieuwer"
41
40
  older: "Ouder"
42
41
  mail:
@@ -36,7 +36,6 @@ pl:
36
36
  reopen: "Wznów"
37
37
  reply_placeholder: "Napisz odpowiedź…"
38
38
  send: "Wyślij"
39
- new_messages: "Nowe wiadomości — odśwież"
40
39
  newer: "Nowsze"
41
40
  older: "Starsze"
42
41
  mail:
@@ -36,7 +36,6 @@ pt:
36
36
  reopen: "Reabrir"
37
37
  reply_placeholder: "Escreva sua resposta…"
38
38
  send: "Enviar"
39
- new_messages: "Novas mensagens — atualizar"
40
39
  newer: "Mais recentes"
41
40
  older: "Mais antigas"
42
41
  mail:
@@ -36,7 +36,6 @@ ro:
36
36
  reopen: "Redeschide"
37
37
  reply_placeholder: "Scrieți răspunsul…"
38
38
  send: "Trimite"
39
- new_messages: "Mesaje noi — reîmprospătați"
40
39
  newer: "Mai noi"
41
40
  older: "Mai vechi"
42
41
  mail:
@@ -36,7 +36,6 @@ ru:
36
36
  reopen: "Возобновить"
37
37
  reply_placeholder: "Напишите ответ…"
38
38
  send: "Отправить"
39
- new_messages: "Новые сообщения — обновите"
40
39
  newer: "Новее"
41
40
  older: "Старше"
42
41
  mail:
@@ -36,7 +36,6 @@ th:
36
36
  reopen: "เปิดอีกครั้ง"
37
37
  reply_placeholder: "เขียนคำตอบของคุณ…"
38
38
  send: "ส่ง"
39
- new_messages: "มีข้อความใหม่ — รีเฟรช"
40
39
  newer: "ใหม่กว่า"
41
40
  older: "เก่ากว่า"
42
41
  mail:
@@ -36,7 +36,6 @@ tr:
36
36
  reopen: "Yeniden aç"
37
37
  reply_placeholder: "Yanıtınızı yazın…"
38
38
  send: "Gönder"
39
- new_messages: "Yeni mesajlar — yenileyin"
40
39
  newer: "Daha yeni"
41
40
  older: "Daha eski"
42
41
  mail:
@@ -36,7 +36,6 @@ uk:
36
36
  reopen: "Відновити"
37
37
  reply_placeholder: "Напишіть відповідь…"
38
38
  send: "Надіслати"
39
- new_messages: "Нові повідомлення — оновіть"
40
39
  newer: "Новіші"
41
40
  older: "Старіші"
42
41
  mail:
@@ -36,7 +36,6 @@ ur:
36
36
  reopen: "دوبارہ کھولیں"
37
37
  reply_placeholder: "اپنا جواب لکھیں…"
38
38
  send: "بھیجیں"
39
- new_messages: "نئے پیغامات — ریفریش کریں"
40
39
  newer: "نئے"
41
40
  older: "پرانے"
42
41
  mail:
@@ -36,7 +36,6 @@ vi:
36
36
  reopen: "Mở lại"
37
37
  reply_placeholder: "Viết câu trả lời của bạn…"
38
38
  send: "Gửi"
39
- new_messages: "Tin nhắn mới — làm mới"
40
39
  newer: "Mới hơn"
41
40
  older: "Cũ hơn"
42
41
  mail:
@@ -36,7 +36,6 @@
36
36
  reopen: "重新打开"
37
37
  reply_placeholder: "输入您的回复…"
38
38
  send: "发送"
39
- new_messages: "有新消息 — 请刷新"
40
39
  newer: "较新"
41
40
  older: "较旧"
42
41
  mail:
@@ -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
- fetch(url, { headers: { Accept: "application/json" }, credentials: "same-origin" })
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.latest <= latest) return;
78
- if (!replyBox || replyBox.value.trim() === "") {
79
- window.location.reload();
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);
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Livechat
4
- VERSION = '0.3.4'
4
+ VERSION = '0.3.6'
5
5
  end
@@ -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-greeting";
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}" +
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.3.4
4
+ version: 0.3.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yaroslav Shmarov