livechat 0.4.6 → 0.4.8

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: daa490b0c4d70500971447e501bd9a8a83bbdf6c0d9d73e093becc84319b3988
4
- data.tar.gz: 1199336c0fa8ab2ee68d338d01a5c80e60361611e257fdaff5228b3eb8f577c2
3
+ metadata.gz: 7f0dd913b9f9ac88756a298d2ad57cf69c78d3f8bf31c08941171a6d13626a16
4
+ data.tar.gz: 1148803aaf1b0d465afd5526ee6196d0814f23f0a3c75d4f9a7cfbd3ba96f6ee
5
5
  SHA512:
6
- metadata.gz: 12e47c2e8b8ff3c412531cbb88033aebde6aaebea5cd0f9fd28cc64ee16e79c32e8448a7e0b2f3d8b951e3f176d2e83566e280b6401d8897c765fd6b76428e7a
7
- data.tar.gz: e32220e0923be59ccabdf99fb3ea93816c203dbecaae4faa769b84aeb36a3f96735f8719b1d8d2e3a1ca23f841ebc0d8b12b6ea66562f8c3e82ebb7e3484cb20
6
+ metadata.gz: 8eff7b6fff9df34b825dd647d41eabc4e415572bd5dd282465f52de3e2ac4fa3ee648a6123b426dc67615f064d82e3354762b0cc9103ea9d724d251de5e769c8
7
+ data.tar.gz: 57905952ca50aaa60b2103577ab54f6ee433f0729329ee5ea999e7a4bade5100a9e585ce9702842857962209f3b63369db9dde0b03358a723d4a9bb960e48251
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.8
4
+
5
+ - A conversation with image attachments now opens scrolled to the bottom, not
6
+ mid-thread. Images load after the initial scroll and grow the thread, so both
7
+ the inbox and the widget now re-pin to the bottom as each image loads (and
8
+ the inbox keeps you pinned until you scroll up). Fixes threads with
9
+ screenshots opening somewhere in the middle.
10
+
11
+ ## 0.4.7
12
+
13
+ - The inbox reply composer now matches the visitor widget: one bordered box
14
+ with the reply on top and a toolbar row (paperclip left, circular send
15
+ right) inside it, lit on focus — instead of a textarea flanked by loose
16
+ buttons. The reply field auto-grows as you type, and file chips ride inside
17
+ the box.
18
+
3
19
  ## 0.4.6
4
20
 
5
21
  - Inbox conversation page no longer scrolls the whole page. It now fills the
@@ -93,28 +93,33 @@
93
93
  .thread .msg.agent .att-file { background: rgba(255,255,255,.2); }
94
94
  #new-messages { margin: 12px 0 0; }
95
95
  #new-messages a { font-weight: 600; }
96
- .reply { display: flex; gap: 8px; padding: 12px; border-top: 1px solid var(--border); }
97
- .reply .compose { flex: 1; display: flex; flex-direction: column; gap: 6px; }
98
- .reply textarea { resize: vertical; min-height: 44px; max-height: 200px; padding: 8px 10px;
99
- border: 1px solid var(--border); border-radius: 10px; font: inherit;
100
- background: var(--bg); color: var(--text); }
96
+ /* Unified composer box (matches the visitor widget): the reply on top, a
97
+ toolbar row below (tools left, send right), all inside one bordered box
98
+ that lights up on focus. */
99
+ .reply { display: flex; flex-direction: column; gap: 6px; margin: 10px 12px; padding: 8px 10px;
100
+ border: 1px solid var(--border); border-radius: 14px; background: var(--bg); }
101
+ .reply:focus-within { border-color: var(--accent); }
102
+ .reply textarea { -webkit-appearance: none; appearance: none; border: none; background: none;
103
+ resize: none; outline: none; font: inherit; color: var(--text);
104
+ padding: 4px 4px 0; min-height: 40px; max-height: 160px; overflow-y: auto; }
101
105
  .reply input[type=file].reply-file { font-size: 12px; color: var(--muted); }
102
- .reply button.attach { align-self: flex-end; width: 42px; min-width: 42px; height: 42px; padding: 0;
103
- display: flex; align-items: center; justify-content: center; color: var(--muted);
104
- background: var(--surface); border: 1px solid var(--border); border-radius: 10px; }
105
- .reply button.attach:hover { color: var(--text); background: var(--bg); }
106
+ .reply-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
107
+ .reply-tools { display: flex; align-items: center; gap: 2px; }
108
+ .reply .attach { width: 32px; height: 32px; padding: 0; border: none; background: none;
109
+ color: var(--muted); border-radius: 8px; cursor: pointer;
110
+ display: flex; align-items: center; justify-content: center; }
111
+ .reply .attach:hover { background: var(--border); color: var(--text); }
106
112
  .reply .chips { display: flex; flex-wrap: wrap; gap: 6px; }
