chats 0.4.0 → 0.4.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 +26 -0
- data/README.md +1 -1
- data/lib/chats/action.rb +16 -1
- data/lib/chats/broadcasts.rb +4 -4
- data/lib/chats/inbox.rb +6 -10
- data/lib/chats/models/concerns/messager.rb +10 -2
- data/lib/chats/models/message.rb +5 -0
- data/lib/chats/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: '001542179fe490a6f8743099b6b31a2d9cff7cf43a28b982c0cac932b8c06bd1'
|
|
4
|
+
data.tar.gz: 29720d94ecd7302ae2adcb17f9a81b6fbc6f0365d900e5773655250e14ea9bb9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3d9f25316a4832204fb285ea6547fe34a5c7f938c148307bef311a3311ed770401329b60e020031d7468b200ecf1170294cb7edce6dfbebb5d8c386052aa1adf
|
|
7
|
+
data.tar.gz: d7f28cf57770b2ad517299f56d431ea11fcc6f33b1886c36a815cce6de81dec983082cd16eb74dfd3528bb966af8feb277cb6ff286d7c89d395f934a12737d3c
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@ 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.4.1] - 2026-10-17
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **An action's strings are frozen copies.** `Data` freezes the action but
|
|
11
|
+
not the strings it was handed, so `action.url.replace("javascript:…")`,
|
|
12
|
+
or the caller mutating the string they passed in, could rewrite a
|
|
13
|
+
validated action in memory before the bubble rendered it. `Chats::Action`
|
|
14
|
+
now keeps a frozen copy of both members: what the validator saw is what
|
|
15
|
+
renders.
|
|
16
|
+
- **An empty port suffix is refused.** `https://host:/x` parses as the
|
|
17
|
+
default port and slipped past the 1..65535 rule; a named port must have
|
|
18
|
+
digits.
|
|
19
|
+
- **An action's label is a plain String.** A label handed over as an
|
|
20
|
+
html_safe string (a host's `t("…_html")`) kept the mark through the copy
|
|
21
|
+
and rendered unescaped in the button. The copy is now a plain `String`,
|
|
22
|
+
so the bubble escapes it like any other content.
|
|
23
|
+
- **`config.inbox_scope` is one rule, applied once.** `Messager#chats` now
|
|
24
|
+
applies it, so the inbox relation, `unread_chats_count`, the inbox screen
|
|
25
|
+
and the live badge all agree. The badge broadcast used to count through
|
|
26
|
+
the raw `inbox_for` relation and could light up for a conversation the
|
|
27
|
+
inbox would never show (a host scoping game records out of the inbox saw
|
|
28
|
+
the badge count them). `Chats::Inbox` builds on `viewer.chats`, so a
|
|
29
|
+
host's own override of `chats` reaches the inbox screen too.
|
|
30
|
+
- The README said the inbox preview still shows a soft-deleted message's
|
|
31
|
+
body; it reads "Message deleted", as for any tombstone.
|
|
32
|
+
|
|
7
33
|
## [0.4.0] - 2026-10-17
|
|
8
34
|
|
|
9
35
|
Action buttons. A support desk writes "Your invoice is ready" and wants a
|
data/README.md
CHANGED
|
@@ -469,7 +469,7 @@ Chats::Action.new(label: "Pay now", url: "/pay") # accepted anywhere a Hash is
|
|
|
469
469
|
- `url`: at most 2048 characters, and EITHER an absolute `http://` / `https://` URL with a host (and, if it names one, a port from 1 to 65535), OR a path on your site that starts with a single `/`. Everything else is refused: `javascript:`, `data:`, `mailto:`, relative paths, and anything a browser would send to another site: `//elsewhere.example`, `///elsewhere.example`, `/\elsewhere.example`, `/<tab>/…`. URLs must be escaped, as Rails' url helpers already produce them.
|
|
470
470
|
- The error is `errors.of_kind?(:action, :invalid)`, with a readable message in `en` and `es`.
|
|
471
471
|
|
|
472
|
-
Body rules don't change: an action goes with a message, it is never a message by itself. Both text and system messages can carry one. `edit!` keeps the action. **A tombstone shows neither the body nor the button**: soft deletion clears the action, and so does a moderator's `remove_reported_field!`, because it goes through the same tombstone. The inbox preview
|
|
472
|
+
Body rules don't change: an action goes with a message, it is never a message by itself. Both text and system messages can carry one. `edit!` keeps the action. **A tombstone shows neither the body nor the button**: soft deletion clears the action, and so does a moderator's `remove_reported_field!`, because it goes through the same tombstone. The inbox preview reads “Message deleted”, as for any tombstone.
|
|
473
473
|
|
|
474
474
|
**Only your Ruby code sets an action.** The composer can't: `Chats::MessagesController` permits `body` and `files` only, the same as with `author:`. Restyle the button with `.chats-message__action` (eject `chats/messages/_action.html.erb` to change its markup). It uses your `--chats-accent` / `--chats-accent-contrast` colours: accent-filled under someone else's message or a system note, reversed inside your own accent-coloured bubble.
|
|
475
475
|
|
data/lib/chats/action.rb
CHANGED
|
@@ -40,8 +40,23 @@ module Chats
|
|
|
40
40
|
# URI, a symbol label — all read back as the String the column holds.
|
|
41
41
|
# nil stays nil so a missing one is a validation error, not a "" that
|
|
42
42
|
# looks present.
|
|
43
|
+
#
|
|
44
|
+
# Each is a frozen, PLAIN String copy. `Data` freezes the action, not the
|
|
45
|
+
# strings it was handed, so without the copy `action.url.replace(
|
|
46
|
+
# "javascript:…")` would rewrite a validated action in place — and so
|
|
47
|
+
# would the caller mutating the string they passed in. Plain, because
|
|
48
|
+
# `String.new` drops an html_safe mark: a label that arrived as a
|
|
49
|
+
# SafeBuffer (a host's `t("…_html")`) would otherwise render unescaped
|
|
50
|
+
# in the button. An action is a value: what the validator saw is what
|
|
51
|
+
# the bubble renders, and the bubble escapes it like any other content.
|
|
43
52
|
def initialize(label:, url:)
|
|
44
|
-
super(label: label
|
|
53
|
+
super(label: plain(label), url: plain(url))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def plain(value)
|
|
59
|
+
String.new(value.to_s).freeze unless value.nil?
|
|
45
60
|
end
|
|
46
61
|
end
|
|
47
62
|
|
data/lib/chats/broadcasts.rb
CHANGED
|
@@ -120,10 +120,10 @@ module Chats
|
|
|
120
120
|
target: "chats_unread_badge",
|
|
121
121
|
partial: BADGE_PARTIAL,
|
|
122
122
|
# Counted at broadcast-enqueue time: the badge is advisory UI; a
|
|
123
|
-
# count that's seconds stale self-heals on the next event.
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
locals: { count:
|
|
123
|
+
# count that's seconds stale self-heals on the next event. The
|
|
124
|
+
# SAME count the page renders (`chats_unread_badge`), so what the
|
|
125
|
+
# inbox scope hides never lights the badge.
|
|
126
|
+
locals: { count: messager.unread_chats_count }
|
|
127
127
|
)
|
|
128
128
|
end
|
|
129
129
|
|
data/lib/chats/inbox.rb
CHANGED
|
@@ -115,15 +115,12 @@ module Chats
|
|
|
115
115
|
Chats.config.inbox_limit
|
|
116
116
|
end
|
|
117
117
|
|
|
118
|
-
#
|
|
119
|
-
#
|
|
120
|
-
#
|
|
118
|
+
# The viewer's own inbox relation (`Messager#chats`: `inbox_for` with
|
|
119
|
+
# `config.inbox_scope` applied, and whatever the host's model adds on
|
|
120
|
+
# top), memoized so it is built ONCE per inbox however many legs the
|
|
121
|
+
# row query is split into (the stack aggregates build their own).
|
|
121
122
|
def base_relation
|
|
122
|
-
@base_relation ||=
|
|
123
|
-
relation = Chats::Conversation.inbox_for(viewer)
|
|
124
|
-
.includes(:last_message, :subject, participants: :messager)
|
|
125
|
-
Chats.config.inbox_scope.call(relation, viewer) || relation
|
|
126
|
-
end
|
|
123
|
+
@base_relation ||= viewer.chats.includes(:last_message, :subject, participants: :messager)
|
|
127
124
|
end
|
|
128
125
|
|
|
129
126
|
# The polymorphic types worth splitting out. Empty when nothing stacks
|
|
@@ -244,12 +241,11 @@ module Chats
|
|
|
244
241
|
# aggregates — independent of how deep the stack is, and of how much of
|
|
245
242
|
# it we loaded.
|
|
246
243
|
def stack_totals(messager)
|
|
247
|
-
scope =
|
|
244
|
+
scope = viewer.chats.reorder(nil).direct.where(
|
|
248
245
|
id: Chats::Participant.select(:conversation_id).where(
|
|
249
246
|
messager_type: messager.class.polymorphic_name, messager_id: messager.id
|
|
250
247
|
)
|
|
251
248
|
)
|
|
252
|
-
scope = Chats.config.inbox_scope.call(scope, viewer) || scope
|
|
253
249
|
|
|
254
250
|
{
|
|
255
251
|
open: scope.distinct.count,
|
|
@@ -176,9 +176,17 @@ module Chats
|
|
|
176
176
|
end
|
|
177
177
|
|
|
178
178
|
# The inbox: every active conversation, blocked counterparts hidden,
|
|
179
|
-
# newest activity first. A relation —
|
|
179
|
+
# `config.inbox_scope` applied, newest activity first. A relation —
|
|
180
|
+
# chain `.limit`, `.includes`, etc.
|
|
181
|
+
#
|
|
182
|
+
# THE one definition of "what is in this messager's inbox": the inbox
|
|
183
|
+
# screen, the unread count, the live badge and a host's own queries all
|
|
184
|
+
# read it, so a conversation the scope hides is hidden everywhere. Until
|
|
185
|
+
# 0.4.1 the badge counted through the raw `inbox_for` relation and could
|
|
186
|
+
# light up for a conversation the inbox would never show.
|
|
180
187
|
def chats
|
|
181
|
-
Chats::Conversation.inbox_for(self)
|
|
188
|
+
relation = Chats::Conversation.inbox_for(self)
|
|
189
|
+
Chats.config.inbox_scope.call(relation, self) || relation
|
|
182
190
|
end
|
|
183
191
|
|
|
184
192
|
# Number of conversations with unread messages (the WhatsApp-style nav
|
data/lib/chats/models/message.rb
CHANGED
|
@@ -40,6 +40,8 @@ module Chats
|
|
|
40
40
|
ACTION_LABEL_LIMIT = 80
|
|
41
41
|
ACTION_URL_LIMIT = 2048
|
|
42
42
|
ACTION_URL_SCHEMES = %w[http https].freeze
|
|
43
|
+
# An authority ending in ":" with nothing after it ("https://host:/x").
|
|
44
|
+
EMPTY_PORT = %r{\A[a-z][a-z0-9+.-]*://[^/?#]*:(?:[/?#]|\z)}i
|
|
43
45
|
ACTION_URL_PORTS = (1..65_535)
|
|
44
46
|
|
|
45
47
|
belongs_to :conversation,
|
|
@@ -382,6 +384,9 @@ module Chats
|
|
|
382
384
|
url = action.url
|
|
383
385
|
return false if url.blank? || url.length > ACTION_URL_LIMIT
|
|
384
386
|
return false if url.start_with?("//", "/\\")
|
|
387
|
+
# `https://host:` parses as the default port, so an empty port suffix
|
|
388
|
+
# would pass the range check below; a named port must have digits.
|
|
389
|
+
return false if url.match?(EMPTY_PORT)
|
|
385
390
|
|
|
386
391
|
uri = URI.parse(url)
|
|
387
392
|
if uri.scheme
|
data/lib/chats/version.rb
CHANGED