livechat 0.2.1 → 0.3.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 +8 -0
- data/README.md +6 -4
- data/app/controllers/livechat/conversations_controller.rb +30 -4
- data/app/views/layouts/livechat/application.html.erb +3 -0
- data/app/views/livechat/conversations/index.html.erb +21 -3
- data/config/locales/livechat.ar.yml +2 -0
- data/config/locales/livechat.bg.yml +2 -0
- data/config/locales/livechat.bn.yml +2 -0
- data/config/locales/livechat.de.yml +2 -0
- data/config/locales/livechat.el.yml +2 -0
- data/config/locales/livechat.en.yml +2 -0
- data/config/locales/livechat.es.yml +2 -0
- data/config/locales/livechat.fr.yml +2 -0
- data/config/locales/livechat.hi.yml +2 -0
- data/config/locales/livechat.hr.yml +2 -0
- data/config/locales/livechat.id.yml +2 -0
- data/config/locales/livechat.it.yml +2 -0
- data/config/locales/livechat.ja.yml +2 -0
- data/config/locales/livechat.ko.yml +2 -0
- data/config/locales/livechat.lb.yml +2 -0
- data/config/locales/livechat.nl.yml +2 -0
- data/config/locales/livechat.pl.yml +2 -0
- data/config/locales/livechat.pt.yml +2 -0
- data/config/locales/livechat.ro.yml +2 -0
- data/config/locales/livechat.ru.yml +2 -0
- data/config/locales/livechat.th.yml +2 -0
- data/config/locales/livechat.tr.yml +2 -0
- data/config/locales/livechat.uk.yml +2 -0
- data/config/locales/livechat.ur.yml +2 -0
- data/config/locales/livechat.vi.yml +2 -0
- data/config/locales/livechat.zh-CN.yml +2 -0
- data/config/routes.rb +3 -0
- data/lib/livechat/dashboard.js +30 -1
- data/lib/livechat/version.rb +1 -1
- 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: 03a4a70e650dd115654bf8ab48e4afc02b5f628009732d3efbff4a7a36e278cc
|
|
4
|
+
data.tar.gz: e74596da328480eacab3097e9c08cc78464369ab2e9db6f9c015904498d53809
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 83f87fff5df20fc4b6ba70190fd66fc01dd4a32920f5acd36069d94c6f15eb8ab07711a865020b639ce2bd0c7d6555d606415844f2f7b639575d61dfd211fd16
|
|
7
|
+
data.tar.gz: ed3589f43b665ae857d6f9a918643feae0c239515f6413a41579a66e6caf4e4a1ca4bc356db9dfeeb274e6162c841649893dc87e535f91cb6733c4ca4f80dbc4
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
- Inbox search: visitor name, email, and full message text, scoped to the
|
|
6
|
+
current tab.
|
|
7
|
+
- The conversation list keeps itself fresh (new messages, new threads,
|
|
8
|
+
resolves) — without ever reloading over a search in progress.
|
|
9
|
+
- An Agents column on the list shows which teammates have worked each thread.
|
|
10
|
+
|
|
3
11
|
## 0.2.1
|
|
4
12
|
|
|
5
13
|
- With `show_launcher = false` the panel now sits in the corner instead of
|
data/README.md
CHANGED
|
@@ -127,10 +127,12 @@ config.on_visitor_message = ->(message) { SlackNotifier.ping(message) }
|
|
|
127
127
|
|
|
128
128
|
## The inbox
|
|
129
129
|
|
|
130
|
-
Browse at the mount path: open and resolved tabs, unread badges,
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
Browse at the mount path: open and resolved tabs, unread badges, search
|
|
131
|
+
(visitor name, email, and everything anyone wrote), and a column showing
|
|
132
|
+
which teammates have worked each thread. One click into a thread; reply
|
|
133
|
+
(Cmd/Ctrl+Enter sends), resolve, reopen. Both pages keep themselves fresh
|
|
134
|
+
while you watch — and never reload over a half-written reply or search.
|
|
135
|
+
Gated by `config.authorize_agent` (development-only until you set it).
|
|
134
136
|
|
|
135
137
|
## Widget API
|
|
136
138
|
|
|
@@ -6,25 +6,39 @@ module Livechat
|
|
|
6
6
|
# not an assignment queue.
|
|
7
7
|
class ConversationsController < ApplicationController
|
|
8
8
|
before_action :require_agent
|
|
9
|
-
before_action :set_conversation, except:
|
|
9
|
+
before_action :set_conversation, except: %i[index index_poll]
|
|
10
10
|
layout 'livechat/application'
|
|
11
11
|
|
|
12
12
|
PER_PAGE = 50
|
|
13
13
|
|
|
14
14
|
def index
|
|
15
15
|
@status = Conversation::STATUSES.include?(params[:status]) ? params[:status] : 'open'
|
|
16
|
+
@query = params[:q].to_s.strip.presence
|
|
16
17
|
@counts = Conversation.group(:status).count
|
|
17
18
|
@offset = params[:offset].to_i.clamp(0, 1_000_000)
|
|
18
19
|
|
|
19
20
|
scope = Conversation.where(status: @status).recent_first
|
|
21
|
+
scope = search(scope, @query) if @query
|
|
20
22
|
page = scope.offset(@offset).limit(PER_PAGE + 1).to_a
|
|
21
23
|
@more = page.size > PER_PAGE
|
|
22
24
|
@conversations = page.first(PER_PAGE)
|
|
23
25
|
|
|
24
|
-
# Unread badges for the whole page
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
# Unread badges and participating agents for the whole page, one query each.
|
|
27
|
+
ids = @conversations.map(&:id)
|
|
28
|
+
@unread = Message.from_visitor.unread.where(conversation_id: ids)
|
|
27
29
|
.group(:conversation_id).count
|
|
30
|
+
@agents = Message.from_agent.where(conversation_id: ids)
|
|
31
|
+
.distinct.pluck(:conversation_id, :agent_label)
|
|
32
|
+
.group_by(&:first).transform_values { |pairs| pairs.map(&:last) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Polled by dashboard.js on the list page: a token that changes whenever
|
|
36
|
+
# anything an agent can see there changes — new message, new thread,
|
|
37
|
+
# resolve/reopen.
|
|
38
|
+
def index_poll
|
|
39
|
+
render json: { token: [Message.maximum(:id).to_i,
|
|
40
|
+
Conversation.count,
|
|
41
|
+
Conversation.where(status: 'open').count].join('-') }
|
|
28
42
|
end
|
|
29
43
|
|
|
30
44
|
def show
|
|
@@ -53,5 +67,17 @@ module Livechat
|
|
|
53
67
|
def set_conversation
|
|
54
68
|
@conversation = Conversation.find(params[:id])
|
|
55
69
|
end
|
|
70
|
+
|
|
71
|
+
# Case-insensitive match on who the visitor is or anything anyone wrote.
|
|
72
|
+
# LOWER(...) LIKE is deliberately plain SQL — portable across SQLite,
|
|
73
|
+
# PostgreSQL and MySQL alike (feedback_engine's proven approach).
|
|
74
|
+
def search(scope, query)
|
|
75
|
+
q = "%#{ActiveRecord::Base.sanitize_sql_like(query.downcase)}%"
|
|
76
|
+
scope.where(
|
|
77
|
+
'LOWER(visitor_label) LIKE :q OR LOWER(visitor_email) LIKE :q OR id IN ' \
|
|
78
|
+
"(SELECT conversation_id FROM #{Message.table_name} WHERE LOWER(body) LIKE :q)",
|
|
79
|
+
q: q
|
|
80
|
+
)
|
|
81
|
+
end
|
|
56
82
|
end
|
|
57
83
|
end
|
|
@@ -35,6 +35,9 @@
|
|
|
35
35
|
.tabs a:hover { text-decoration: none; color: var(--text); }
|
|
36
36
|
.count { display: inline-block; min-width: 20px; padding: 0 6px; margin-left: 4px; border-radius: 999px;
|
|
37
37
|
background: var(--border); font-size: 12px; text-align: center; }
|
|
38
|
+
.filters { margin-bottom: 16px; display: flex; gap: 8px; }
|
|
39
|
+
.filters input[type=search] { flex: 1; max-width: 320px; padding: 6px 10px; border: 1px solid var(--border);
|
|
40
|
+
border-radius: 8px; background: var(--surface); color: var(--text); font: inherit; }
|
|
38
41
|
.card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; }
|
|
39
42
|
.card.pad { padding: 16px; }
|
|
40
43
|
table { width: 100%; border-collapse: collapse; }
|
|
@@ -2,13 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
<div class="tabs">
|
|
4
4
|
<% Livechat::Conversation::STATUSES.each do |status| %>
|
|
5
|
-
<%= link_to conversations_path(status: status), class: ('active' if @status == status) do %>
|
|
5
|
+
<%= link_to conversations_path(status: status, q: @query), class: ('active' if @status == status) do %>
|
|
6
6
|
<%= t("livechat.statuses.#{status}", default: status.humanize) %>
|
|
7
7
|
<span class="count"><%= @counts.fetch(status, 0) %></span>
|
|
8
8
|
<% end %>
|
|
9
9
|
<% end %>
|
|
10
10
|
</div>
|
|
11
11
|
|
|
12
|
+
<%= form_with url: conversations_path, method: :get, class: 'filters' do %>
|
|
13
|
+
<input type="hidden" name="status" value="<%= @status %>">
|
|
14
|
+
<input type="search" name="q" value="<%= @query %>"
|
|
15
|
+
placeholder="<%= t('livechat.dashboard.search', default: 'Search') %>"
|
|
16
|
+
aria-label="<%= t('livechat.dashboard.search', default: 'Search') %>">
|
|
17
|
+
<button type="submit"><%= t('livechat.dashboard.search', default: 'Search') %></button>
|
|
18
|
+
<% end %>
|
|
19
|
+
|
|
20
|
+
<%# The list keeps itself fresh (dashboard.js reloads when the token moves) —
|
|
21
|
+
unless the agent is mid-search, whose typing must never be eaten. %>
|
|
22
|
+
<div id="lvc-index" data-poll-url="<%= poll_conversations_path %>"
|
|
23
|
+
data-token="<%= [Livechat::Message.maximum(:id).to_i, Livechat::Conversation.count,
|
|
24
|
+
@counts.fetch('open', 0)].join('-') %>"></div>
|
|
25
|
+
|
|
12
26
|
<div class="card">
|
|
13
27
|
<% if @conversations.empty? %>
|
|
14
28
|
<p class="empty"><%= t('livechat.dashboard.empty',
|
|
@@ -20,6 +34,7 @@
|
|
|
20
34
|
<th>#</th>
|
|
21
35
|
<th><%= t('livechat.dashboard.visitor', default: 'Visitor') %></th>
|
|
22
36
|
<th><%= t('livechat.dashboard.last_message', default: 'Last message') %></th>
|
|
37
|
+
<th><%= t('livechat.dashboard.agents', default: 'Agents') %></th>
|
|
23
38
|
<th><%= t('livechat.dashboard.updated', default: 'Updated') %></th>
|
|
24
39
|
</tr>
|
|
25
40
|
</thead>
|
|
@@ -38,6 +53,7 @@
|
|
|
38
53
|
<% end %>
|
|
39
54
|
</td>
|
|
40
55
|
<td class="muted"><%= conversation.last_message_preview %></td>
|
|
56
|
+
<td class="muted"><%= @agents.fetch(conversation.id, []).join(', ') %></td>
|
|
41
57
|
<td class="muted">
|
|
42
58
|
<% if conversation.last_activity_at %>
|
|
43
59
|
<%= time_ago_in_words(conversation.last_activity_at) %>
|
|
@@ -53,10 +69,12 @@
|
|
|
53
69
|
<div class="pager">
|
|
54
70
|
<% if @offset.positive? %>
|
|
55
71
|
<%= link_to t('livechat.dashboard.newer', default: 'Newer'),
|
|
56
|
-
conversations_path(status: @status,
|
|
72
|
+
conversations_path(status: @status, q: @query,
|
|
73
|
+
offset: [@offset - Livechat::ConversationsController::PER_PAGE, 0].max) %>
|
|
57
74
|
<% end %>
|
|
58
75
|
<% if @more %>
|
|
59
76
|
<%= link_to t('livechat.dashboard.older', default: 'Older'),
|
|
60
|
-
conversations_path(status: @status,
|
|
77
|
+
conversations_path(status: @status, q: @query,
|
|
78
|
+
offset: @offset + Livechat::ConversationsController::PER_PAGE) %>
|
|
61
79
|
<% end %>
|
|
62
80
|
</div>
|
data/config/routes.rb
CHANGED
|
@@ -15,6 +15,9 @@ Livechat::Engine.routes.draw do
|
|
|
15
15
|
|
|
16
16
|
# The inbox. Flat, human URLs: the mount path IS the conversation list.
|
|
17
17
|
resources :conversations, path: '', only: %i[index show], constraints: { id: /\d+/ } do
|
|
18
|
+
collection do
|
|
19
|
+
get :poll, action: :index_poll
|
|
20
|
+
end
|
|
18
21
|
member do
|
|
19
22
|
get :poll
|
|
20
23
|
post :resolve
|
data/lib/livechat/dashboard.js
CHANGED
|
@@ -21,6 +21,35 @@
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
ready(function () {
|
|
24
|
+
watchIndex();
|
|
25
|
+
watchThread();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// The conversation list keeps itself fresh: when the server's token moves
|
|
29
|
+
// (new message, new thread, resolve/reopen), just reload — unless the
|
|
30
|
+
// agent is mid-search, whose typing must never be eaten.
|
|
31
|
+
function watchIndex() {
|
|
32
|
+
var index = document.getElementById("lvc-index");
|
|
33
|
+
if (!index) return;
|
|
34
|
+
|
|
35
|
+
var token = index.getAttribute("data-token");
|
|
36
|
+
var url = index.getAttribute("data-poll-url");
|
|
37
|
+
var searchBox = document.querySelector(".filters input[type=search]");
|
|
38
|
+
|
|
39
|
+
setInterval(function () {
|
|
40
|
+
if (document.hidden) return;
|
|
41
|
+
if (searchBox && searchBox.value !== searchBox.defaultValue) return;
|
|
42
|
+
if (searchBox && document.activeElement === searchBox) return;
|
|
43
|
+
fetch(url, { headers: { Accept: "application/json" }, credentials: "same-origin" })
|
|
44
|
+
.then(function (response) { return response.ok ? response.json() : null; })
|
|
45
|
+
.then(function (data) {
|
|
46
|
+
if (data && data.token !== token) window.location.reload();
|
|
47
|
+
})
|
|
48
|
+
.catch(function () { /* transient network error — next tick retries */ });
|
|
49
|
+
}, POLL_MS);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function watchThread() {
|
|
24
53
|
var thread = document.getElementById("thread");
|
|
25
54
|
if (!thread) return;
|
|
26
55
|
|
|
@@ -54,5 +83,5 @@
|
|
|
54
83
|
})
|
|
55
84
|
.catch(function () { /* transient network error — next tick retries */ });
|
|
56
85
|
}, POLL_MS);
|
|
57
|
-
}
|
|
86
|
+
}
|
|
58
87
|
})();
|
data/lib/livechat/version.rb
CHANGED