107
113
  .reply .chip { display: inline-flex; align-items: center; gap: 6px; max-width: 100%;
108
114
  padding: 4px 6px 4px 10px; border: 1px solid var(--border); border-radius: 999px;
109
- background: var(--bg); font-size: 12px; }
115
+ background: var(--surface); font-size: 12px; }
110
116
  .reply .chip-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 220px; }
111
117
  .reply .chip-x { padding: 0 2px; border: none; background: none; color: var(--muted);
112
118
  font-size: 16px; line-height: 1; cursor: pointer; }
113
119
  .reply .chip-x:hover { color: var(--text); }
114
- .reply button.send { align-self: flex-end; width: 42px; min-width: 42px; height: 42px; padding: 0;
115
- display: flex; align-items: center; justify-content: center;
116
- background: var(--accent); border-color: var(--accent); color: var(--accent-text);
117
- border-radius: 10px; }
120
+ .reply .send { width: 32px; min-width: 32px; height: 32px; padding: 0; border: none; border-radius: 50%;
121
+ background: var(--accent); color: var(--accent-text); cursor: pointer;
122
+ display: flex; align-items: center; justify-content: center; }
118
123
  /* Conversation page: fill the viewport and scroll the THREAD, not the
119
124
  page — so a reply never jumps the page, and the composer stays put. */
120
125
  .lvc-convo, .lvc-convo .container { height: 100vh; height: 100dvh; }
@@ -63,23 +63,26 @@
63
63
  <% end %>
64
64
  </div>
65
65
 
66
+ <%# One bordered composer box (matches the visitor widget): the reply on top,
67
+ a toolbar row below with tools on the left and send on the right. %>
66
68
  <%= form_with url: conversation_messages_path(@conversation), method: :post,
67
69
  multipart: true, class: 'reply' do |form| %>
68
- <div class="compose">
69
- <%= form.text_area :body, rows: 2, autofocus: true, required: !Livechat.attachments_enabled?,
70
- placeholder: t('livechat.dashboard.reply_placeholder', default: 'Write your reply…') %>
71
- <%# dashboard.js hides this and swaps in a paperclip button + file chips.
72
- Left plain as a no-JS fallback so agents can still attach without it. %>
73
- <% if Livechat.attachments_enabled? %>
74
- <%= form.file_field :files, multiple: true, class: 'reply-file',
75
- aria: { label: t('livechat.dashboard.attach', default: 'Attach files') } %>
76
- <% end %>
70
+ <%= form.text_area :body, rows: 2, autofocus: true, required: !Livechat.attachments_enabled?,
71
+ placeholder: t('livechat.dashboard.reply_placeholder', default: 'Write your reply…') %>
72
+ <%# dashboard.js hides this and swaps in a paperclip button (into .reply-tools)
73
+ plus file chips at the top of the box. Left plain as a no-JS fallback. %>
74
+ <% if Livechat.attachments_enabled? %>
75
+ <%= form.file_field :files, multiple: true, class: 'reply-file',
76
+ aria: { label: t('livechat.dashboard.attach', default: 'Attach files') } %>
77
+ <% end %>
78
+ <div class="reply-toolbar">
79
+ <div class="reply-tools"></div>
80
+ <button type="submit" class="send"
81
+ aria-label="<%= t('livechat.dashboard.send', default: 'Send') %>"
82
+ title="<%= t('livechat.dashboard.send', default: 'Send') %>">
83
+ <%# Paper airplane (Heroicons, MIT) — same icon as the widget. %>
84
+ <svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path fill="currentColor" d="M3.478 2.404a.75.75 0 0 0-.926.941l2.432 7.905H13.5a.75.75 0 0 1 0 1.5H4.984l-2.432 7.905a.75.75 0 0 0 .926.94 60.519 60.519 0 0 0 18.445-8.986.75.75 0 0 0 0-1.218A60.517 60.517 0 0 0 3.478 2.404Z"/></svg>
85
+ </button>
77
86
  </div>
