chats 0.2.0 → 0.3.1
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 +67 -0
- data/README.md +52 -1
- data/app/assets/stylesheets/chats.css +42 -0
- data/app/controllers/chats/conversations_controller.rb +1 -4
- data/app/helpers/chats/engine_helper.rb +19 -2
- data/app/views/chats/conversations/_conversation_row.html.erb +7 -1
- data/app/views/chats/conversations/_group.html.erb +4 -1
- data/app/views/chats/conversations/show.html.erb +6 -2
- data/app/views/chats/shared/_verified_badge.html.erb +32 -0
- data/config/locales/en.yml +2 -0
- data/config/locales/es.yml +2 -0
- data/lib/chats/configuration.rb +16 -0
- data/lib/chats/macros.rb +12 -3
- data/lib/chats/models/concerns/messager.rb +21 -3
- data/lib/chats/models/conversation.rb +19 -2
- data/lib/chats/version.rb +1 -1
- data/lib/chats.rb +8 -0
- data/lib/generators/chats/templates/initializer.rb +13 -2
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a0e116038dc60b864f51b65edb4f5b879305da0cabef4c8aa2db73e23464c674
|
|
4
|
+
data.tar.gz: '0179846f4e07b60cb1d07a9d50909a836456794636da584a6178843f0d39f8ff'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1c3dc69a2bdba881bc7fc6543bc768885797e121f8d23b619e72a578cb3e7c6555c9b19c6dde52bec3085efe52d98e40dc277af932015bb08f65a7aaf67ca5c1
|
|
7
|
+
data.tar.gz: 4ab1a0ae08133bbe02240e12b1efa8efccb6dfe5bfbf53a1662acce5542a74ec4bb3adfc1eeaf10e6db94a961b17dc81523e8b2ac5044fa02ff22bba6904e303
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,73 @@ All notable changes to this project are documented here.
|
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.3.1] - 2026-09-16
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **The verified badge was a star, not a check.** 0.3.0 shipped a
|
|
11
|
+
five-pointed star as the "official account" mark, copied from a host icon
|
|
12
|
+
that was named `verified_badge` but drawn as a star. A star reads as
|
|
13
|
+
"favourite" or "rated", not "this account is really us", and at 14px a
|
|
14
|
+
solid star and a solid rosette are the same blob. It is now heroicons
|
|
15
|
+
`check-badge`: a scalloped rosette with a tick knocked out of it, which
|
|
16
|
+
is the mark people already read as verified. `fill-rule="evenodd"` is
|
|
17
|
+
what knocks the tick out — without it the rosette fills solid.
|
|
18
|
+
|
|
19
|
+
## [0.3.0] - 2026-09-16
|
|
20
|
+
|
|
21
|
+
Official accounts. Some counterparts are not people you met — they are a
|
|
22
|
+
support desk, an organization, a shop, a brand — and a person should be able
|
|
23
|
+
to tell at a glance. **Nothing changes until a model says so**: 0.2.x
|
|
24
|
+
installs upgrade by bumping the gem, with no migration and no new
|
|
25
|
+
configuration.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- **`acts_as_messager verified: true`.** A messager declares itself an
|
|
29
|
+
OFFICIAL account, and every bundled view that shows its name marks it: the
|
|
30
|
+
inbox row, the stacked inbox row, and the thread header. `Klass.
|
|
31
|
+
chat_verified?` is the class predicate and `Chats.verified?(messager)`
|
|
32
|
+
reads it duck-typed — false for a plain model, a nil, a non-messager — so
|
|
33
|
+
hosts can badge their own screens without a class check. The option is
|
|
34
|
+
independent of the headless ones: a desk is usually headless *and*
|
|
35
|
+
official, a shop is usually official and nothing else. Unlike its boolean
|
|
36
|
+
neighbours it refuses to coerce — `verified: "false"` raises
|
|
37
|
+
`Chats::ConfigurationError` at boot rather than quietly verifying an
|
|
38
|
+
account, because a badge is a trust claim and not a display preference.
|
|
39
|
+
- **The badge itself**, `chats/shared/_verified_badge` — an inline rosette
|
|
40
|
+
that sizes itself from the text it sits beside. It is an image with a
|
|
41
|
+
name, not decoration: `role="img"` plus a localized label
|
|
42
|
+
(`chats.verified.label`, "Official account" / "Cuenta oficial"), and the
|
|
43
|
+
glyph is `aria-hidden` so a screen reader never announces it twice.
|
|
44
|
+
- **`config.verified_badge`** `->(messager) { markup }` — swap the glyph for
|
|
45
|
+
your design system's own, vary it per messager, or return nil for no badge.
|
|
46
|
+
The default (nil) renders the gem's rosette.
|
|
47
|
+
- **`--chats-verified`** (`#0284c7`) — the badge colour, a CSS custom
|
|
48
|
+
property like the rest of the gem's theming, inherited through
|
|
49
|
+
`currentColor`. Not the familiar `#1d9bf0`: the badge is a meaningful
|
|
50
|
+
graphic, so WCAG 1.4.11 asks 3:1 of it, and `#1d9bf0` is 3.00:1 on
|
|
51
|
+
`--chats-bg` but 2.73:1 on `--chats-surface` — the inbox row's HOVER
|
|
52
|
+
background, so it failed exactly while somebody was pointing at it.
|
|
53
|
+
`#0284c7` clears the bar on both grounds and on a dark one, so a host
|
|
54
|
+
inverting the palette inherits a badge that still passes.
|
|
55
|
+
`test/verified_badge_contrast_test.rb` computes it rather than trusting a
|
|
56
|
+
swatch.
|
|
57
|
+
- **`chats_verified_badge(messager)`** — the view helper behind all three
|
|
58
|
+
surfaces, available in host views too. Nil for everyone who hasn't
|
|
59
|
+
declared `verified: true`, so it is safe to drop next to any name
|
|
60
|
+
unconditionally.
|
|
61
|
+
- **`Chats::Conversation#counterpart_for(viewer)`** — the other messager in a
|
|
62
|
+
direct thread (nil for a group, and for a thread whose other seat left).
|
|
63
|
+
The title, the avatar and the badge on an inbox row now resolve the
|
|
64
|
+
counterpart through this one method, memoized per viewer, so a row that
|
|
65
|
+
cost one query in 0.2.0 still costs one.
|
|
66
|
+
|
|
67
|
+
### Changed
|
|
68
|
+
- The bundled inbox row wraps its title in `.chats-row__name`, and the thread
|
|
69
|
+
header wraps its name in `.chats-thread__name`, so a long name still
|
|
70
|
+
ellipsizes while the badge beside it stays visible. If you ejected these
|
|
71
|
+
views with `rails generate chats:views`, your copies are untouched and keep
|
|
72
|
+
working; re-run the generator only if you want the badge.
|
|
73
|
+
|
|
7
74
|
## [0.2.0] - 2026-09-16
|
|
8
75
|
|
|
9
76
|
The release that makes `chats` a foundation other products can be built on:
|
data/README.md
CHANGED
|
@@ -97,7 +97,7 @@ Five concepts, namespaced and polymorphic from day one (no hard `User` coupling
|
|
|
97
97
|
- **`Chats::Participant`** — a messager's seat in a conversation. Holds role, read horizon, mute, soft-leave, and notification bookkeeping.
|
|
98
98
|
- **`Chats::Message`** — `text` (human) or `system` (posted by your app). Soft-deletes to a tombstone. Attachments via ActiveStorage.
|
|
99
99
|
- **`Chats::Reaction`** — one row per (message, reactor, emoji); tap-to-toggle, race-safe.
|
|
100
|
-
- **Any model with `acts_as_messager`** — users, organizations, support desks, bots: participants and senders are polymorphic. A messager that is not a person declares it (`notifications: false, blockable: false, inbox: :grouped`) and the gem stops treating it like one. See [`support_desk`](https://github.com/rameerez/support_desk) for the worked example.
|
|
100
|
+
- **Any model with `acts_as_messager`** — users, organizations, support desks, bots: participants and senders are polymorphic. A messager that is not a person declares it (`notifications: false, blockable: false, inbox: :grouped`) and the gem stops treating it like one; an official one (`verified: true`) gets the badge everywhere its name appears. See [`support_desk`](https://github.com/rameerez/support_desk) for the worked example.
|
|
101
101
|
|
|
102
102
|
Two deliberate design decisions worth knowing:
|
|
103
103
|
|
|
@@ -269,6 +269,53 @@ end
|
|
|
269
269
|
|
|
270
270
|
That's the whole point of the option: **your notifiers and views stop asking `is_a?(User)`**. The predicates are on the class (`SupportDesk.chat_notifications?`, `.chat_blockable?`, `.chat_inbox_mode`) and duck-typed everywhere the gem reads them, so an ordinary `acts_as_messager` model behaves exactly as it always did.
|
|
271
271
|
|
|
272
|
+
## ✅ Official accounts: the verified badge
|
|
273
|
+
|
|
274
|
+
A support desk, an organization, a shop or a brand is an **official** counterpart, and the person talking to it should see that at a glance — the blue tick everyone already reads. Say it once, on the model, next to the other `acts_as_messager` options:
|
|
275
|
+
|
|
276
|
+
```ruby
|
|
277
|
+
class SupportDesk < ApplicationRecord
|
|
278
|
+
acts_as_messager verified: true
|
|
279
|
+
end
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Every bundled view that shows a messager's name now marks it: the inbox row, the stacked inbox row, and the thread header. The mark is an image with a name, not decoration — `role="img"` plus a localized label (`chats.verified.label`: "Official account" / "Cuenta oficial"), with the glyph itself `aria-hidden` so nothing is announced twice.
|
|
283
|
+
|
|
284
|
+
`verified:` is **independent of everything else**. A desk is usually headless *and* official; a shop is usually official and completely ordinary otherwise. Combine what you need:
|
|
285
|
+
|
|
286
|
+
```ruby
|
|
287
|
+
acts_as_messager verified: true # official, notifiable, blockable
|
|
288
|
+
acts_as_messager notifications: false, inbox: :grouped, verified: true # an official desk
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
It is the one boolean option that **refuses to coerce**, and that is deliberate — please don't "fix" it into a `!!` to match its neighbours. `notifications:` and `blockable:` coerce, so `notifications: "false"` quietly means `true`; on those two the damage is a stray notification. Here the same slip would hand an account the mark that tells people it is really us, and the strings that reach a model declaration come from exactly the places that produce `"false"`: an ENV var, a YAML round-trip, a settings row. So `verified: "false"` raises `Chats::ConfigurationError` at boot, where somebody is looking, rather than shipping a verified impostor nobody notices.
|
|
292
|
+
|
|
293
|
+
Read it anywhere you render your own screens — duck-typed, never a class check:
|
|
294
|
+
|
|
295
|
+
```ruby
|
|
296
|
+
SupportDesk.chat_verified? # the class predicate
|
|
297
|
+
Chats.verified?(messager) # false for a plain model, a nil, a non-messager
|
|
298
|
+
chats_verified_badge(messager) # the view helper: markup, or nil for everyone else
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
**Change the colour** with one CSS variable (the badge inherits it through `currentColor`):
|
|
302
|
+
|
|
303
|
+
```css
|
|
304
|
+
:root { --chats-verified: #0284c7; }
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
The default is `#0284c7` rather than the more familiar `#1d9bf0`. The badge is a meaningful graphic, so it owes 3:1 against what it sits on (WCAG 1.4.11), and `#1d9bf0` is 3.00:1 on the page but **2.73:1 on `--chats-surface`** — the inbox row's hover background, so it failed exactly while someone was pointing at it. `#0284c7` clears the bar on both (4.10 and 3.72) and on a dark ground too (4.33 on `#111827`), so inverting the palette doesn't leave you with a badge you have to remember to fix. If you override it, `test/verified_badge_contrast_test.rb` shows the arithmetic worth repeating.
|
|
308
|
+
|
|
309
|
+
**Change the glyph** — to your design system's icon, a per-messager mark, or nothing — with a callable that gets the messager and returns html_safe markup (or `nil` for no badge):
|
|
310
|
+
|
|
311
|
+
```ruby
|
|
312
|
+
config.verified_badge = lambda do |messager|
|
|
313
|
+
ApplicationController.helpers.image_tag("official.svg", class: "badge", alt: "Official account")
|
|
314
|
+
end
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Or eject `app/views/chats/shared/_verified_badge.html.erb` with `rails generate chats:views` and rewrite it.
|
|
318
|
+
|
|
272
319
|
## 🗂️ Grouped inbox rows
|
|
273
320
|
|
|
274
321
|
With `inbox: :grouped`, every direct conversation a viewer has with that messager folds into a single inbox row — a stack:
|
|
@@ -368,6 +415,7 @@ The bundled UI is intentionally framework-free (semantic `chats-*` classes + one
|
|
|
368
415
|
:root {
|
|
369
416
|
--chats-accent: #facc15; /* own bubbles, send button, badges */
|
|
370
417
|
--chats-accent-contrast: #111827;
|
|
418
|
+
--chats-verified: #0284c7; /* the "official account" badge */
|
|
371
419
|
}
|
|
372
420
|
```
|
|
373
421
|
|
|
@@ -429,6 +477,7 @@ Chats.configure do |config|
|
|
|
429
477
|
config.messager_avatar = ->(messager) { messager.avatar } # URL/attachment/variant or nil
|
|
430
478
|
config.messager_url = ->(messager) { nil } # nil ⇒ names render as plain text
|
|
431
479
|
config.message_signature = nil # ->(message) { } for signed bubbles
|
|
480
|
+
config.verified_badge = nil # ->(messager) { markup } for verified: true
|
|
432
481
|
end
|
|
433
482
|
```
|
|
434
483
|
|
|
@@ -445,10 +494,12 @@ alice.chats # inbox relation, newest first
|
|
|
445
494
|
alice.unread_chats_count # conversations with unread messages
|
|
446
495
|
alice.message!(bob, "hi", author: lucia) # written by lucia, sent from alice's seat
|
|
447
496
|
Chats::Inbox.for(alice) # [Conversation | InboxGroup] + #unread_count
|
|
497
|
+
Chats.verified?(desk) # official account? (acts_as_messager verified: true)
|
|
448
498
|
|
|
449
499
|
# Conversations
|
|
450
500
|
conversation.participant?(user) # active membership
|
|
451
501
|
conversation.other_participants(user)
|
|
502
|
+
conversation.counterpart_for(viewer) # the other messager (nil for groups)
|
|
452
503
|
conversation.title_for(viewer) # counterpart name / group title
|
|
453
504
|
conversation.subject_label # "Madrid → Barcelona"
|
|
454
505
|
conversation.unread_count_for(user)
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* :root {
|
|
12
12
|
* --chats-accent: #facc15; /\* brand color: own bubbles, send button *\/
|
|
13
13
|
* --chats-accent-contrast: #111827; /\* text on top of the accent *\/
|
|
14
|
+
* --chats-verified: #0284c7; /\* the "official account" badge *\/
|
|
14
15
|
* }
|
|
15
16
|
*
|
|
16
17
|
* Want a completely different look? `rails g chats:views` ejects the
|
|
@@ -26,6 +27,7 @@
|
|
|
26
27
|
--chats-border: rgba(0, 0, 0, 0.08);
|
|
27
28
|
--chats-text: #111827;
|
|
28
29
|
--chats-text-muted: #6b7280;
|
|
30
|
+
--chats-verified: #0284c7;
|
|
29
31
|
--chats-bubble-bg: #f3f4f6;
|
|
30
32
|
--chats-bubble-text: #111827;
|
|
31
33
|
--chats-radius: 1.1rem;
|
|
@@ -79,6 +81,12 @@
|
|
|
79
81
|
justify-content: space-between;
|
|
80
82
|
gap: 0.75rem;
|
|
81
83
|
}
|
|
84
|
+
.chats-row__name {
|
|
85
|
+
display: flex;
|
|
86
|
+
align-items: center;
|
|
87
|
+
gap: 0.25rem;
|
|
88
|
+
min-width: 0;
|
|
89
|
+
}
|
|
82
90
|
.chats-row__title {
|
|
83
91
|
font-weight: 650;
|
|
84
92
|
white-space: nowrap;
|
|
@@ -147,6 +155,34 @@
|
|
|
147
155
|
user-select: none;
|
|
148
156
|
}
|
|
149
157
|
|
|
158
|
+
/* --- Verified messagers ------------------------------------------------------
|
|
159
|
+
*
|
|
160
|
+
* The "official account" mark (`acts_as_messager verified: true`). It sizes
|
|
161
|
+
* itself from the text it sits next to, so it reads right in an inbox row and
|
|
162
|
+
* in the thread header without either one hard-coding a pixel value. Recolour
|
|
163
|
+
* it with --chats-verified; replace the glyph with config.verified_badge.
|
|
164
|
+
*
|
|
165
|
+
* On #0284c7 rather than the more familiar #1d9bf0: the badge is a meaningful
|
|
166
|
+
* graphic, so WCAG 1.4.11 asks for 3:1 against what it sits on, and #1d9bf0
|
|
167
|
+
* is 3.00:1 on --chats-bg but only 2.73:1 on --chats-surface — which is the
|
|
168
|
+
* row hover background, so the mark failed exactly while someone was pointing
|
|
169
|
+
* at it. #0284c7 clears 3:1 on both (4.10 and 3.72) and on a dark ground too
|
|
170
|
+
* (4.33 on #111827), so a host that inverts the palette inherits a badge that
|
|
171
|
+
* still passes instead of one it has to remember to fix. Pinned by
|
|
172
|
+
* test/verified_badge_contrast_test.rb.
|
|
173
|
+
*/
|
|
174
|
+
.chats-verified {
|
|
175
|
+
flex-shrink: 0;
|
|
176
|
+
display: inline-flex;
|
|
177
|
+
align-items: center;
|
|
178
|
+
color: var(--chats-verified);
|
|
179
|
+
line-height: 0;
|
|
180
|
+
}
|
|
181
|
+
.chats-verified__glyph {
|
|
182
|
+
width: 1em;
|
|
183
|
+
height: 1em;
|
|
184
|
+
}
|
|
185
|
+
|
|
150
186
|
/* --- Thread -------------------------------------------------------------------- */
|
|
151
187
|
|
|
152
188
|
.chats-thread {
|
|
@@ -179,6 +215,12 @@
|
|
|
179
215
|
font-size: 1rem;
|
|
180
216
|
font-weight: 750;
|
|
181
217
|
margin: 0;
|
|
218
|
+
display: flex;
|
|
219
|
+
align-items: center;
|
|
220
|
+
gap: 0.25rem;
|
|
221
|
+
min-width: 0;
|
|
222
|
+
}
|
|
223
|
+
.chats-thread__name {
|
|
182
224
|
white-space: nowrap;
|
|
183
225
|
overflow: hidden;
|
|
184
226
|
text-overflow: ellipsis;
|
|
@@ -189,10 +189,7 @@ module Chats
|
|
|
189
189
|
def chats_counterpart
|
|
190
190
|
return @chats_counterpart if defined?(@chats_counterpart)
|
|
191
191
|
|
|
192
|
-
@chats_counterpart =
|
|
193
|
-
if @conversation&.direct?
|
|
194
|
-
@conversation.other_participants(chats_current_messager).includes(:messager).first&.messager
|
|
195
|
-
end
|
|
192
|
+
@chats_counterpart = @conversation&.counterpart_for(chats_current_messager)
|
|
196
193
|
end
|
|
197
194
|
end
|
|
198
195
|
end
|
|
@@ -128,8 +128,7 @@ module Chats
|
|
|
128
128
|
# initials disc from the group name.
|
|
129
129
|
def chats_conversation_avatar(conversation, viewer)
|
|
130
130
|
if conversation.direct?
|
|
131
|
-
|
|
132
|
-
chats_messager_avatar(other&.messager)
|
|
131
|
+
chats_messager_avatar(conversation.counterpart_for(viewer))
|
|
133
132
|
else
|
|
134
133
|
initials = conversation.title_for(viewer).split.first(2).map { |word| word[0] }.join.upcase
|
|
135
134
|
tag.span(initials.presence || "👥", class: "chats-avatar chats-avatar--initials chats-avatar--group",
|
|
@@ -195,6 +194,24 @@ module Chats
|
|
|
195
194
|
url.present? ? link_to(name, url, class: css_class) : tag.span(name, class: css_class)
|
|
196
195
|
end
|
|
197
196
|
|
|
197
|
+
# The "official account" mark for a messager declared `acts_as_messager
|
|
198
|
+
# verified: true`, and NIL for everyone else — so any view, bundled or
|
|
199
|
+
# host, can drop it next to a name unconditionally:
|
|
200
|
+
#
|
|
201
|
+
# <%= chats_messager_name(author) %><%= chats_verified_badge(author) %>
|
|
202
|
+
#
|
|
203
|
+
# `config.verified_badge` swaps the glyph for the host's own design
|
|
204
|
+
# system; the default renders `chats/shared/_verified_badge`, whose
|
|
205
|
+
# colour is the `--chats-verified` CSS variable.
|
|
206
|
+
def chats_verified_badge(messager)
|
|
207
|
+
return unless Chats.verified?(messager)
|
|
208
|
+
|
|
209
|
+
custom = Chats.config.verified_badge
|
|
210
|
+
return custom.call(messager) if custom
|
|
211
|
+
|
|
212
|
+
render(partial: "chats/shared/verified_badge", locals: { messager: messager })
|
|
213
|
+
end
|
|
214
|
+
|
|
198
215
|
# The signature line under a signed message ("— Lucía G."), or nil.
|
|
199
216
|
def chats_message_signature(message)
|
|
200
217
|
Chats.message_signature_for(message)
|
|
@@ -7,7 +7,13 @@
|
|
|
7
7
|
|
|
8
8
|
<span class="chats-row__body">
|
|
9
9
|
<span class="chats-row__top">
|
|
10
|
-
|
|
10
|
+
<%# The badge sits OUTSIDE the ellipsizing title so a long name
|
|
11
|
+
truncates without ever eating the mark. Nil for an ordinary
|
|
12
|
+
counterpart and for groups, so the row is unchanged. %>
|
|
13
|
+
<span class="chats-row__name">
|
|
14
|
+
<span class="chats-row__title"><%= conversation.title_for(viewer) %></span>
|
|
15
|
+
<%= chats_verified_badge(conversation.counterpart_for(viewer)) %>
|
|
16
|
+
</span>
|
|
11
17
|
<time class="chats-row__time" datetime="<%= conversation.last_message_at&.iso8601 %>">
|
|
12
18
|
<%= chats_timestamp(conversation.last_message_at) %>
|
|
13
19
|
</time>
|
|
@@ -13,7 +13,10 @@
|
|
|
13
13
|
|
|
14
14
|
<span class="chats-row__body">
|
|
15
15
|
<span class="chats-row__top">
|
|
16
|
-
<span class="chats-
|
|
16
|
+
<span class="chats-row__name">
|
|
17
|
+
<span class="chats-row__title"><%= group.title_for(viewer) %></span>
|
|
18
|
+
<%= chats_verified_badge(group.messager) %>
|
|
19
|
+
</span>
|
|
17
20
|
<time class="chats-row__time" datetime="<%= group.last_message_at&.iso8601 %>">
|
|
18
21
|
<%= chats_timestamp(group.last_message_at) %>
|
|
19
22
|
</time>
|
|
@@ -44,11 +44,15 @@
|
|
|
44
44
|
<% end %>
|
|
45
45
|
|
|
46
46
|
<div class="chats-thread__identity">
|
|
47
|
+
<%# Both arms wrap the name in .chats-thread__name: the <h1> is a flex
|
|
48
|
+
row (the badge is its second child), and only a real element can
|
|
49
|
+
carry the ellipsis. %>
|
|
47
50
|
<h1 class="chats-thread__title">
|
|
48
51
|
<% if chats_counterpart %>
|
|
49
|
-
<%= chats_messager_name(chats_counterpart) %>
|
|
52
|
+
<%= chats_messager_name(chats_counterpart, css_class: "chats-thread__name") %>
|
|
53
|
+
<%= chats_verified_badge(chats_counterpart) %>
|
|
50
54
|
<% else %>
|
|
51
|
-
|
|
55
|
+
<span class="chats-thread__name"><%= @conversation.title_for(chats_current_messager) %></span>
|
|
52
56
|
<% end %>
|
|
53
57
|
</h1>
|
|
54
58
|
<% if @conversation.subject_label %>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<%# The "official account" mark. Rendered next to a verified messager's name
|
|
2
|
+
(`acts_as_messager verified: true`) in every bundled view that shows one:
|
|
3
|
+
the inbox row, the stacked row and the thread header.
|
|
4
|
+
|
|
5
|
+
It is an IMAGE with a name, not decoration: the wrapper carries
|
|
6
|
+
role="img" plus the localized label, so a screen reader announces
|
|
7
|
+
"Cuenta oficial" / "Official account" where a sighted person sees the
|
|
8
|
+
badge, and the glyph itself is aria-hidden so it is never read twice.
|
|
9
|
+
|
|
10
|
+
The glyph is heroicons `check-badge` (24/solid): a scalloped rosette
|
|
11
|
+
with a TICK knocked out of it, which is the mark people already read as
|
|
12
|
+
"verified". It is deliberately not a plain star — a star says "favourite"
|
|
13
|
+
or "rated", and at 14px a solid star and a solid rosette are the same
|
|
14
|
+
blob, so the tick is the whole point. `fill-rule: evenodd` is what knocks
|
|
15
|
+
the tick out; without it the rosette fills solid and the mark is a blob.
|
|
16
|
+
|
|
17
|
+
Colour comes from the --chats-verified CSS variable (see chats.css) via
|
|
18
|
+
`fill: currentColor` — override that variable, or swap this whole partial
|
|
19
|
+
with `config.verified_badge`. %>
|
|
20
|
+
<span class="chats-verified"
|
|
21
|
+
role="img"
|
|
22
|
+
title="<%= t("chats.verified.label") %>"
|
|
23
|
+
aria-label="<%= t("chats.verified.label") %>">
|
|
24
|
+
<svg xmlns="http://www.w3.org/2000/svg"
|
|
25
|
+
viewBox="0 0 24 24"
|
|
26
|
+
fill="currentColor"
|
|
27
|
+
class="chats-verified__glyph"
|
|
28
|
+
aria-hidden="true"
|
|
29
|
+
focusable="false">
|
|
30
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.603 3.799A4.49 4.49 0 0 1 12 2.25c1.357 0 2.573.6 3.397 1.549a4.49 4.49 0 0 1 3.498 1.307 4.491 4.491 0 0 1 1.307 3.497A4.49 4.49 0 0 1 21.75 12a4.49 4.49 0 0 1-1.549 3.397 4.491 4.491 0 0 1-1.307 3.497 4.491 4.491 0 0 1-3.497 1.307A4.49 4.49 0 0 1 12 21.75a4.49 4.49 0 0 1-3.397-1.549 4.49 4.49 0 0 1-3.498-1.306 4.491 4.491 0 0 1-1.307-3.498A4.49 4.49 0 0 1 2.25 12c0-1.357.6-2.573 1.549-3.397a4.49 4.49 0 0 1 1.307-3.497 4.49 4.49 0 0 1 3.497-1.307Zm7.007 6.387a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z" />
|
|
31
|
+
</svg>
|
|
32
|
+
</span>
|
data/config/locales/en.yml
CHANGED
data/config/locales/es.yml
CHANGED
data/lib/chats/configuration.rb
CHANGED
|
@@ -153,6 +153,17 @@ module Chats
|
|
|
153
153
|
# "— Author Name".
|
|
154
154
|
attr_reader :message_signature
|
|
155
155
|
|
|
156
|
+
# ->(messager) { markup } — the badge shown next to an OFFICIAL
|
|
157
|
+
# account's name (`acts_as_messager verified: true`). nil (the default)
|
|
158
|
+
# renders the gem's own rosette, `chats/shared/_verified_badge`, coloured
|
|
159
|
+
# by the `--chats-verified` CSS variable.
|
|
160
|
+
#
|
|
161
|
+
# Return an html_safe value (anything `render`, `tag` or `image_tag`
|
|
162
|
+
# gives you) — a bare String is escaped, exactly as it would be anywhere
|
|
163
|
+
# else in a Rails view. Returning nil renders nothing, so a host can
|
|
164
|
+
# suppress the badge on some messagers without touching the models.
|
|
165
|
+
attr_reader :verified_badge
|
|
166
|
+
|
|
156
167
|
def initialize
|
|
157
168
|
@messager_class = "User"
|
|
158
169
|
@parent_controller = "::ApplicationController"
|
|
@@ -187,6 +198,7 @@ module Chats
|
|
|
187
198
|
@notifier = ->(_event, **_payload) {}
|
|
188
199
|
@messager_url = ->(_messager) { nil }
|
|
189
200
|
@message_signature = nil
|
|
201
|
+
@verified_badge = nil
|
|
190
202
|
|
|
191
203
|
@messager_display_name = lambda do |messager|
|
|
192
204
|
messager.try(:display_name) || messager.try(:name) ||
|
|
@@ -296,6 +308,10 @@ module Chats
|
|
|
296
308
|
@message_signature = value.nil? ? nil : ensure_callable(value, "message_signature")
|
|
297
309
|
end
|
|
298
310
|
|
|
311
|
+
def verified_badge=(value)
|
|
312
|
+
@verified_badge = value.nil? ? nil : ensure_callable(value, "verified_badge")
|
|
313
|
+
end
|
|
314
|
+
|
|
299
315
|
def messager_display_name=(value)
|
|
300
316
|
@messager_display_name = ensure_callable(value, "messager_display_name")
|
|
301
317
|
end
|
data/lib/chats/macros.rb
CHANGED
|
@@ -29,18 +29,27 @@ module Chats
|
|
|
29
29
|
# stacks into ONE inbox row (see Chats::Inbox).
|
|
30
30
|
# group_path: ->(viewer) { } where that stacked row links to; defaults
|
|
31
31
|
# to the filtered inbox (`?with=<sgid>`).
|
|
32
|
+
# verified: true this messager is an OFFICIAL account — a support
|
|
33
|
+
# desk, an organization, a brand. The bundled views
|
|
34
|
+
# mark its name with the rosette everyone already
|
|
35
|
+
# reads as "verified". Strictly true/false: a badge
|
|
36
|
+
# is a trust claim, so a stray "false" string must
|
|
37
|
+
# raise rather than quietly verify.
|
|
32
38
|
#
|
|
33
39
|
# class Desk < ApplicationRecord
|
|
34
|
-
# acts_as_messager notifications: false, blockable: false, inbox: :grouped
|
|
40
|
+
# acts_as_messager notifications: false, blockable: false, inbox: :grouped,
|
|
41
|
+
# verified: true
|
|
35
42
|
# end
|
|
36
|
-
def acts_as_messager(notifications: true, blockable: true, inbox: :default, group_path: nil
|
|
43
|
+
def acts_as_messager(notifications: true, blockable: true, inbox: :default, group_path: nil,
|
|
44
|
+
verified: false)
|
|
37
45
|
include Chats::Messager
|
|
38
46
|
|
|
39
47
|
self.chat_options = Chats::Messager.normalize_options(
|
|
40
48
|
notifications: notifications,
|
|
41
49
|
blockable: blockable,
|
|
42
50
|
inbox: inbox,
|
|
43
|
-
group_path: group_path
|
|
51
|
+
group_path: group_path,
|
|
52
|
+
verified: verified
|
|
44
53
|
)
|
|
45
54
|
end
|
|
46
55
|
|
|
@@ -22,14 +22,15 @@ module Chats
|
|
|
22
22
|
notifications: true,
|
|
23
23
|
blockable: true,
|
|
24
24
|
inbox: :default,
|
|
25
|
-
group_path: nil
|
|
25
|
+
group_path: nil,
|
|
26
|
+
verified: false
|
|
26
27
|
}.freeze
|
|
27
28
|
|
|
28
29
|
INBOX_MODES = %i[default grouped].freeze
|
|
29
30
|
|
|
30
31
|
# Validate + freeze the macro's options, failing at BOOT with a plain
|
|
31
32
|
# English message rather than at 3am with a NoMethodError.
|
|
32
|
-
def self.normalize_options(notifications:, blockable:, inbox:, group_path:)
|
|
33
|
+
def self.normalize_options(notifications:, blockable:, inbox:, group_path:, verified: false)
|
|
33
34
|
inbox = inbox.to_sym
|
|
34
35
|
unless INBOX_MODES.include?(inbox)
|
|
35
36
|
raise Chats::ConfigurationError,
|
|
@@ -41,11 +42,21 @@ module Chats
|
|
|
41
42
|
"acts_as_messager group_path: must respond to #call (a proc/lambda), got #{group_path.inspect}"
|
|
42
43
|
end
|
|
43
44
|
|
|
45
|
+
# Deliberately STRICTER than its boolean neighbours, which coerce with
|
|
46
|
+
# `!!`. "Official account" is a trust claim shown to everyone who talks
|
|
47
|
+
# to this messager, so `verified: "false"` (an ENV var, a YAML
|
|
48
|
+
# round-trip) has to fail at boot rather than quietly verify it.
|
|
49
|
+
unless [true, false].include?(verified)
|
|
50
|
+
raise Chats::ConfigurationError,
|
|
51
|
+
"acts_as_messager verified: must be true or false, got #{verified.inspect}"
|
|
52
|
+
end
|
|
53
|
+
|
|
44
54
|
{
|
|
45
55
|
notifications: !!notifications,
|
|
46
56
|
blockable: !!blockable,
|
|
47
57
|
inbox: inbox,
|
|
48
|
-
group_path: group_path
|
|
58
|
+
group_path: group_path,
|
|
59
|
+
verified: verified
|
|
49
60
|
}.freeze
|
|
50
61
|
end
|
|
51
62
|
|
|
@@ -76,6 +87,13 @@ module Chats
|
|
|
76
87
|
def chat_group_path
|
|
77
88
|
chat_options[:group_path]
|
|
78
89
|
end
|
|
90
|
+
|
|
91
|
+
# True when declared with `acts_as_messager verified: true` — an
|
|
92
|
+
# OFFICIAL account (a support desk, an organization, a brand). The
|
|
93
|
+
# bundled views badge its name wherever they show it.
|
|
94
|
+
def chat_verified?
|
|
95
|
+
chat_options[:verified]
|
|
96
|
+
end
|
|
79
97
|
end
|
|
80
98
|
|
|
81
99
|
included do
|
|
@@ -222,13 +222,30 @@ module Chats
|
|
|
222
222
|
)
|
|
223
223
|
end
|
|
224
224
|
|
|
225
|
+
# The OTHER messager in a direct thread, from +viewer+'s seat — nil for a
|
|
226
|
+
# group, and nil for a direct thread whose other seat has left. The one
|
|
227
|
+
# place the counterpart is resolved, so the title, the avatar and the
|
|
228
|
+
# verified badge on an inbox row always name the same person.
|
|
229
|
+
#
|
|
230
|
+
# Memoized per viewer: an inbox row asks for it two or three times, and a
|
|
231
|
+
# row that cost one query in 0.2.0 must not start costing three.
|
|
232
|
+
def counterpart_for(viewer)
|
|
233
|
+
return nil unless direct?
|
|
234
|
+
|
|
235
|
+
@counterparts ||= {}
|
|
236
|
+
key = viewer && Chats.messager_key(viewer)
|
|
237
|
+
return @counterparts[key] if @counterparts.key?(key)
|
|
238
|
+
|
|
239
|
+
@counterparts[key] = other_participants(viewer).includes(:messager).first&.messager
|
|
240
|
+
end
|
|
241
|
+
|
|
225
242
|
# What this conversation is called from +viewer+'s seat: a direct thread
|
|
226
243
|
# is named after the counterpart; a group after its title (or its
|
|
227
244
|
# members, when untitled).
|
|
228
245
|
def title_for(viewer)
|
|
229
246
|
if direct?
|
|
230
|
-
other =
|
|
231
|
-
other ? Chats.display_name_for(other
|
|
247
|
+
other = counterpart_for(viewer)
|
|
248
|
+
other ? Chats.display_name_for(other) : I18n.t("chats.conversation.empty_title")
|
|
232
249
|
else
|
|
233
250
|
title.presence || participants.active.includes(:messager).limit(4).map do |p|
|
|
234
251
|
Chats.display_name_for(p.messager)
|
data/lib/chats/version.rb
CHANGED
data/lib/chats.rb
CHANGED
|
@@ -219,6 +219,14 @@ module Chats
|
|
|
219
219
|
messager_option(messager, :chat_grouped_inbox?, default: false)
|
|
220
220
|
end
|
|
221
221
|
|
|
222
|
+
# Whether +messager+ is an OFFICIAL account (`acts_as_messager verified:
|
|
223
|
+
# true`) — a support desk, an organization, a brand. The bundled views
|
|
224
|
+
# badge its name; hosts can read it to do the same on their own screens.
|
|
225
|
+
# Defaults to false: nothing is verified until a model says so.
|
|
226
|
+
def verified?(messager)
|
|
227
|
+
messager_option(messager, :chat_verified?, default: false)
|
|
228
|
+
end
|
|
229
|
+
|
|
222
230
|
# The polymorphic type names of every registered messager class that
|
|
223
231
|
# stacks (`inbox: :grouped`). Empty in an ordinary app — which is what
|
|
224
232
|
# keeps the inbox query there byte-identical to 0.1.x. Used as a SQL
|
|
@@ -19,8 +19,10 @@ Chats.configure do |config|
|
|
|
19
19
|
# class SupportDesk < ApplicationRecord
|
|
20
20
|
# acts_as_messager notifications: false, # never notifiable
|
|
21
21
|
# blockable: false, # no block/report affordances
|
|
22
|
-
# inbox: :grouped # every thread with it is ONE
|
|
23
|
-
#
|
|
22
|
+
# inbox: :grouped, # every thread with it is ONE
|
|
23
|
+
# # inbox row (a "stack")
|
|
24
|
+
# verified: true # an OFFICIAL account: the
|
|
25
|
+
# # views badge its name
|
|
24
26
|
# end
|
|
25
27
|
#
|
|
26
28
|
# `group_path:` says where that stacked row goes when it holds more than
|
|
@@ -200,6 +202,15 @@ Chats.configure do |config|
|
|
|
200
202
|
# localized "— Agent Name":
|
|
201
203
|
#
|
|
202
204
|
# config.message_signature = ->(message) { "answered by #{message.author.first_name}" }
|
|
205
|
+
#
|
|
206
|
+
# The "official account" badge next to a `verified: true` messager's name.
|
|
207
|
+
# nil (the default) renders chats' own rosette, recoloured by the
|
|
208
|
+
# `--chats-verified` CSS variable. Set this to hand back your design
|
|
209
|
+
# system's mark instead — return html_safe markup, or nil for no badge:
|
|
210
|
+
#
|
|
211
|
+
# config.verified_badge = lambda do |messager|
|
|
212
|
+
# ApplicationController.helpers.image_tag("official.svg", class: "badge", alt: "Official account")
|
|
213
|
+
# end
|
|
203
214
|
|
|
204
215
|
# ==========================================================================
|
|
205
216
|
# SLOTS — add one row or one button without ejecting a screen
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: chats
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rameerez
|
|
@@ -151,6 +151,7 @@ files:
|
|
|
151
151
|
- app/views/chats/messages/errors.turbo_stream.erb
|
|
152
152
|
- app/views/chats/messages/locked.turbo_stream.erb
|
|
153
153
|
- app/views/chats/shared/_unread_badge.html.erb
|
|
154
|
+
- app/views/chats/shared/_verified_badge.html.erb
|
|
154
155
|
- config/importmap.rb
|
|
155
156
|
- config/locales/en.yml
|
|
156
157
|
- config/locales/es.yml
|