chats 0.1.1 → 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 +233 -0
- data/README.md +214 -28
- data/app/assets/stylesheets/chats.css +86 -0
- data/app/controllers/chats/conversations_controller.rb +39 -42
- data/app/controllers/chats/messages_controller.rb +34 -0
- data/app/controllers/chats/reactions_controller.rb +14 -0
- data/app/helpers/chats/engine_helper.rb +113 -2
- data/app/javascript/chats/refresh_inbox_controller.js +86 -0
- data/app/views/chats/conversations/_conversation_row.html.erb +7 -1
- data/app/views/chats/conversations/_group.html.erb +41 -0
- data/app/views/chats/conversations/_locked_composer.html.erb +15 -0
- data/app/views/chats/conversations/index.html.erb +39 -9
- data/app/views/chats/conversations/show.html.erb +48 -3
- data/app/views/chats/messages/_composer.html.erb +4 -0
- data/app/views/chats/messages/_message.html.erb +29 -8
- data/app/views/chats/messages/locked.turbo_stream.erb +6 -0
- data/app/views/chats/shared/_verified_badge.html.erb +32 -0
- data/config/importmap.rb +2 -1
- data/config/locales/en.yml +13 -0
- data/config/locales/es.yml +13 -0
- data/context7.json +4 -0
- data/docs/PRD.md +1 -1
- data/docs/campfire_review.md +1 -1
- data/gemfiles/rails_7.1.gemfile +1 -0
- data/gemfiles/rails_7.2.gemfile +1 -0
- data/gemfiles/rails_8.1.gemfile +1 -0
- data/lib/chats/configuration.rb +79 -1
- data/lib/chats/engine.rb +29 -7
- data/lib/chats/errors.rb +16 -0
- data/lib/chats/inbox.rb +303 -0
- data/lib/chats/inbox_group.rb +75 -0
- data/lib/chats/macros.rb +33 -1
- data/lib/chats/models/concerns/chat_subject.rb +23 -0
- data/lib/chats/models/concerns/messager.rb +99 -2
- data/lib/chats/models/conversation.rb +78 -7
- data/lib/chats/models/message.rb +67 -3
- data/lib/chats/models/participant.rb +59 -0
- data/lib/chats/models/reaction.rb +5 -0
- data/lib/chats/subscribers.rb +156 -0
- data/lib/chats/version.rb +1 -1
- data/lib/chats.rb +122 -15
- data/lib/generators/chats/templates/add_author_to_chats_messages.rb.erb +44 -0
- data/lib/generators/chats/templates/create_chats_tables.rb.erb +18 -2
- data/lib/generators/chats/templates/initializer.rb +106 -14
- data/lib/generators/chats/upgrade_generator.rb +48 -0
- metadata +13 -2
data/lib/chats.rb
CHANGED
|
@@ -6,6 +6,7 @@ require "global_id"
|
|
|
6
6
|
require_relative "chats/version"
|
|
7
7
|
require_relative "chats/errors"
|
|
8
8
|
require_relative "chats/configuration"
|
|
9
|
+
require_relative "chats/subscribers"
|
|
9
10
|
require_relative "chats/macros"
|
|
10
11
|
|
|
11
12
|
require_relative "chats/engine" if defined?(::Rails::Engine)
|
|
@@ -24,6 +25,8 @@ require_relative "chats/engine" if defined?(::Rails::Engine)
|
|
|
24
25
|
# user.chat_with(other) # find-or-create a direct conversation
|
|
25
26
|
# user.message!(other, "hello!") # ...and say something in one line
|
|
26
27
|
#
|
|
28
|
+
# Chats.on(:message_created) { |m| } # subscribe to the domain moments
|
|
29
|
+
#
|
|
27
30
|
# Everything else (controllers, views, broadcasts) ships with the engine and
|
|
28
31
|
# is overridable the Devise way (`rails g chats:views`).
|
|
29
32
|
module Chats
|
|
@@ -48,9 +51,16 @@ module Chats
|
|
|
48
51
|
@config = Configuration.new
|
|
49
52
|
@messager_classes = nil
|
|
50
53
|
@subject_classes = nil
|
|
54
|
+
reset_subscribers!
|
|
51
55
|
self
|
|
52
56
|
end
|
|
53
57
|
|
|
58
|
+
# The gem's own deprecator (registered with `Rails.application.deprecators`
|
|
59
|
+
# by the engine, so `config.active_support.deprecation` governs it).
|
|
60
|
+
def deprecator
|
|
61
|
+
@deprecator ||= ActiveSupport::Deprecation.new("1.0", "chats")
|
|
62
|
+
end
|
|
63
|
+
|
|
54
64
|
# --- Registries -----------------------------------------------------------
|
|
55
65
|
#
|
|
56
66
|
# `acts_as_messager` / `acts_as_chat_subject` self-register the calling
|
|
@@ -123,23 +133,34 @@ module Chats
|
|
|
123
133
|
config.can_message.call(sender, recipient)
|
|
124
134
|
end
|
|
125
135
|
|
|
126
|
-
#
|
|
127
|
-
|
|
128
|
-
#
|
|
129
|
-
#
|
|
136
|
+
# --- Events ---------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
# Subscribe to a domain moment. Many subscribers per event; each one runs
|
|
139
|
+
# isolated, so a raising subscriber is reported and the others still run.
|
|
140
|
+
#
|
|
141
|
+
# Chats.on(:message_created) { |message| }
|
|
142
|
+
# Chats.on(:conversation_created) { |conversation| }
|
|
143
|
+
# Chats.on(:participant_left) { |participant| }
|
|
144
|
+
# Chats.on(:conversation_read) { |conversation:, participant:| }
|
|
130
145
|
#
|
|
131
|
-
#
|
|
132
|
-
#
|
|
133
|
-
#
|
|
134
|
-
|
|
146
|
+
# Pass `key:` from reloadable code (a `to_prepare` block): re-registering
|
|
147
|
+
# the same key REPLACES the previous subscriber instead of stacking a
|
|
148
|
+
# duplicate on every code reload.
|
|
149
|
+
def on(event, key: nil, &block)
|
|
150
|
+
Subscribers.on(event, key: key, &block)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Drop every `Chats.on` registration (also called by `reset!`).
|
|
154
|
+
def reset_subscribers!
|
|
155
|
+
Subscribers.reset!
|
|
156
|
+
self
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Fire a domain event at every subscriber (see Chats.on). Error-isolated:
|
|
160
|
+
# a broken subscriber must never break message delivery itself — the
|
|
161
|
+
# message is already committed; notifications are best-effort fan-out.
|
|
135
162
|
def notify(event, **payload)
|
|
136
|
-
|
|
137
|
-
rescue StandardError => e
|
|
138
|
-
# A broken notifier must never break message delivery itself — the
|
|
139
|
-
# message is already committed; notifications are best-effort fan-out.
|
|
140
|
-
# Same error-isolation philosophy as pricing_plans' lifecycle callbacks.
|
|
141
|
-
logger&.error("[chats] notifier raised on #{event}: #{e.class}: #{e.message}")
|
|
142
|
-
nil
|
|
163
|
+
Subscribers.emit(event, **payload)
|
|
143
164
|
end
|
|
144
165
|
|
|
145
166
|
# --- Display helpers (used by the bundled views) --------------------------
|
|
@@ -156,6 +177,80 @@ module Chats
|
|
|
156
177
|
config.messager_avatar.call(messager)
|
|
157
178
|
end
|
|
158
179
|
|
|
180
|
+
# Where a messager's profile lives, per `config.messager_url` (nil by
|
|
181
|
+
# default — the bundled views then render plain text, never a dead link).
|
|
182
|
+
def messager_url_for(messager)
|
|
183
|
+
return nil if messager.nil?
|
|
184
|
+
|
|
185
|
+
config.messager_url.call(messager).presence
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# The signature line under a signed message ("— Lucía G."), per
|
|
189
|
+
# `config.message_signature` when the host sets one. Nil for messages
|
|
190
|
+
# that aren't signed (see Chats::Message#signed?).
|
|
191
|
+
def message_signature_for(message)
|
|
192
|
+
return nil if message.nil? || !message.signed?
|
|
193
|
+
|
|
194
|
+
if config.message_signature
|
|
195
|
+
config.message_signature.call(message).presence
|
|
196
|
+
else
|
|
197
|
+
I18n.t("chats.message.signature", name: display_name_for(message.author))
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# --- Messager options (see acts_as_messager) --------------------------------
|
|
202
|
+
|
|
203
|
+
# Whether +messager+ can be notified at all. False for headless messagers
|
|
204
|
+
# declared with `acts_as_messager notifications: false` (a support desk, a
|
|
205
|
+
# bot): hosts stop branching on class in every notifier.
|
|
206
|
+
def notifications_for?(messager)
|
|
207
|
+
messager_option(messager, :chat_notifications?)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Whether block/report affordances make sense against +messager+.
|
|
211
|
+
# False for `acts_as_messager blockable: false`.
|
|
212
|
+
def blockable?(messager)
|
|
213
|
+
messager_option(messager, :chat_blockable?)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Whether +messager+'s direct conversations stack into one inbox row
|
|
217
|
+
# (`acts_as_messager inbox: :grouped`).
|
|
218
|
+
def grouped_inbox?(messager)
|
|
219
|
+
messager_option(messager, :chat_grouped_inbox?, default: false)
|
|
220
|
+
end
|
|
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
|
+
|
|
230
|
+
# The polymorphic type names of every registered messager class that
|
|
231
|
+
# stacks (`inbox: :grouped`). Empty in an ordinary app — which is what
|
|
232
|
+
# keeps the inbox query there byte-identical to 0.1.x. Used as a SQL
|
|
233
|
+
# PREFILTER only; whether a given counterpart actually stacks is still
|
|
234
|
+
# decided per-record by `grouped_inbox?` (STI subclasses share a
|
|
235
|
+
# polymorphic_name with siblings that may not be grouped).
|
|
236
|
+
def grouped_messager_types
|
|
237
|
+
messager_class_names.filter_map do |name|
|
|
238
|
+
klass = name.safe_constantize
|
|
239
|
+
next unless klass.respond_to?(:chat_grouped_inbox?) && klass.chat_grouped_inbox?
|
|
240
|
+
|
|
241
|
+
klass.polymorphic_name
|
|
242
|
+
end.uniq
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# The signed GlobalID that scopes the inbox to conversations with
|
|
246
|
+
# +messager+ (`GET /conversations?with=…`). Purpose-scoped and
|
|
247
|
+
# non-expiring: inbox rows live on long-lived pages.
|
|
248
|
+
def inbox_with_sgid(messager)
|
|
249
|
+
return nil if messager.nil?
|
|
250
|
+
|
|
251
|
+
messager.to_sgid(expires_in: nil, for: :chats_inbox_with).to_s
|
|
252
|
+
end
|
|
253
|
+
|
|
159
254
|
# --- Internals ------------------------------------------------------------
|
|
160
255
|
|
|
161
256
|
def logger
|
|
@@ -172,6 +267,18 @@ module Chats
|
|
|
172
267
|
|
|
173
268
|
private
|
|
174
269
|
|
|
270
|
+
# Ask a messager's CLASS about an `acts_as_messager` option. Duck-typed
|
|
271
|
+
# (never `is_a?`): anything that doesn't answer is treated as a stock
|
|
272
|
+
# messager, so a plain host model keeps 0.1.x behaviour.
|
|
273
|
+
def messager_option(messager, predicate, default: true)
|
|
274
|
+
return default if messager.nil?
|
|
275
|
+
|
|
276
|
+
klass = messager.is_a?(Class) ? messager : messager.class
|
|
277
|
+
return default unless klass.respond_to?(predicate)
|
|
278
|
+
|
|
279
|
+
klass.public_send(predicate)
|
|
280
|
+
end
|
|
281
|
+
|
|
175
282
|
def registered_class?(registry, klass)
|
|
176
283
|
klass = klass.class unless klass.is_a?(Class) || klass.is_a?(String)
|
|
177
284
|
name = klass.is_a?(String) ? klass : klass.name
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# chats 0.2.0 — message AUTHORSHIP.
|
|
4
|
+
#
|
|
5
|
+
# `sender` is the seat a message came from; `author` is who wrote it on that
|
|
6
|
+
# seat's behalf (an agent answering from a shared support desk). Both
|
|
7
|
+
# nullable, both polymorphic, indexed together because they're always read
|
|
8
|
+
# together. See Chats::Message#signed?.
|
|
9
|
+
#
|
|
10
|
+
# Safe to run on an install that already has the columns (a 0.2.0 fresh
|
|
11
|
+
# install does): every step is guarded, so this migration is a no-op there.
|
|
12
|
+
class AddAuthorToChatsMessages < ActiveRecord::Migration<%= migration_version %>
|
|
13
|
+
def up
|
|
14
|
+
unless column_exists?(:chats_messages, :author_type)
|
|
15
|
+
add_reference :chats_messages, :author, polymorphic: true, null: true,
|
|
16
|
+
type: chats_foreign_key_type, index: false
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
return if index_exists?(:chats_messages, [ :author_type, :author_id ], name: "index_chats_messages_on_author")
|
|
20
|
+
|
|
21
|
+
add_index :chats_messages, [ :author_type, :author_id ], name: "index_chats_messages_on_author"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def down
|
|
25
|
+
if index_exists?(:chats_messages, [ :author_type, :author_id ], name: "index_chats_messages_on_author")
|
|
26
|
+
remove_index :chats_messages, name: "index_chats_messages_on_author"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
return unless column_exists?(:chats_messages, :author_type)
|
|
30
|
+
|
|
31
|
+
remove_column :chats_messages, :author_type
|
|
32
|
+
remove_column :chats_messages, :author_id
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# Honor the host's configured primary key type (uuid vs bigint), exactly
|
|
38
|
+
# like the install migration does — the author columns must match the key
|
|
39
|
+
# type of the models they point at.
|
|
40
|
+
def chats_foreign_key_type
|
|
41
|
+
config = Rails.configuration.generators
|
|
42
|
+
config.options[config.orm][:primary_key_type] || :bigint
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -88,6 +88,12 @@ class CreateChatsTables < ActiveRecord::Migration<%= migration_version %>
|
|
|
88
88
|
foreign_key: { to_table: :chats_conversations }, index: false
|
|
89
89
|
t.references :sender, polymorphic: true, null: true, type: foreign_key_type, index: false
|
|
90
90
|
|
|
91
|
+
# Who WROTE it, when that isn't the seat it was sent FROM: an agent
|
|
92
|
+
# answering from a shared support-desk seat signs the bubble while the
|
|
93
|
+
# desk stays the conversation identity. Nullable — ordinary messages
|
|
94
|
+
# have no author. See Chats::Message#signed?.
|
|
95
|
+
t.references :author, polymorphic: true, null: true, type: foreign_key_type, index: false
|
|
96
|
+
|
|
91
97
|
t.string :kind, null: false, default: "text"
|
|
92
98
|
t.text :body
|
|
93
99
|
t.references :reply_to, type: foreign_key_type, null: true,
|
|
@@ -103,6 +109,7 @@ class CreateChatsTables < ActiveRecord::Migration<%= migration_version %>
|
|
|
103
109
|
# (created_at, id) — see Chats::Message.before_message.
|
|
104
110
|
add_index :chats_messages, [ :conversation_id, :created_at, :id ], name: "index_chats_messages_on_conversation_and_created_at"
|
|
105
111
|
add_index :chats_messages, [ :sender_type, :sender_id ], name: "index_chats_messages_on_sender"
|
|
112
|
+
add_index :chats_messages, [ :author_type, :author_id ], name: "index_chats_messages_on_author"
|
|
106
113
|
add_index :chats_messages, :reply_to_id, name: "index_chats_messages_on_reply_to_id"
|
|
107
114
|
|
|
108
115
|
# ---------------------------------------------------------------------------
|
|
@@ -142,8 +149,12 @@ class CreateChatsTables < ActiveRecord::Migration<%= migration_version %>
|
|
|
142
149
|
[primary_key_type, foreign_key_type]
|
|
143
150
|
end
|
|
144
151
|
|
|
152
|
+
# jsonb on every PostgreSQL adapter — matched by prefix because PostGIS
|
|
153
|
+
# (activerecord-postgis-adapter) answers "PostGIS", not "PostgreSQL", and
|
|
154
|
+
# an `include?("postgresql")` check silently sent such hosts down the plain
|
|
155
|
+
# json path.
|
|
145
156
|
def json_column_type
|
|
146
|
-
return :jsonb if connection.adapter_name.
|
|
157
|
+
return :jsonb if connection.adapter_name.match?(/\Apostg/i)
|
|
147
158
|
|
|
148
159
|
:json
|
|
149
160
|
end
|
|
@@ -151,8 +162,13 @@ class CreateChatsTables < ActiveRecord::Migration<%= migration_version %>
|
|
|
151
162
|
# MySQL 8+ doesn't allow default values on JSON columns.
|
|
152
163
|
# Returns an empty-hash default for SQLite/PostgreSQL, nil for MySQL.
|
|
153
164
|
# The model handles nil metadata gracefully (attribute default {}).
|
|
165
|
+
#
|
|
166
|
+
# Trilogy is MySQL under a different ADAPTER_NAME (Rails reports "Trilogy"),
|
|
167
|
+
# so match both — a /mysql/ pattern alone hands a Trilogy host a default
|
|
168
|
+
# MySQL rejects. api_keys hit this first; see its create_api_keys_table
|
|
169
|
+
# template, which matches /mysql|trilogy/.
|
|
154
170
|
def json_column_default
|
|
155
|
-
return nil if connection.adapter_name.
|
|
171
|
+
return nil if connection.adapter_name.match?(/mysql|trilogy/i)
|
|
156
172
|
|
|
157
173
|
{}
|
|
158
174
|
end
|
|
@@ -14,6 +14,22 @@ Chats.configure do |config|
|
|
|
14
14
|
# Default: "User"
|
|
15
15
|
config.messager_class = "User"
|
|
16
16
|
|
|
17
|
+
# Any model can converse, and a model that isn't a person can say so:
|
|
18
|
+
#
|
|
19
|
+
# class SupportDesk < ApplicationRecord
|
|
20
|
+
# acts_as_messager notifications: false, # never notifiable
|
|
21
|
+
# blockable: false, # no block/report affordances
|
|
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
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# `group_path:` says where that stacked row goes when it holds more than
|
|
29
|
+
# one conversation (default: chats' own filtered inbox):
|
|
30
|
+
#
|
|
31
|
+
# acts_as_messager inbox: :grouped, group_path: ->(viewer) { support_path }
|
|
32
|
+
|
|
17
33
|
# ==========================================================================
|
|
18
34
|
# CONTROLLER INTEGRATION
|
|
19
35
|
# ==========================================================================
|
|
@@ -62,6 +78,12 @@ Chats.configure do |config|
|
|
|
62
78
|
#
|
|
63
79
|
# config.send_rate_limit = { to: 60, within: 1.minute }
|
|
64
80
|
#
|
|
81
|
+
# How many conversations the inbox loads (and therefore how deep search
|
|
82
|
+
# and stacking see). The inbox is a "recent activity" surface, not an
|
|
83
|
+
# archive.
|
|
84
|
+
#
|
|
85
|
+
# config.inbox_limit = 200
|
|
86
|
+
#
|
|
65
87
|
# Encrypt message bodies at rest (ActiveRecord Encryption; requires
|
|
66
88
|
# `bin/rails db:encryption:init`). Body search degrades when enabled.
|
|
67
89
|
#
|
|
@@ -80,6 +102,23 @@ Chats.configure do |config|
|
|
|
80
102
|
# }
|
|
81
103
|
#
|
|
82
104
|
# config.can_create_group = ->(creator) { creator.admin? }
|
|
105
|
+
#
|
|
106
|
+
# Composed into the inbox query before the limit — hide rows, re-scope
|
|
107
|
+
# them, whatever your product needs, without overriding the controller:
|
|
108
|
+
#
|
|
109
|
+
# config.inbox_scope = ->(relation, viewer) { relation }
|
|
110
|
+
#
|
|
111
|
+
# Whether a conversation still accepts messages is NOT a proc: the SUBJECT
|
|
112
|
+
# owns it, because the subject already owns the conversation's meaning.
|
|
113
|
+
#
|
|
114
|
+
# class Ticket < ApplicationRecord
|
|
115
|
+
# acts_as_chat_subject
|
|
116
|
+
# def chat_locked? = closed?
|
|
117
|
+
# def chat_locked_notice = "This ticket is closed. Reply to reopen it."
|
|
118
|
+
# end
|
|
119
|
+
#
|
|
120
|
+
# Locking gates SENDING only: the thread stays readable and the composer
|
|
121
|
+
# is replaced by the notice (see the `locked_composer` slot below).
|
|
83
122
|
|
|
84
123
|
# ==========================================================================
|
|
85
124
|
# TRUST & SAFETY — snap onto the `moderate` gem (or anything else)
|
|
@@ -103,25 +142,35 @@ Chats.configure do |config|
|
|
|
103
142
|
# config.filter "Chats::Message", :body, mode: :flag
|
|
104
143
|
|
|
105
144
|
# ==========================================================================
|
|
106
|
-
#
|
|
145
|
+
# EVENTS — subscribe to the domain moments, fan out anywhere
|
|
107
146
|
# ==========================================================================
|
|
108
147
|
#
|
|
109
|
-
#
|
|
110
|
-
#
|
|
148
|
+
# Many subscribers per event; each runs isolated (a raising one is reported
|
|
149
|
+
# through Rails.error and never breaks message delivery). Keep them fast —
|
|
150
|
+
# enqueue jobs, don't do work inline.
|
|
111
151
|
#
|
|
112
|
-
# :message_created
|
|
113
|
-
# :
|
|
152
|
+
# Chats.on(:message_created) { |message| } # every human message
|
|
153
|
+
# Chats.on(:conversation_created) { |conversation| } # a thread came into being
|
|
154
|
+
# Chats.on(:participant_left) { |participant| } # someone left a group
|
|
155
|
+
# Chats.on(:conversation_read) { |conversation:, participant:| }
|
|
114
156
|
#
|
|
115
157
|
# With Noticed:
|
|
116
|
-
#
|
|
117
|
-
#
|
|
118
|
-
#
|
|
119
|
-
#
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
#
|
|
123
|
-
#
|
|
124
|
-
#
|
|
158
|
+
# Chats.on(:message_created) { |message| NewMessageNotifier.with(record: message).deliver }
|
|
159
|
+
#
|
|
160
|
+
# With a debounced-email job (see Chats::Participant#should_notify? for the
|
|
161
|
+
# "only email once until they come back" etiquette helper):
|
|
162
|
+
# Chats.on(:message_created) { |message| ChatsUnreadEmailJob.set(wait: 10.minutes).perform_later(message) }
|
|
163
|
+
#
|
|
164
|
+
# Registering from reloadable code? Pass a key, and a reload replaces the
|
|
165
|
+
# subscriber instead of stacking a second one:
|
|
166
|
+
#
|
|
167
|
+
# Rails.application.config.to_prepare do
|
|
168
|
+
# Chats.on(:message_created, key: :unread_email) { |message| … }
|
|
169
|
+
# end
|
|
170
|
+
#
|
|
171
|
+
# DEPRECATED (removed in 1.0): `config.notifier = ->(event, **payload) {}`
|
|
172
|
+
# still works, but receives :message_created and :conversation_read only —
|
|
173
|
+
# the events 0.1.1 had. The ones added in 0.2.0 are Chats.on-only.
|
|
125
174
|
|
|
126
175
|
# ==========================================================================
|
|
127
176
|
# DISPLAY — how messagers appear in the bundled views
|
|
@@ -135,4 +184,47 @@ Chats.configure do |config|
|
|
|
135
184
|
# config.messager_avatar = ->(messager) {
|
|
136
185
|
# messager.avatar.attached? ? messager.avatar.variant(:thumb) : nil
|
|
137
186
|
# }
|
|
187
|
+
#
|
|
188
|
+
# Where a messager's profile lives. nil (the default) means the bundled
|
|
189
|
+
# views render names as plain text — chats never assumes you have a
|
|
190
|
+
# `user_path`, and never renders a dead anchor:
|
|
191
|
+
#
|
|
192
|
+
# config.messager_url = lambda do |messager|
|
|
193
|
+
# routes = Rails.application.routes.url_helpers
|
|
194
|
+
#
|
|
195
|
+
# case messager
|
|
196
|
+
# when User then routes.user_path(messager) # a desk or a bot has no profile: nil
|
|
197
|
+
# end
|
|
198
|
+
# end
|
|
199
|
+
#
|
|
200
|
+
# The signature under a message written by an AUTHOR on a sender's behalf
|
|
201
|
+
# (`desk.message!(user, "On it!", author: agent)`). Defaults to the
|
|
202
|
+
# localized "— Agent Name":
|
|
203
|
+
#
|
|
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
|
|
214
|
+
|
|
215
|
+
# ==========================================================================
|
|
216
|
+
# SLOTS — add one row or one button without ejecting a screen
|
|
217
|
+
# ==========================================================================
|
|
218
|
+
#
|
|
219
|
+
# The bundled views render a partial named `chats/slots/_<slot>` whenever
|
|
220
|
+
# one exists in your app. No configuration, no registration: create the
|
|
221
|
+
# file and it appears.
|
|
222
|
+
#
|
|
223
|
+
# app/views/chats/slots/_inbox_top.html.erb above the first inbox row
|
|
224
|
+
# app/views/chats/slots/_inbox_empty.html.erb inside the empty state
|
|
225
|
+
# app/views/chats/slots/_conversation_header_actions.html.erb thread menu
|
|
226
|
+
# app/views/chats/slots/_locked_composer.html.erb the locked composer's body
|
|
227
|
+
# app/views/chats/slots/_message_meta.html.erb after each bubble's timestamp
|
|
228
|
+
#
|
|
229
|
+
# `rails generate chats:views` is still there for wholesale restyling.
|
|
138
230
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
module Chats
|
|
7
|
+
module Generators
|
|
8
|
+
# `rails generate chats:upgrade` — copy the migrations a version bump
|
|
9
|
+
# needs into an EXISTING install. Nothing else: the initializer, the
|
|
10
|
+
# views and the routes you already own stay untouched.
|
|
11
|
+
#
|
|
12
|
+
# Currently writes the 0.2.0 migration (message authorship). It is
|
|
13
|
+
# written guarded, so running it against an install that already has the
|
|
14
|
+
# columns (a fresh 0.2.0 install) is a no-op rather than an error.
|
|
15
|
+
class UpgradeGenerator < Rails::Generators::Base
|
|
16
|
+
include ActiveRecord::Generators::Migration
|
|
17
|
+
|
|
18
|
+
source_root File.expand_path("templates", __dir__)
|
|
19
|
+
desc "Add the migrations a chats version bump needs (0.2.0: message authorship)"
|
|
20
|
+
|
|
21
|
+
def self.next_migration_number(dir)
|
|
22
|
+
ActiveRecord::Generators::Base.next_migration_number(dir)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def create_author_migration
|
|
26
|
+
migration_template "add_author_to_chats_messages.rb.erb",
|
|
27
|
+
File.join(db_migrate_path, "add_author_to_chats_messages.rb")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def display_post_upgrade_message
|
|
31
|
+
say "\n💬 chats upgrade migrations copied.", :green
|
|
32
|
+
say "\n 1. Run 'rails db:migrate'."
|
|
33
|
+
say " 2. New in 0.2.0 — all opt-in, nothing changes until you ask:"
|
|
34
|
+
say " acts_as_messager notifications: false, blockable: false, inbox: :grouped"
|
|
35
|
+
say " Chats.on(:message_created) { |message| … } # replaces config.notifier"
|
|
36
|
+
say " config.messager_url / config.message_signature / config.inbox_scope"
|
|
37
|
+
say " chat_locked? / chat_locked_notice on your chat subjects"
|
|
38
|
+
say " 3. See the CHANGELOG for the full list.\n", :green
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def migration_version
|
|
44
|
+
"[#{ActiveRecord::VERSION::STRING.to_f}]"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
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.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rameerez
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-09-16 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: activerecord
|
|
@@ -135,8 +135,11 @@ files:
|
|
|
135
135
|
- app/helpers/chats/engine_helper.rb
|
|
136
136
|
- app/javascript/chats/composer_controller.js
|
|
137
137
|
- app/javascript/chats/debounced_submit_controller.js
|
|
138
|
+
- app/javascript/chats/refresh_inbox_controller.js
|
|
138
139
|
- app/javascript/chats/thread_controller.js
|
|
139
140
|
- app/views/chats/conversations/_conversation_row.html.erb
|
|
141
|
+
- app/views/chats/conversations/_group.html.erb
|
|
142
|
+
- app/views/chats/conversations/_locked_composer.html.erb
|
|
140
143
|
- app/views/chats/conversations/_messages_page.html.erb
|
|
141
144
|
- app/views/chats/conversations/_read_state.html.erb
|
|
142
145
|
- app/views/chats/conversations/index.html.erb
|
|
@@ -146,11 +149,14 @@ files:
|
|
|
146
149
|
- app/views/chats/messages/_message.html.erb
|
|
147
150
|
- app/views/chats/messages/create.turbo_stream.erb
|
|
148
151
|
- app/views/chats/messages/errors.turbo_stream.erb
|
|
152
|
+
- app/views/chats/messages/locked.turbo_stream.erb
|
|
149
153
|
- app/views/chats/shared/_unread_badge.html.erb
|
|
154
|
+
- app/views/chats/shared/_verified_badge.html.erb
|
|
150
155
|
- config/importmap.rb
|
|
151
156
|
- config/locales/en.yml
|
|
152
157
|
- config/locales/es.yml
|
|
153
158
|
- config/routes.rb
|
|
159
|
+
- context7.json
|
|
154
160
|
- docs/PRD.md
|
|
155
161
|
- docs/campfire_review.md
|
|
156
162
|
- gemfiles/rails_7.1.gemfile
|
|
@@ -161,6 +167,8 @@ files:
|
|
|
161
167
|
- lib/chats/configuration.rb
|
|
162
168
|
- lib/chats/engine.rb
|
|
163
169
|
- lib/chats/errors.rb
|
|
170
|
+
- lib/chats/inbox.rb
|
|
171
|
+
- lib/chats/inbox_group.rb
|
|
164
172
|
- lib/chats/macros.rb
|
|
165
173
|
- lib/chats/models/application_record.rb
|
|
166
174
|
- lib/chats/models/concerns/chat_subject.rb
|
|
@@ -169,10 +177,13 @@ files:
|
|
|
169
177
|
- lib/chats/models/message.rb
|
|
170
178
|
- lib/chats/models/participant.rb
|
|
171
179
|
- lib/chats/models/reaction.rb
|
|
180
|
+
- lib/chats/subscribers.rb
|
|
172
181
|
- lib/chats/version.rb
|
|
173
182
|
- lib/generators/chats/install_generator.rb
|
|
183
|
+
- lib/generators/chats/templates/add_author_to_chats_messages.rb.erb
|
|
174
184
|
- lib/generators/chats/templates/create_chats_tables.rb.erb
|
|
175
185
|
- lib/generators/chats/templates/initializer.rb
|
|
186
|
+
- lib/generators/chats/upgrade_generator.rb
|
|
176
187
|
- lib/generators/chats/views_generator.rb
|
|
177
188
|
homepage: https://github.com/rameerez/chats
|
|
178
189
|
licenses:
|