livechat 0.3.3 → 0.3.5

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: 3263710888d695851123f5afeffc0dd73985901e0ad559f5d04562a59c411537
4
- data.tar.gz: 9db6f6b5df671b05b594dd174180cc54c2988695aac907ab3f7f3e38c32df732
3
+ metadata.gz: af07804137de8734d4851efdea56a8715af2ab199cdd99694f896abf42f908c9
4
+ data.tar.gz: 4272cb7b782303cd4ff21de16b16f849b9a32c3b4b3e91a057fe70fcc23aa86a
5
5
  SHA512:
6
- metadata.gz: c5ae2806f1b7f1c8d08b14bc61043c8fe23159873c260a75c3ed6a5b263bc2ada95630af19a323a64c3de774d107e17aef8094454535460bba76cb10c91d5846
7
- data.tar.gz: cda4c08755760e0a1a1996be4aa5165e8d7ddf10ca86cd0362905b0d02c5ee1dd8e73f3a2f8dcec3cbb9b3b68a23418d3898f9bca48146365799544ca029d332
6
+ metadata.gz: 3a0064196bc9737e6f008aa3f558d364ad2c29d09701774e629bc4bbb25a23074088597ec8e9a55c228b1dd20d7cbc8e6c5186fc35ac3e2a34445a710a793638
7
+ data.tar.gz: b41ce44b50df38aa11228ab5082f755ce4424d0dd3955a5d1e41af53829f1210492d13fa02e716d2b146e08bb54125f05b336396b3944c25953645c2e7a972b7
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.5
4
+
5
+ - The inbox thread now appends new messages live instead of showing a
6
+ "New messages — refresh" link — no page reload, so an agent's half-written
7
+ reply is never lost, and scroll position is kept unless you're at the
8
+ bottom. Visitor messages are marked read as they stream in. Still polling
9
+ (no Action Cable), still draft-safe.
10
+
11
+ ## 0.3.4
12
+
13
+ - Docs: show how to wire `current_user` with Rails 8's built-in authentication
14
+ (`bin/rails generate authentication`), alongside the existing Devise/Warden
15
+ example — in the README and the generated initializer.
16
+
3
17
  ## 0.3.3
4
18
 
5
19
  - Accessibility: on phones (where the panel is a full-screen modal) focus is
data/README.md CHANGED
@@ -91,6 +91,20 @@ Livechat.configure do |config|
91
91
  end
92
92
  ```
93
93
 
94
+ `current_user` (and any admin gate) receives the raw request, so it works
95
+ with whatever auth you have:
96
+
97
+ ```ruby
98
+ # Devise / Warden:
99
+ config.current_user = ->(request) { request.env["warden"]&.user }
100
+
101
+ # Rails 8 built-in auth (bin/rails generate authentication):
102
+ config.current_user = lambda do |request|
103
+ token = request.cookies["session_token"]
104
+ Session.find_signed(token)&.user if token
105
+ end
106
+ ```
107
+
94
108
  ### Brand color
95
109
 
96
110
  ```ruby
@@ -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:
@@ -16,7 +16,15 @@ Livechat.configure do |config|
16
16
  # Resolve the current user (optional). Return an object responding to #id,
17
17
  # or nil. Signed-in visitors keep one conversation across devices; guests
18
18
  # are tracked with a cookie. The same user is the agent when replying.
19
+ #
20
+ # Devise / Warden:
19
21
  # config.current_user = ->(request) { request.env["warden"]&.user }
22
+ #
23
+ # Rails 8 built-in auth (bin/rails generate authentication):
24
+ # config.current_user = lambda do |request|
25
+ # token = request.cookies["session_token"]
26
+ # Session.find_signed(token)&.user if token
27
+ # end
20
28
 
21
29
  # How visitors appear in the inbox.
22
30
  # config.visitor_label = ->(user) { user.name.presence || user.email }
@@ -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.3'
4
+ VERSION = '0.3.5'
5
5
  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.3.3
4
+ version: 0.3.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yaroslav Shmarov