chats 0.3.1 → 0.4.0
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 +100 -0
- data/README.md +232 -10
- data/app/assets/stylesheets/chats.css +95 -1
- data/app/controllers/chats/messages_controller.rb +1 -13
- data/app/helpers/chats/engine_helper.rb +2 -1
- data/app/views/chats/conversations/_locked_composer.html.erb +2 -2
- data/app/views/chats/conversations/index.html.erb +16 -0
- data/app/views/chats/messages/_action.html.erb +15 -0
- data/app/views/chats/messages/_composer.html.erb +74 -60
- data/app/views/chats/messages/_message.html.erb +6 -1
- data/chats-0.3.1.gem +0 -0
- data/config/locales/en.yml +5 -0
- data/config/locales/es.yml +5 -0
- data/docs/screenshots/composer-actions.png +0 -0
- data/docs/screenshots/inbox.png +0 -0
- data/docs/screenshots/system-note-action.png +0 -0
- data/docs/screenshots/thread-actions-phone.png +0 -0
- data/lib/chats/action.rb +70 -0
- data/lib/chats/engine.rb +1 -1
- data/lib/chats/inbox.rb +30 -4
- data/lib/chats/models/concerns/messager.rb +10 -7
- data/lib/chats/models/conversation.rb +20 -22
- data/lib/chats/models/message.rb +81 -8
- data/lib/chats/models/participant.rb +9 -1
- data/lib/chats/send_rate_limited.rb +30 -0
- data/lib/chats/version.rb +1 -1
- data/lib/chats.rb +1 -0
- data/lib/generators/chats/templates/add_action_to_chats_messages.rb.erb +37 -0
- data/lib/generators/chats/templates/create_chats_tables.rb.erb +4 -0
- data/lib/generators/chats/templates/initializer.rb +1 -0
- data/lib/generators/chats/upgrade_generator.rb +37 -6
- metadata +11 -2
|
@@ -43,6 +43,8 @@ module Chats
|
|
|
43
43
|
# concurrent joins race-safe. Same rationale as Conversation#direct_key.
|
|
44
44
|
validate :group_must_have_room, on: :create
|
|
45
45
|
|
|
46
|
+
after_update_commit :notify_departure, if: -> { saved_change_to_left_at? && left? }
|
|
47
|
+
|
|
46
48
|
def owner? = role == "owner"
|
|
47
49
|
def left? = left_at.present?
|
|
48
50
|
def active? = left_at.nil?
|
|
@@ -104,11 +106,17 @@ module Chats
|
|
|
104
106
|
def unmute! = update!(muted_at: nil)
|
|
105
107
|
|
|
106
108
|
def leave!
|
|
109
|
+
return self if left?
|
|
110
|
+
|
|
107
111
|
update!(left_at: Time.current)
|
|
108
|
-
Chats.notify(:participant_left, participant: self)
|
|
109
112
|
self
|
|
110
113
|
end
|
|
111
114
|
|
|
115
|
+
def notify_departure
|
|
116
|
+
Chats.notify(:participant_left, participant: self)
|
|
117
|
+
end
|
|
118
|
+
private :notify_departure
|
|
119
|
+
|
|
112
120
|
# Hand this seat to a different messager, keeping the read horizon, the
|
|
113
121
|
# role and the history: the guest who signs up, the agent who takes over
|
|
114
122
|
# a shared mailbox. The MESSAGES keep their original sender — what was
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Chats
|
|
4
|
+
# Share one sender budget across chat and product-specific HTTP composers.
|
|
5
|
+
# The host owns the cache store. Using its atomic increment also works on
|
|
6
|
+
# Rails 7 and avoids depending on Rails' private controller limiter API.
|
|
7
|
+
module SendRateLimited
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
included do
|
|
11
|
+
before_action :enforce_chat_send_rate_limit, only: :create
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def chat_rate_limit_messager
|
|
17
|
+
send(Chats.config.current_messager_method)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def enforce_chat_send_rate_limit
|
|
21
|
+
limit = Chats.config.send_rate_limit
|
|
22
|
+
return unless limit
|
|
23
|
+
|
|
24
|
+
sender = chat_rate_limit_messager&.to_gid&.to_s || request.remote_ip
|
|
25
|
+
count = self.class.cache_store.increment("rate-limit:chats/messages:#{sender}", 1,
|
|
26
|
+
expires_in: limit.fetch(:within))
|
|
27
|
+
head :too_many_requests if count && count > limit.fetch(:to)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
data/lib/chats/version.rb
CHANGED
data/lib/chats.rb
CHANGED
|
@@ -7,6 +7,7 @@ require_relative "chats/version"
|
|
|
7
7
|
require_relative "chats/errors"
|
|
8
8
|
require_relative "chats/configuration"
|
|
9
9
|
require_relative "chats/subscribers"
|
|
10
|
+
require_relative "chats/send_rate_limited"
|
|
10
11
|
require_relative "chats/macros"
|
|
11
12
|
|
|
12
13
|
require_relative "chats/engine" if defined?(::Rails::Engine)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# chats 0.4.0 — message ACTIONS.
|
|
4
|
+
#
|
|
5
|
+
# A message may carry one featured button: a label and the url it opens
|
|
6
|
+
# ("Your invoice is ready" → Pay now). Stored as {"label":…,"url":…} in its
|
|
7
|
+
# own JSON column — `metadata` stays yours. Nullable, no default: most
|
|
8
|
+
# messages have no action, and MySQL refuses defaults on JSON columns.
|
|
9
|
+
# See Chats::Action.
|
|
10
|
+
#
|
|
11
|
+
# Safe to run on an install that already has the column (a 0.4.0 fresh
|
|
12
|
+
# install does): every step is guarded, so this migration is a no-op there.
|
|
13
|
+
class AddActionToChatsMessages < ActiveRecord::Migration<%= migration_version %>
|
|
14
|
+
def up
|
|
15
|
+
return if column_exists?(:chats_messages, :action)
|
|
16
|
+
|
|
17
|
+
add_column :chats_messages, :action, json_column_type
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Rolling back KEEPS the column, on purpose: this migration never removes
|
|
21
|
+
# a column it may not have added. On a fresh 0.4.0 install the create
|
|
22
|
+
# migration owns `action` and `up` above did nothing, so dropping it here
|
|
23
|
+
# would destroy every message's button with it — and nothing a migration
|
|
24
|
+
# can see records which of the two added it. A leftover nullable column
|
|
25
|
+
# is harmless to older versions of chats; drop it by hand if you mean to.
|
|
26
|
+
def down; end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# jsonb on every PostgreSQL adapter, like the install migration — matched
|
|
31
|
+
# by prefix because PostGIS answers "PostGIS", not "PostgreSQL".
|
|
32
|
+
def json_column_type
|
|
33
|
+
return :jsonb if connection.adapter_name.match?(/\Apostg/i)
|
|
34
|
+
|
|
35
|
+
:json
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -101,6 +101,10 @@ class CreateChatsTables < ActiveRecord::Migration<%= migration_version %>
|
|
|
101
101
|
t.datetime :edited_at
|
|
102
102
|
t.datetime :deleted_at
|
|
103
103
|
t.send(json_column_type, :metadata, default: json_column_default)
|
|
104
|
+
# The one featured button a message may carry ({"label":…,"url":…}),
|
|
105
|
+
# set by the host from Ruby — see Chats::Action. Nullable with no
|
|
106
|
+
# default: most messages have none, and MySQL refuses JSON defaults.
|
|
107
|
+
t.send(json_column_type, :action)
|
|
104
108
|
|
|
105
109
|
t.timestamps
|
|
106
110
|
end
|
|
@@ -223,6 +223,7 @@ Chats.configure do |config|
|
|
|
223
223
|
# app/views/chats/slots/_inbox_top.html.erb above the first inbox row
|
|
224
224
|
# app/views/chats/slots/_inbox_empty.html.erb inside the empty state
|
|
225
225
|
# app/views/chats/slots/_conversation_header_actions.html.erb thread menu
|
|
226
|
+
# app/views/chats/slots/_composer_actions.html.erb your own buttons above the composer
|
|
226
227
|
# app/views/chats/slots/_locked_composer.html.erb the locked composer's body
|
|
227
228
|
# app/views/chats/slots/_message_meta.html.erb after each bubble's timestamp
|
|
228
229
|
#
|
|
@@ -9,14 +9,17 @@ module Chats
|
|
|
9
9
|
# needs into an EXISTING install. Nothing else: the initializer, the
|
|
10
10
|
# views and the routes you already own stay untouched.
|
|
11
11
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
12
|
+
# Writes every migration since 0.1.x — 0.2.0's message authorship and
|
|
13
|
+
# 0.4.0's message action — in that order. Each is written guarded, so
|
|
14
|
+
# running this against an install that already has some of the columns
|
|
15
|
+
# (a fresh 0.2.0 install has the author ones) is a no-op for those
|
|
16
|
+
# rather than an error, and re-running it skips files already copied.
|
|
17
|
+
# The action migration is not written at all where the column exists.
|
|
15
18
|
class UpgradeGenerator < Rails::Generators::Base
|
|
16
19
|
include ActiveRecord::Generators::Migration
|
|
17
20
|
|
|
18
21
|
source_root File.expand_path("templates", __dir__)
|
|
19
|
-
desc "Add the migrations a chats version bump needs (0.2.0: message authorship)"
|
|
22
|
+
desc "Add the migrations a chats version bump needs (0.2.0: message authorship; 0.4.0: message actions)"
|
|
20
23
|
|
|
21
24
|
def self.next_migration_number(dir)
|
|
22
25
|
ActiveRecord::Generators::Base.next_migration_number(dir)
|
|
@@ -27,19 +30,47 @@ module Chats
|
|
|
27
30
|
File.join(db_migrate_path, "add_author_to_chats_messages.rb")
|
|
28
31
|
end
|
|
29
32
|
|
|
33
|
+
# Not written when the schema already HAS the column — a fresh 0.4.0
|
|
34
|
+
# install, whose create migration owns it. A second migration for a
|
|
35
|
+
# column it didn't add is one nobody can safely roll back. Without a
|
|
36
|
+
# database to ask, it is written anyway; its `up` is guarded, and its
|
|
37
|
+
# `down` never drops the column.
|
|
38
|
+
def create_action_migration
|
|
39
|
+
if action_column_present?
|
|
40
|
+
say_status :skip, "chats_messages.action already exists — no migration needed", :yellow
|
|
41
|
+
return
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
migration_template "add_action_to_chats_messages.rb.erb",
|
|
45
|
+
File.join(db_migrate_path, "add_action_to_chats_messages.rb")
|
|
46
|
+
end
|
|
47
|
+
|
|
30
48
|
def display_post_upgrade_message
|
|
31
49
|
say "\n💬 chats upgrade migrations copied.", :green
|
|
32
50
|
say "\n 1. Run 'rails db:migrate'."
|
|
33
|
-
say " 2. New in 0.
|
|
51
|
+
say " 2. New in 0.4.0 — a message can carry one button (the `action` column):"
|
|
52
|
+
say " alice.message!(bob, \"Let's play!\", action: { label: \"Join my room\", url: room_path(room) })"
|
|
53
|
+
say " app/views/chats/slots/_composer_actions.html.erb # your own buttons by the composer"
|
|
54
|
+
say " 3. New in 0.2.0 — all opt-in, nothing changes until you ask:"
|
|
34
55
|
say " acts_as_messager notifications: false, blockable: false, inbox: :grouped"
|
|
35
56
|
say " Chats.on(:message_created) { |message| … } # replaces config.notifier"
|
|
36
57
|
say " config.messager_url / config.message_signature / config.inbox_scope"
|
|
37
58
|
say " chat_locked? / chat_locked_notice on your chat subjects"
|
|
38
|
-
say "
|
|
59
|
+
say " 4. See the CHANGELOG for the full list.\n", :green
|
|
39
60
|
end
|
|
40
61
|
|
|
41
62
|
private
|
|
42
63
|
|
|
64
|
+
def action_column_present?
|
|
65
|
+
ActiveRecord::Base.connection_pool.with_connection do |connection|
|
|
66
|
+
connection.column_exists?(:chats_messages, :action)
|
|
67
|
+
end
|
|
68
|
+
rescue ActiveRecord::ActiveRecordError
|
|
69
|
+
# No database configured, not created yet, or no chats tables: there
|
|
70
|
+
# is nothing to ask, so write the (guarded) migration.
|
|
71
|
+
false
|
|
72
|
+
end
|
|
73
|
+
|
|
43
74
|
def migration_version
|
|
44
75
|
"[#{ActiveRecord::VERSION::STRING.to_f}]"
|
|
45
76
|
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.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rameerez
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-10-17 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: activerecord
|
|
@@ -145,6 +145,7 @@ files:
|
|
|
145
145
|
- app/views/chats/conversations/index.html.erb
|
|
146
146
|
- app/views/chats/conversations/refresh.turbo_stream.erb
|
|
147
147
|
- app/views/chats/conversations/show.html.erb
|
|
148
|
+
- app/views/chats/messages/_action.html.erb
|
|
148
149
|
- app/views/chats/messages/_composer.html.erb
|
|
149
150
|
- app/views/chats/messages/_message.html.erb
|
|
150
151
|
- app/views/chats/messages/create.turbo_stream.erb
|
|
@@ -152,6 +153,7 @@ files:
|
|
|
152
153
|
- app/views/chats/messages/locked.turbo_stream.erb
|
|
153
154
|
- app/views/chats/shared/_unread_badge.html.erb
|
|
154
155
|
- app/views/chats/shared/_verified_badge.html.erb
|
|
156
|
+
- chats-0.3.1.gem
|
|
155
157
|
- config/importmap.rb
|
|
156
158
|
- config/locales/en.yml
|
|
157
159
|
- config/locales/es.yml
|
|
@@ -159,10 +161,15 @@ files:
|
|
|
159
161
|
- context7.json
|
|
160
162
|
- docs/PRD.md
|
|
161
163
|
- docs/campfire_review.md
|
|
164
|
+
- docs/screenshots/composer-actions.png
|
|
165
|
+
- docs/screenshots/inbox.png
|
|
166
|
+
- docs/screenshots/system-note-action.png
|
|
167
|
+
- docs/screenshots/thread-actions-phone.png
|
|
162
168
|
- gemfiles/rails_7.1.gemfile
|
|
163
169
|
- gemfiles/rails_7.2.gemfile
|
|
164
170
|
- gemfiles/rails_8.1.gemfile
|
|
165
171
|
- lib/chats.rb
|
|
172
|
+
- lib/chats/action.rb
|
|
166
173
|
- lib/chats/broadcasts.rb
|
|
167
174
|
- lib/chats/configuration.rb
|
|
168
175
|
- lib/chats/engine.rb
|
|
@@ -177,9 +184,11 @@ files:
|
|
|
177
184
|
- lib/chats/models/message.rb
|
|
178
185
|
- lib/chats/models/participant.rb
|
|
179
186
|
- lib/chats/models/reaction.rb
|
|
187
|
+
- lib/chats/send_rate_limited.rb
|
|
180
188
|
- lib/chats/subscribers.rb
|
|
181
189
|
- lib/chats/version.rb
|
|
182
190
|
- lib/generators/chats/install_generator.rb
|
|
191
|
+
- lib/generators/chats/templates/add_action_to_chats_messages.rb.erb
|
|
183
192
|
- lib/generators/chats/templates/add_author_to_chats_messages.rb.erb
|
|
184
193
|
- lib/generators/chats/templates/create_chats_tables.rb.erb
|
|
185
194
|
- lib/generators/chats/templates/initializer.rb
|