78
- <button type="submit" class="primary send"
79
- aria-label="<%= t('livechat.dashboard.send', default: 'Send') %>"
80
- title="<%= t('livechat.dashboard.send', default: 'Send') %>">
81
- <%# Paper airplane (Heroicons, MIT) — same icon as the widget. %>
82
- <svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path fill="currentColor" d="M3.478 2.404a.75.75 0 0 0-.926.941l2.432 7.905H13.5a.75.75 0 0 1 0 1.5H4.984l-2.432 7.905a.75.75 0 0 0 .926.94 60.519 60.519 0 0 0 18.445-8.986.75.75 0 0 0 0-1.218A60.517 60.517 0 0 0 3.478 2.404Z"/></svg>
83
- </button>
84
87
  <% end %>
85
88
  </div>
@@ -46,18 +46,19 @@
46
46
  button.title = label;
47
47
  // Paperclip (Heroicons, MIT) — same icon as the widget.
48
48
  button.innerHTML =
49
- '<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true">' +
49
+ '<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">' +
50
50
  '<path fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" ' +
51
51
  'stroke-linejoin="round" d="M18.4 8.6 9.9 17a3.5 3.5 0 0 1-5-5l8.5-8.4a2.3 2.3 0 0 1 ' +
52
52
  '3.3 3.3l-8.5 8.4a1.1 1.1 0 0 1-1.6-1.6l7.8-7.8"/></svg>';
53
53
  button.addEventListener("click", function () { input.click(); });
54
- form.insertBefore(button, form.firstChild);
54
+ // The paperclip lives in the toolbar (left of send); chips sit at the top
55
+ // of the box — so it reads as one composer, like the visitor widget.
56
+ (form.querySelector(".reply-tools") || form).appendChild(button);
55
57
 
56
58
  var chips = document.createElement("div");
57
59
  chips.className = "chips";
58
60
  chips.hidden = true;
59
- var compose = form.querySelector(".compose") || form;
60
- compose.insertBefore(chips, compose.firstChild);
61
+ form.insertBefore(chips, form.firstChild);
61
62
 
62
63
  input.addEventListener("change", render);
63
64
 
@@ -129,7 +130,21 @@
129
130
  var thread = document.getElementById("thread");
130
131
  if (!thread) return;
131
132
 
132
- thread.scrollTop = thread.scrollHeight;
133
+ // Stick to the bottom until the agent scrolls up. Images (screenshots and
134
+ // attachments) finish loading after DOMContentLoaded and grow the thread,
135
+ // so a single scroll-to-bottom now would land mid-thread once they expand
136
+ // — re-pin as each image loads and once the page has fully loaded.
137
+ var stick = true;
138
+ var keepBottom = function () { if (stick) thread.scrollTop = thread.scrollHeight; };
139
+ thread.addEventListener("scroll", function () {
140
+ stick = thread.scrollHeight - thread.scrollTop - thread.clientHeight < 80;
141
+ });
142
+ keepBottom();
143
+ var images = thread.querySelectorAll("img");
144
+ for (var i = 0; i < images.length; i++) {
145
+ if (!images[i].complete) images[i].addEventListener("load", keepBottom);
146
+ }
147
+ window.addEventListener("load", keepBottom);
133
148
 
134
149
  var replyBox = document.querySelector(".reply textarea");
135
150
  var latest = parseInt(thread.getAttribute("data-latest"), 10) || 0;
@@ -143,6 +158,14 @@
143
158
  replyBox.form.requestSubmit();
144
159
  }
145
160
  });
161
+ // Grow the box with the reply, up to the CSS max-height — matching the
162
+ // widget composer instead of a fixed two-row field.
163
+ var autogrow = function () {
164
+ replyBox.style.height = "auto";
165
+ replyBox.style.height = Math.min(replyBox.scrollHeight, 160) + "px";
166
+ };
167
+ replyBox.addEventListener("input", autogrow);
168
+ autogrow();
146
169
  }
147
170
 
148
171
  // New messages are appended in place — never a reload — so a half-written
@@ -195,6 +218,7 @@
195
218
  img.src = att.url;
196
219
  img.alt = att.name;
197
220
  img.loading = "lazy";
221
+ img.addEventListener("load", keepBottom); // a live image grows the thread
198
222
  link.appendChild(img);
199
223
  } else {
200
224
  link.className = "att-file";
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Livechat
4
- VERSION = '0.4.6'
4
+ VERSION = '0.4.8'
5
5
  end
@@ -681,6 +681,9 @@
681
681
  img.src = att.url;
682
682
  img.alt = att.name;
683
683
  img.loading = "lazy";
684
+ // The image loads after its bubble is placed and grows the panel —
685
+ // re-pin to the bottom so a just-arrived image stays in view.
686
+ img.addEventListener("load", scrollToBottom);
684
687
  link.appendChild(img);
685
688
  } else {
686
689
  link.className = "lvc-att-file";
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.6
4
+ version: 0.4.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yaroslav Shmarov