chats 0.6.0 → 0.6.2
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 +53 -0
- data/README.md +4 -1
- data/app/assets/stylesheets/chats.css +20 -6
- data/app/controllers/chats/conversations_controller.rb +6 -1
- data/app/helpers/chats/engine_helper.rb +15 -0
- data/app/javascript/chats/split_controller.js +34 -0
- data/app/javascript/chats/thread_controller.js +5 -0
- data/app/views/chats/conversations/_conversation_pane.html.erb +12 -1
- data/app/views/chats/conversations/index.html.erb +4 -0
- data/app/views/chats/conversations/pane.html.erb +3 -1
- data/app/views/chats/conversations/show.html.erb +1 -0
- data/config/locales/en.yml +3 -0
- data/config/locales/es.yml +3 -0
- data/lib/chats/configuration.rb +13 -0
- data/lib/chats/models/conversation.rb +10 -2
- data/lib/chats/models/message.rb +19 -3
- data/lib/chats/version.rb +1 -1
- data/lib/generators/chats/templates/initializer.rb +8 -0
- metadata +2 -9
- data/chats-0.3.1.gem +0 -0
- data/chats-0.3.2.gem +0 -0
- data/chats-0.4.0.gem +0 -0
- data/chats-0.4.1.gem +0 -0
- data/chats-0.4.2.gem +0 -0
- data/chats-0.5.0.gem +0 -0
- data/chats-0.5.1.gem +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fa521d419a2dc6b1d672e2919c4db795674c744fd97df02027ce33b81f582675
|
|
4
|
+
data.tar.gz: c64cf6cfad1d3f41b9eaf9938936017ecb3af6840feb74f342699c7e0d5d6a01
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 99f45e5ce16f754d2e32a39bffa6664c4205fd401882b2794152a339eacfc6bb155514303c3e483e68f48f0e7dc52028ea0a7ce9180d776c3083df1b6ff3dd29
|
|
7
|
+
data.tar.gz: 660e92ea36bc25cea9ad374479495c32f5fcbd1f04f661cfff561eb9f6ce300e8afecd0b5a8294d30630e7577cd35a1f81395ae2848a806be60046b729f25f46
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,59 @@ 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.6.2] - 2026-10-18
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **A moderation removal that removed nothing said it had.**
|
|
11
|
+
`Chats::Message#remove_reported_field!(:body)`, the seam moderate calls to
|
|
12
|
+
take reported content down, returned true for a message that was already
|
|
13
|
+
deleted, so moderate recorded a second "content removed" with a statement
|
|
14
|
+
of reasons that was false. It now returns whether anything was removed:
|
|
15
|
+
false for a message already deleted (by its sender or an earlier
|
|
16
|
+
decision), one whose body is already blank, and one already gone
|
|
17
|
+
(`deletion: :hard`); true only when it actually removed something (the
|
|
18
|
+
tombstone, or the row under `deletion: :hard`). It decides under the
|
|
19
|
+
message's row lock and reads the message again under it, so two
|
|
20
|
+
decisions taken at once cannot both see it standing. moderate
|
|
21
|
+
1.0.0.beta3 and later refuse a removal that removed nothing; earlier
|
|
22
|
+
versions ignore the return value. A group's `remove_reported_field!(:title)`
|
|
23
|
+
keeps the same contract (false for a title already removed).
|
|
24
|
+
|
|
25
|
+
## [0.6.1] - 2026-10-18
|
|
26
|
+
|
|
27
|
+
The package is the gem again, and three fixes to the two-pane page.
|
|
28
|
+
**No migration, nothing to configure.**
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- **The package shipped seven old packages.** The gemspec takes its files
|
|
32
|
+
from `git ls-files`, and the built `chats-0.3.2.gem` … `chats-0.5.1.gem`
|
|
33
|
+
had been committed (`chats-0.3.1.gem` since 0.3.1), so 0.6.0 weighed
|
|
34
|
+
3.5 MB. They are untracked, `*.gem` is in `.gitignore`, and the gemspec
|
|
35
|
+
leaves out any `*.gem` it is handed anyway; `test/gemspec_test.rb` keeps
|
|
36
|
+
it so. The README's screenshots still ship, since the README links them.
|
|
37
|
+
- **The tab kept the previous page's title after a row click.** Turbo
|
|
38
|
+
leaves `<head>` alone when it promotes a frame load to a visit, so from
|
|
39
|
+
`/messages/A` clicking B left A's title. The messaging page's title now
|
|
40
|
+
has one source, the new `chats_page_title` helper ("Messages", "Bob ·
|
|
41
|
+
Messages": `chats.page_title.inbox` / `.conversation`, localized), which
|
|
42
|
+
`config.page_title = ->(conversation, viewer) { … }` rewords. The bundled
|
|
43
|
+
`index` and `show` give it to `content_for :title` (a layout doing
|
|
44
|
+
`yield(:title)` shows it on a full page), and the pane carries it
|
|
45
|
+
(`data-title`), which `chats--split` gives the tab when a row opens it.
|
|
46
|
+
The pane's answer stays in turbo-rails' minimal frame layout, now even
|
|
47
|
+
under a `config.layout`. A layout that titles pages from what the
|
|
48
|
+
controller loaded keeps its title on full pages and shows
|
|
49
|
+
`chats_page_title` after a row click: say it the same way in
|
|
50
|
+
`config.page_title`.
|
|
51
|
+
- **A tapped row kept its hover tint on touch screens.** Every hover rule
|
|
52
|
+
on inbox rows and on the dock (rows, the bar's chevron, "See all
|
|
53
|
+
messages", the window buttons) now sits inside `@media (hover: hover)`.
|
|
54
|
+
- **The open row kept its unread count until the list's live refresh.** The
|
|
55
|
+
pane says when the server read the conversation as it rendered it
|
|
56
|
+
(`data-read`), and the thread now dispatches `chats:read` once a read it
|
|
57
|
+
posted lands; on either, `chats--split` clears that row's count and bold
|
|
58
|
+
at once. The refresh that follows renders the same.
|
|
59
|
+
|
|
7
60
|
## [0.6.0] - 2026-10-18
|
|
8
61
|
|
|
9
62
|
The messaging page is two panes, LinkedIn-style: on a wide screen
|
data/README.md
CHANGED
|
@@ -253,7 +253,7 @@ Use **`:flag`, never `:block`** for chat: you don't gag someone mid-conversation
|
|
|
253
253
|
|---|---|
|
|
254
254
|
| `reported_owner` | the sender (who a decision notifies, who a ban targets) |
|
|
255
255
|
| `moderation_snapshot(:body)` | the body — frozen as evidence at report time, surviving later edits/deletes |
|
|
256
|
-
| `remove_reported_field!(:body)` | the **soft-delete tombstone** — a moderator's removal looks exactly like a user deletion ("Message deleted"), no special admin rendering path |
|
|
256
|
+
| `remove_reported_field!(:body)` | the **soft-delete tombstone** — a moderator's removal looks exactly like a user deletion ("Message deleted"), no special admin rendering path. Returns whether anything was removed: `true` only when it actually removed something (the tombstone, or the row under `deletion: :hard`), `false` for a message already deleted (by its sender or an earlier decision), already gone, or whose body is already blank, decided under the message's row lock so two decisions at once can't both remove it. moderate 1.0.0.beta3 and later refuse a removal that removed nothing; earlier versions ignore the return value. A group's `remove_reported_field!(:title)` keeps the same contract |
|
|
257
257
|
| `report_visible_to?(viewer)` | participants only (a DM isn't public content), and never the author |
|
|
258
258
|
| `moderation_field_value(:files)` / change detection | attachment-aware seams so image filters classify what actually changed |
|
|
259
259
|
|
|
@@ -598,6 +598,8 @@ Each row is a real link to `/messages/:id` (keeping the list's search or stack i
|
|
|
598
598
|
|
|
599
599
|
**Reads** happen when you open a conversation: a row clicked, a link, a reload. The page's own refreshes never read (one may arrive in a background tab), and a conversation the page renders unread reads itself as soon as it's on screen.
|
|
600
600
|
|
|
601
|
+
**The page's title** comes from one place, `chats_page_title`: "Messages" for the inbox, "Bob · Messages" with a conversation open (`chats.page_title.*`, localized). The bundled pages hand it to `content_for :title`, so a layout that shows `<title><%= content_for(:title) || "My app" %></title>` (the Rails default) gets it, and the pane carries it, so the tab follows when a row opens a conversation (Turbo leaves `<head>` alone on a frame visit). Word it your way, for both, with `config.page_title = ->(conversation, viewer) { … }` (`conversation` is nil on the inbox). A layout that builds its own title from what the controller loaded (a `@page_title`) keeps it on full page loads, and shows `chats_page_title` after a row click: say it the same way in `config.page_title`.
|
|
602
|
+
|
|
601
603
|
The [messaging dock](#-messaging-dock) stays off this page, as on every engine page; its "See all messages" opens it, and a window's ⤢ opens its conversation here.
|
|
602
604
|
|
|
603
605
|
```ruby
|
|
@@ -770,6 +772,7 @@ Chats.configure do |config|
|
|
|
770
772
|
config.messager_url = ->(messager) { nil } # nil ⇒ names render as plain text
|
|
771
773
|
config.message_signature = nil # ->(message) { } for signed bubbles
|
|
772
774
|
config.verified_badge = nil # ->(messager) { markup } for verified: true
|
|
775
|
+
config.page_title = nil # ->(conversation, viewer) { String }; nil ⇒ "Bob · Messages"
|
|
773
776
|
end
|
|
774
777
|
```
|
|
775
778
|
|
|
@@ -86,7 +86,11 @@
|
|
|
86
86
|
text-decoration: none;
|
|
87
87
|
color: inherit;
|
|
88
88
|
}
|
|
89
|
-
|
|
89
|
+
/* Hover only where there is one: a touch screen "hovers" what it tapped and
|
|
90
|
+
keeps the tint after the finger is gone (the open row stayed grey). */
|
|
91
|
+
@media (hover: hover) {
|
|
92
|
+
.chats-row__link:hover { background: var(--chats-surface); }
|
|
93
|
+
}
|
|
90
94
|
.chats-row__body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.1rem; }
|
|
91
95
|
.chats-row__top, .chats-row__bottom {
|
|
92
96
|
display: flex;
|
|
@@ -1312,7 +1316,9 @@ turbo-frame[id^="chats_page_"] {
|
|
|
1312
1316
|
height: 18px;
|
|
1313
1317
|
transition: transform var(--chats-dock-duration) var(--chats-dock-ease);
|
|
1314
1318
|
}
|
|
1315
|
-
|
|
1319
|
+
@media (hover: hover) {
|
|
1320
|
+
.chats-dock__toggle:hover .chats-dock__chevron { background: var(--chats-dock-hover); color: var(--chats-text); }
|
|
1321
|
+
}
|
|
1316
1322
|
.chats-dock__panel[open] .chats-dock__chevron svg { transform: rotate(180deg); }
|
|
1317
1323
|
.chats-dock__close-glyph { display: none; }
|
|
1318
1324
|
|
|
@@ -1350,7 +1356,9 @@ turbo-frame[id^="chats_page_"] {
|
|
|
1350
1356
|
}
|
|
1351
1357
|
.chats-dock__list { list-style: none; margin: 0; padding: 4px 0; }
|
|
1352
1358
|
.chats-dock__list .chats-row__link { padding: 9px 12px; gap: 10px; }
|
|
1353
|
-
|
|
1359
|
+
@media (hover: hover) {
|
|
1360
|
+
.chats-dock__list .chats-row__link:hover { background: var(--chats-dock-hover); }
|
|
1361
|
+
}
|
|
1354
1362
|
.chats-dock__list .chats-avatar { width: 40px; height: 40px; font-size: 0.85rem; }
|
|
1355
1363
|
.chats-dock__list .chats-row__title { font-size: 0.875rem; }
|
|
1356
1364
|
.chats-dock__list .chats-row__subject { font-size: 0.6875rem; }
|
|
@@ -1371,7 +1379,9 @@ turbo-frame[id^="chats_page_"] {
|
|
|
1371
1379
|
font-weight: 650;
|
|
1372
1380
|
text-decoration: none;
|
|
1373
1381
|
}
|
|
1374
|
-
|
|
1382
|
+
@media (hover: hover) {
|
|
1383
|
+
.chats-dock__see-all:hover { background: var(--chats-dock-hover); text-decoration: underline; }
|
|
1384
|
+
}
|
|
1375
1385
|
|
|
1376
1386
|
.chats-dock .chats-empty { padding: 40px 20px; }
|
|
1377
1387
|
.chats-dock .chats-empty__icon { font-size: 2rem; margin-bottom: 0.5rem; }
|
|
@@ -1453,7 +1463,9 @@ turbo-frame[id^="chats_page_"] {
|
|
|
1453
1463
|
}
|
|
1454
1464
|
.chats-window__icon::-webkit-details-marker { display: none; }
|
|
1455
1465
|
.chats-window__icon svg { width: 18px; height: 18px; }
|
|
1456
|
-
|
|
1466
|
+
@media (hover: hover) {
|
|
1467
|
+
.chats-window__icon:hover { background: var(--chats-dock-hover); color: var(--chats-text); }
|
|
1468
|
+
}
|
|
1457
1469
|
.chats-window__back { display: none; }
|
|
1458
1470
|
/* The host's thread actions (conversation_header_actions). */
|
|
1459
1471
|
.chats-window__menu { flex: 0 0 auto; }
|
|
@@ -1475,7 +1487,9 @@ turbo-frame[id^="chats_page_"] {
|
|
|
1475
1487
|
box-shadow: none;
|
|
1476
1488
|
}
|
|
1477
1489
|
.chats-window--unread :is(.chats-window__subject, .chats-window__icon, .chats-verified) { color: inherit; }
|
|
1478
|
-
|
|
1490
|
+
@media (hover: hover) {
|
|
1491
|
+
.chats-window--unread .chats-window__icon:hover { background: color-mix(in srgb, currentColor 16%, transparent); color: inherit; }
|
|
1492
|
+
}
|
|
1479
1493
|
.chats-window--unread .chats-window__header .chats-badge {
|
|
1480
1494
|
background: var(--chats-dock-unread-text);
|
|
1481
1495
|
color: var(--chats-dock-unread-bg);
|
|
@@ -248,8 +248,13 @@ module Chats
|
|
|
248
248
|
# page's own conversation frame, else the whole page around it (any
|
|
249
249
|
# other frame of the page, the search's results among them, finds its
|
|
250
250
|
# own part in it).
|
|
251
|
+
#
|
|
252
|
+
# The pane alone comes in turbo-rails' minimal frame layout, always (even
|
|
253
|
+
# under a `config.layout`): nothing of the host's page is rendered for
|
|
254
|
+
# Turbo to throw away. Its title travels on the pane (`data-title`,
|
|
255
|
+
# chats_page_title), since Turbo leaves <head> alone on a frame visit.
|
|
251
256
|
def render_split_page
|
|
252
|
-
return render :pane, layout:
|
|
257
|
+
return render :pane, layout: "turbo_rails/frame" if pane_request?
|
|
253
258
|
|
|
254
259
|
load_inbox
|
|
255
260
|
render :index
|
|
@@ -329,6 +329,21 @@ module Chats
|
|
|
329
329
|
path.presence || chats_routes.conversations_path(with: Chats.inbox_with_sgid(messager))
|
|
330
330
|
end
|
|
331
331
|
|
|
332
|
+
# The messaging page's title: the inbox's, or +conversation+'s.
|
|
333
|
+
# `config.page_title` when the host sets one, else the localized
|
|
334
|
+
# `chats.page_title.*`. ONE source for both ways a conversation opens:
|
|
335
|
+
# the bundled pages hand it to `content_for :title` on a full visit, and
|
|
336
|
+
# the two-pane page's pane carries it (`data-title`) for chats--split to
|
|
337
|
+
# give the tab when a row opens it, since Turbo leaves <head> alone then.
|
|
338
|
+
def chats_page_title(conversation = nil)
|
|
339
|
+
viewer = chats_current_messager
|
|
340
|
+
custom = Chats.config.page_title&.call(conversation, viewer)
|
|
341
|
+
return custom.to_s if custom.present?
|
|
342
|
+
return t("chats.page_title.inbox") unless conversation
|
|
343
|
+
|
|
344
|
+
t("chats.page_title.conversation", name: conversation.title_for(viewer))
|
|
345
|
+
end
|
|
346
|
+
|
|
332
347
|
# The gem's bundled stylesheet (CSS-variable themed — see chats.css).
|
|
333
348
|
# Called from the engine's own views; hosts that eject + restyle the
|
|
334
349
|
# views with their own framework simply don't include it.
|
|
@@ -22,6 +22,16 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
22
22
|
// update of the list) would disable it again on a wide screen; the frame
|
|
23
23
|
// keeps what the screen says.
|
|
24
24
|
//
|
|
25
|
+
// The tab's title follows the pane: Turbo leaves <head> alone when it
|
|
26
|
+
// promotes a frame load to a visit, so the title would stay the previous
|
|
27
|
+
// page's. The pane carries the page's title with it open (`data-title`,
|
|
28
|
+
// chats_page_title: the same the full page gives `content_for :title`).
|
|
29
|
+
//
|
|
30
|
+
// The open conversation's row stops counting it unread as soon as it is
|
|
31
|
+
// read (the pane's answer says the server read it, `data-read`; or its
|
|
32
|
+
// thread reads it on screen, `chats:read`), without waiting for the list's
|
|
33
|
+
// live refresh, which may be seconds away or not come at all.
|
|
34
|
+
//
|
|
25
35
|
// When the pane opens a conversation, its row becomes the current one
|
|
26
36
|
// (aria-current="page", .chats-row--current) at once, exactly as the server
|
|
27
37
|
// renders /messages/:id, instead of when the list next refreshes. And the
|
|
@@ -82,9 +92,33 @@ export default class extends Controller {
|
|
|
82
92
|
}
|
|
83
93
|
link.closest(".chats-row")?.classList.toggle("chats-row--current", current)
|
|
84
94
|
})
|
|
95
|
+
const pane = this.frameTarget.querySelector(".chats-split__pane")
|
|
96
|
+
if (pane?.dataset.title) document.title = pane.dataset.title
|
|
97
|
+
if (pane?.dataset.read === "true") this.markRead(pane)
|
|
85
98
|
this.arrive()
|
|
86
99
|
}
|
|
87
100
|
|
|
101
|
+
// chats:read, from the pane's thread.
|
|
102
|
+
threadRead(event) {
|
|
103
|
+
const pane = event.target?.closest?.(".chats-split__pane")
|
|
104
|
+
if (pane) this.markRead(pane)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// The conversation's row, read: no count, no bold, as the next refresh
|
|
108
|
+
// will render it.
|
|
109
|
+
markRead(pane) {
|
|
110
|
+
if (!this.hasListTarget || !pane.dataset.path) return
|
|
111
|
+
|
|
112
|
+
const path = new URL(pane.dataset.path, document.baseURI).pathname
|
|
113
|
+
this.listTarget.querySelectorAll(`a[data-turbo-frame="${this.frameTarget.id}"]`).forEach((link) => {
|
|
114
|
+
if (new URL(link.getAttribute("href"), document.baseURI).pathname !== path) return
|
|
115
|
+
|
|
116
|
+
const row = link.closest(".chats-row")
|
|
117
|
+
row?.classList.remove("chats-row--unread")
|
|
118
|
+
row?.querySelector(".chats-row__bottom .chats-badge")?.remove()
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
88
122
|
arrive() {
|
|
89
123
|
const pane = this.frameTarget.querySelector(".chats-split__pane")
|
|
90
124
|
if (!pane) return
|
|
@@ -456,9 +456,14 @@ export default class extends Controller {
|
|
|
456
456
|
this.pendingRead = true
|
|
457
457
|
return
|
|
458
458
|
}
|
|
459
|
+
// chats:read, once the server has it: the two-pane page clears the
|
|
460
|
+
// conversation's row (chats--split) without waiting for the list's
|
|
461
|
+
// live refresh.
|
|
459
462
|
fetch(this.readUrlValue, {
|
|
460
463
|
method: "POST",
|
|
461
464
|
headers: { "X-CSRF-Token": csrfToken(), Accept: "application/json" }
|
|
465
|
+
}).then((response) => {
|
|
466
|
+
if (response.ok) this.element.dispatchEvent(new CustomEvent("chats:read", { bubbles: true }))
|
|
462
467
|
}).catch(() => {})
|
|
463
468
|
}
|
|
464
469
|
|
|
@@ -17,11 +17,22 @@
|
|
|
17
17
|
The header stays outside it, so a refresh still brings it up to date
|
|
18
18
|
(a mute, a renamed group).
|
|
19
19
|
|
|
20
|
+
data-title: the page's title with this conversation open
|
|
21
|
+
(chats_page_title, the same the full page gives `content_for :title`):
|
|
22
|
+
chats--split gives it to the tab when a row opens the pane, since Turbo
|
|
23
|
+
leaves <head> alone on a frame visit.
|
|
24
|
+
|
|
25
|
+
data-read: the server read the conversation as it rendered this (a row
|
|
26
|
+
clicked); chats--split then clears its row's unread count at once,
|
|
27
|
+
instead of when the list next refreshes. A pane rendered unread clears
|
|
28
|
+
it when its thread reads (the thread's chats:read event).
|
|
29
|
+
|
|
20
30
|
Locals: conversation, participant, messages, more, first_unread_id,
|
|
21
31
|
counterpart, read_pending (as in chats/conversations/_thread). %>
|
|
22
32
|
<% title_id = dom_id(conversation, :title) %>
|
|
23
33
|
<%= tag.section class: "chats-split__pane", "aria-labelledby": title_id,
|
|
24
|
-
data: { conversation_id: conversation.id
|
|
34
|
+
data: { conversation_id: conversation.id, path: conversation_path(conversation),
|
|
35
|
+
title: chats_page_title(conversation), read: !read_pending } do %>
|
|
25
36
|
<%= render "chats/conversations/thread_header",
|
|
26
37
|
conversation: conversation, participant: participant, counterpart: counterpart,
|
|
27
38
|
heading: :h2, title_id: title_id, back_path: conversations_path(**chats_inbox_filter) %>
|
|
@@ -50,6 +50,9 @@
|
|
|
50
50
|
`.restore` visit that honors this. Requires the host layout to
|
|
51
51
|
`yield :head` (the Rails convention); harmless no-op otherwise.
|
|
52
52
|
https://turbo.hotwired.dev/handbook/building#opting-out-of-caching %>
|
|
53
|
+
<%# The page's title, for a layout that shows `yield(:title)`: the inbox's,
|
|
54
|
+
or the open conversation's (chats_page_title, config.page_title). %>
|
|
55
|
+
<% content_for :title, chats_page_title(@conversation) %>
|
|
53
56
|
<% content_for :head do %>
|
|
54
57
|
<meta name="turbo-cache-control" content="no-cache">
|
|
55
58
|
<% end %>
|
|
@@ -149,6 +152,7 @@
|
|
|
149
152
|
controller: "chats--split",
|
|
150
153
|
action: "turbo:before-prefetch->chats--split#prefetch " \
|
|
151
154
|
"turbo:frame-load->chats--split#opened " \
|
|
155
|
+
"chats:read->chats--split#threadRead " \
|
|
152
156
|
"turbo:before-morph-attribute->chats--split#keepEnabled"
|
|
153
157
|
} do %>
|
|
154
158
|
<nav class="chats-split__list" aria-label="<%= t("chats.inbox.title") %>" data-chats--split-target="list">
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
<%# The answer to a row click on the two-pane messaging page: the request
|
|
2
2
|
comes from the page's `chats_conversation` frame (Turbo-Frame header),
|
|
3
3
|
so the answer is that frame with the conversation pane in it and
|
|
4
|
-
nothing else — no list,
|
|
4
|
+
nothing else — no list, turbo-rails' minimal frame layout
|
|
5
|
+
(ConversationsController#show). The pane carries its title
|
|
6
|
+
(data-title), which chats--split gives the tab.
|
|
5
7
|
Turbo swaps it into the frame and, the frame being
|
|
6
8
|
data-turbo-action="advance", moves the address bar to /messages/:id. %>
|
|
7
9
|
<%= turbo_frame_tag Chats::ConversationsController::PANE_FRAME do %>
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
two-pane page's conversation pane render too — see that partial for how
|
|
5
5
|
the live thread works). Under the default `:split`, /messages/:id is
|
|
6
6
|
the two-pane page instead (index.html.erb). %>
|
|
7
|
+
<% content_for :title, chats_page_title(@conversation) %>
|
|
7
8
|
<%= chats_styles %>
|
|
8
9
|
|
|
9
10
|
<%= render layout: "chats/conversations/thread",
|
data/config/locales/en.yml
CHANGED
data/config/locales/es.yml
CHANGED
data/lib/chats/configuration.rb
CHANGED
|
@@ -174,6 +174,14 @@ module Chats
|
|
|
174
174
|
# suppress the badge on some messagers without touching the models.
|
|
175
175
|
attr_reader :verified_badge
|
|
176
176
|
|
|
177
|
+
# ->(conversation, viewer) { String } — the title of the messaging page:
|
|
178
|
+
# the inbox (conversation nil) or a conversation. The bundled views
|
|
179
|
+
# give it to `content_for :title` (a layout doing `yield(:title)` shows
|
|
180
|
+
# it), and the two-pane page gives the tab the conversation's when a
|
|
181
|
+
# row opens it in the pane. nil (the default) is the localized
|
|
182
|
+
# `chats.page_title.inbox` / `chats.page_title.conversation`.
|
|
183
|
+
attr_reader :page_title
|
|
184
|
+
|
|
177
185
|
def initialize
|
|
178
186
|
@messager_class = "User"
|
|
179
187
|
@parent_controller = "::ApplicationController"
|
|
@@ -210,6 +218,7 @@ module Chats
|
|
|
210
218
|
@messager_url = ->(_messager) { nil }
|
|
211
219
|
@message_signature = nil
|
|
212
220
|
@verified_badge = nil
|
|
221
|
+
@page_title = nil
|
|
213
222
|
|
|
214
223
|
@messager_display_name = lambda do |messager|
|
|
215
224
|
messager.try(:display_name) || messager.try(:name) ||
|
|
@@ -335,6 +344,10 @@ module Chats
|
|
|
335
344
|
@verified_badge = value.nil? ? nil : ensure_callable(value, "verified_badge")
|
|
336
345
|
end
|
|
337
346
|
|
|
347
|
+
def page_title=(value)
|
|
348
|
+
@page_title = value.nil? ? nil : ensure_callable(value, "page_title")
|
|
349
|
+
end
|
|
350
|
+
|
|
338
351
|
def messager_display_name=(value)
|
|
339
352
|
@messager_display_name = ensure_callable(value, "messager_display_name")
|
|
340
353
|
end
|
|
@@ -449,11 +449,19 @@ module Chats
|
|
|
449
449
|
field.to_s == "title" && title.present?
|
|
450
450
|
end
|
|
451
451
|
|
|
452
|
+
# Whether anything was removed (see Chats::Message#remove_reported_field!):
|
|
453
|
+
# false for a title already gone, decided under the row lock.
|
|
452
454
|
def remove_reported_field!(field)
|
|
453
455
|
return false unless field.to_s == "title"
|
|
454
456
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
+
with_lock do
|
|
458
|
+
next false if title.blank?
|
|
459
|
+
|
|
460
|
+
update!(title: nil)
|
|
461
|
+
true
|
|
462
|
+
end
|
|
463
|
+
rescue ActiveRecord::RecordNotFound
|
|
464
|
+
false
|
|
457
465
|
end
|
|
458
466
|
|
|
459
467
|
def report_visible_to?(viewer, field: nil)
|
data/lib/chats/models/message.rb
CHANGED
|
@@ -356,12 +356,28 @@ module Chats
|
|
|
356
356
|
# A moderator removing a reported message body = the soft-delete
|
|
357
357
|
# tombstone path, so the thread shows "Message deleted" instead of a
|
|
358
358
|
# hole, and attachments are purged with it.
|
|
359
|
+
#
|
|
360
|
+
# Returns whether anything was removed: true only when it actually
|
|
361
|
+
# removed something (the tombstone, or the row under `deletion: :hard`);
|
|
362
|
+
# false for a message already deleted (by its sender, by an earlier
|
|
363
|
+
# decision), already gone, or whose body is already blank. moderate
|
|
364
|
+
# 1.0.0.beta3 and later take that as the truth and refuse a removal that
|
|
365
|
+
# removed nothing, rather than record "content removed" with a statement
|
|
366
|
+
# of reasons that would be false; earlier versions ignore the return
|
|
367
|
+
# value. Decided under the message's row lock, read again under it: two
|
|
368
|
+
# decisions taken at once cannot both see it standing.
|
|
359
369
|
def remove_reported_field!(field)
|
|
360
370
|
return false unless field.to_s == "body"
|
|
361
371
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
372
|
+
with_lock do
|
|
373
|
+
next false if deleted? || body.blank?
|
|
374
|
+
|
|
375
|
+
# Trust & Safety outranks a product lock: a closed conversation must
|
|
376
|
+
# never be a place reported content can hide.
|
|
377
|
+
soft_delete!(enforce_lock: false)
|
|
378
|
+
end
|
|
379
|
+
rescue ActiveRecord::RecordNotFound
|
|
380
|
+
false # hard-deleted meanwhile: nothing left to remove
|
|
365
381
|
end
|
|
366
382
|
|
|
367
383
|
# Only people *in* the conversation may report a message (a message
|
data/lib/chats/version.rb
CHANGED
|
@@ -93,6 +93,14 @@ Chats.configure do |config|
|
|
|
93
93
|
#
|
|
94
94
|
# config.inbox_layout = :split
|
|
95
95
|
#
|
|
96
|
+
# The messaging page's title (the bundled views give it to
|
|
97
|
+
# `content_for :title`, and the two-pane page to the tab when a row opens
|
|
98
|
+
# a conversation). nil: "Messages" / "Bob · Messages" (chats.page_title.*).
|
|
99
|
+
#
|
|
100
|
+
# config.page_title = ->(conversation, viewer) {
|
|
101
|
+
# conversation ? "#{conversation.title_for(viewer)} | MyApp" : "Messages | MyApp"
|
|
102
|
+
# }
|
|
103
|
+
#
|
|
96
104
|
# Encrypt message bodies at rest (ActiveRecord Encryption; requires
|
|
97
105
|
# `bin/rails db:encryption:init`). Body search degrades when enabled.
|
|
98
106
|
#
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: chats
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.6.
|
|
4
|
+
version: 0.6.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rameerez
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-10-
|
|
10
|
+
date: 2026-10-19 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: activerecord
|
|
@@ -173,13 +173,6 @@ files:
|
|
|
173
173
|
- app/views/chats/messages/locked.turbo_stream.erb
|
|
174
174
|
- app/views/chats/shared/_unread_badge.html.erb
|
|
175
175
|
- app/views/chats/shared/_verified_badge.html.erb
|
|
176
|
-
- chats-0.3.1.gem
|
|
177
|
-
- chats-0.3.2.gem
|
|
178
|
-
- chats-0.4.0.gem
|
|
179
|
-
- chats-0.4.1.gem
|
|
180
|
-
- chats-0.4.2.gem
|
|
181
|
-
- chats-0.5.0.gem
|
|
182
|
-
- chats-0.5.1.gem
|
|
183
176
|
- config/importmap.rb
|
|
184
177
|
- config/locales/en.yml
|
|
185
178
|
- config/locales/es.yml
|
data/chats-0.3.1.gem
DELETED
|
Binary file
|
data/chats-0.3.2.gem
DELETED
|
Binary file
|
data/chats-0.4.0.gem
DELETED
|
Binary file
|
data/chats-0.4.1.gem
DELETED
|
Binary file
|
data/chats-0.4.2.gem
DELETED
|
Binary file
|
data/chats-0.5.0.gem
DELETED
|
Binary file
|
data/chats-0.5.1.gem
DELETED
|
Binary file
|