Releases: rameerez/chats
Release list
v0.3.2
Three correctness fixes and one new seam, from an integration review against a real host.
Fixed
- A busy stack can no longer evict other people from the inbox. The grouped-inbox window is now taken per counterpart, so one desk with many recent conversations cannot consume another counterpart's row. At most twice the row limit is materialized.
- Lifecycle events fire after commit, and stay silent on rollback.
conversation_createdandparticipant_leftpreviously ran inside the transaction, so a subscriber could act on a conversation that never existed. - An author no longer has to be a messager. Signing a desk's reply is about who wrote it, not about holding a seat, so staff need not be given
acts_as_messagerjust to sign. The author must be persisted; the host decides who may sign for a shared identity, and the HTTP controller never accepts an author from request parameters.
Added
-
Chats::SendRateLimited— the send-rate budget as a controller concern, so a product built on chats (its own composer, its own controller) shares one configured budget with the chat composer instead of inventing a second one.config.send_rate_limit = { to: 60, within: 1.minute } # shared; nil disables
Upgrading
Nothing to do. If you were relying on an author having to be a messager, note that constraint is gone by design.
Verified
| Tests | 324 runs, 1350 assertions, 0 failures |
| Databases | SQLite, PostgreSQL |
| Rails | 7.1, 7.2, 8.1 |
What's Changed
Full Changelog: v0.3.1...v0.3.2
v0.3.1
A patch on 0.3.0: the official-account mark is a check, not a star.
0.3.0 shipped a five-pointed star as the verified badge. It came from a host icon named verified_badge whose path is actually drawn as a star, and it was copied on the strength of the name without anyone looking at the shape.
A star says favourite, or rated. It does not say this account is really us. And at the size the badge renders — roughly 14px, next to a name in an inbox row — a solid star and a solid rosette are the same small blob, so the tick was the entire point and it was the one thing missing.
| 0.3.0 | ★ a solid star |
| 0.3.1 | a scalloped rosette with the tick knocked out |
The glyph is now heroicons check-badge. fill-rule="evenodd" is what knocks the tick out of the rosette — without it the shape fills solid and you are back to a blob.
Nothing else changed
Same option, same CSS variable, same accessible label, same colour:
acts_as_messager verified: trueThe #0284c7 default still clears WCAG 1.4.11's 3:1 on every ground it lands on, including the inbox row's hover state, which is where the familiar #1d9bf0 had failed at 2.73:1. Overrides are unchanged: --chats-verified for the colour, config.verified_badge or ejecting chats/shared/_verified_badge.html.erb for the glyph.
Upgrading
Nothing to do. If you set verified: true on 0.3.0 you get a better mark; if you never set it, nothing about your app changes.
Verified
| Tests | 320 runs, 1342 assertions, 0 failures |
| Databases | SQLite, PostgreSQL |
| Ruby | 3.3, 3.4, 4.0 |
| Rails | 7.1, 7.2, 8.1 |
Full detail in CHANGELOG.md.
Full Changelog: v0.3.0...v0.3.1
v0.3.0
Official accounts. A support desk, a shop or an organization is not a person, and the people it talks to need to see that at a glance — the blue tick everyone already reads.
class SupportDesk < ApplicationRecord
acts_as_messager verified: true, notifications: false, blockable: false, inbox: :grouped
endThe badge renders next to the name in the inbox row, the grouped row and the thread header. Never replacing the name, always beside it.
What ships
acts_as_messager verified: true→Klass.chat_verified?, alongside the existingnotifications:,blockable:andinbox:options.- Accessible by default. The badge carries an
aria-labelandtitlereading "Official account" (Cuenta oficialin Spanish), with the glyph itselfaria-hiddenso nothing is announced twice. - Yours to restyle. The colour is the
--chats-verifiedcustom property; the glyph isconfig.verified_badge = ->(messager) { markup }, or ejectchats/shared/_verified_badge.html.erb. - It refuses to coerce.
verified: "false"raises at boot rather than quietly verifying an account. Every other boolean option here is forgiving; this one is the mark that says an account is really us, and the strings that reach a model declaration come from exactly the places that produce"false".
The default colour was chosen by measurement
The familiar #1d9bf0 failed: 3.00:1 on the page and 2.73:1 on the inbox row's hover background — the badge dropped below the contrast bar precisely while someone was pointing at it.
| Ground | #1d9bf0 |
#0284c7 (shipped) |
|---|---|---|
page #ffffff |
3.00 | 4.10 |
row hover #f3f4f6 |
2.73 | 3.72 |
inverted #111827 |
5.91 | 4.33 |
A test computes these ratios from the stylesheet rather than asserting a hex, so changing the colour tells you if it stops passing.
Upgrading
Nothing to do. The option defaults to false and an app that does not set it renders exactly as it did on 0.2.0.
Verified
| Tests | 320 runs, 1342 assertions, 0 failures |
| Databases | SQLite, PostgreSQL |
| Rails | 7.1, 7.2, 8.1 |
Full detail in CHANGELOG.md.
Full Changelog: v0.2.0...v0.3.0
v0.2.0
chats becomes a foundation other products can build on. 0.1.x gave your users real-time DMs and group threads. 0.2.0 adds the seams a second gem needs to build something on top of chats without ejecting your screens or monkey-patching its models.
The proof it works: support_desk, customer support as conversations, is built entirely on these seams.
Nothing here changes existing behaviour until you set an option. Upgrading from 0.1.x is rails generate chats:upgrade && rails db:migrate. Two caveats are in the CHANGELOG under Upgrading.
What ships in 0.2.0, and what each is for
Messagers that are not people
acts_as_messager notifications: false, blockable: false— a messager that is not a person stops being treated like one. Never notifiable, no block or report affordances, absent from unread digests.- Why: a support desk, a shop, a bot. Before this, every host filtered them by class in each notifier and view (
is_a?(User)scattered through the app). Now the model declares what it is, once.
Conversations that can close
chat_locked?/chat_locked_noticeon the subject, alockedstate on the conversation, a locked-composer partial, and a 422 that re-renders it.- Why:
can_messageis consulted once, when a conversation opens, so nothing governed writing into an existing one. Hosts were patchingChats::Messageto stop a closed thread accepting replies. The subject already owns what the conversation is about; now it owns whether it is open. - Every write path is gated, not just creation: editing, deleting and reacting all refuse on a locked thread, and the UI stops offering them. Moderation deliberately outranks the lock, so reported content stays removable.
Messages someone wrote on someone else's behalf
authoron messages,#signed?,message!(…, author:), a signature line, andconfig.message_signature.- Why: an agent answering from a shared desk seat should sign the bubble while the desk stays the conversation's identity. Handoffs change who writes, never who the thread is with. Organizations-as-messagers need exactly the same.
- A signed message reports its author as the owner for moderation, not the seat, so a report or flag points at a person your app can act on.
An inbox that does not fill with noise
inbox: :groupedplusChats::Inbox.for(viewer), returning conversations and groups;config.inbox_limit,config.inbox_scope, andGET /conversations?with=<sgid>for the filtered list.- Why: every thread with one counterpart collapses into a single row. Someone with a dozen support cases sees one "Support" row, not twelve, and their real conversations stay where they were.
- The row limit bounds rows, not conversations, so a deep stack can never push someone's friends off the end of their inbox. Group counts are global, not just what the page loaded.
Extension points instead of ejected views
- View slots:
chats/slots/_inbox_top,_inbox_empty,_conversation_header_actions,_locked_composer,_message_meta. - Why: adding one row to the inbox used to mean ejecting the whole view and inheriting every future fix by hand.
Events with more than one listener
Chats.on(:event) { }— multi-subscriber, error-isolated throughRails.error, reload-safe withkey:. Newconversation_createdandparticipant_left.- Why:
config.notifierwas a single callable, so a gem could not listen without wrapping the host's. A subscriber that raises is reported and the next one still runs.config.notifierkeeps working, deprecated.
Smaller seams
config.messager_url— stock views stop assumingmain_app.user_path.Participant#reseat!— swap who holds a seat, keeping the read horizon, for when a guest becomes a user.
Fixes worth calling out
- The missed-broadcast recovery never worked. The catch-up endpoint answered
render html:with an explicit Turbo Stream content type, andrender html:forcestext/htmland ignores it — so the browser never treated the response as a stream. It failed silently in exactly the case it was written for. Reproduced, then fixed. - A host's own locale file lost to the gem's. The engine appended its locales onto the app's load path on top of Rails' own, and railtie paths are unshifted ahead of everything, so the second copy landed after the host's and quietly overrode it. Rewording any chats string in your own
es.ymldid nothing. Gem first, host last, now pinned by a test. - Migrations misnamed two adapters. PostGIS reports
"PostGIS", not"PostgreSQL", so those hosts silently gotjsonwhere the gem meantjsonb. Trilogy reports"Trilogy", so a MySQL check missed it and handed those hosts a default MySQL rejects. - The test suite wrote 80 MB per run. Rails rotates at 100 MB, which is how a 100 MB log once reached a repository. A full run now writes nothing;
VERBOSE_TEST_LOG=1brings it back. - json 3.0 broke every adapter. It dropped the two-argument parse form Rails relies on, so reading any JSON column raised. Pinned below 3.0 until Rails ships a compatible decoder.
Verified
| Tests | 297 runs, 1228 assertions, 0 failures |
| Databases | SQLite, PostgreSQL, MySQL |
| Ruby | 3.3, 3.4, 4.0 |
| Rails | 7.1, 7.2, 8.1 |
Full detail in CHANGELOG.md.
What's Changed
- Heal the inbox after a missed broadcast (chats--refresh-inbox) by @rameerez in #2
- chats 0.2.0 — the seams a product gem needs by @rameerez in #4
New Contributors
Full Changelog: v0.1.1...v0.2.0
v0.1.1
The first public release of chats — real-time DMs and group chats for your Rails users in minutes, Hotwire-native end to end: Turbo Streams over Action Cable for live delivery, Stimulus for the thread/composer behavior, zero build step, any Rails 7.1+/8.x app.
gem install chats · on RubyGems: https://rubygems.org/gems/chats
What ships in 0.1.1
- Direct + group conversations with a policy seam (
can_message?) the host controls, viewer-agnostic broadcast bubbles (one render per message, shared by every subscriber), and keyset scroll-up pagination. - Telegram-style long-press actions — a FLIP-morphing bubble lift over a blurred backdrop with a reactions pill and contextual menu (Copy / Edit / Delete + host-injected items). Verified on real WebViews: copy runs inside the user-gesture frame (WebKit drops activation in promise continuations), and Copy/Edit read the popup's visible clone (the original bubble is hidden while lifted).
- Composer edit mode (Telegram's flow), read-horizon receipts (sent/seen double ticks), typing indicators via a Turbo Stream custom action, reactions, attachments with pre-send previews, soft-delete tombstones, inbox search.
- Mobile-WebView resilience: stale-thread catch-up endpoint for suspended sockets, «new messages» divider, DOM budget (~300 bubbles) with reachable trimmed history, chronology guard for out-of-order broadcast appends, tail-gated autoscroll (edits/pagination never yank the viewport).
- Host integration: ejectable views (
rails g chats:views), engine importmap pins,:message_created/:conversation_readnotifier hooks, and moderation-friendly seams (single marked body node, report-link injection point).
Battle-tested as the messaging layer of a production app (iOS + Android Hotwire Native shells + web) before extraction.
Full Changelog: https://github.com/rameerez/chats/commits/v0.1.1