turbo_chat 0.1.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.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +21 -0
  3. data/README.md +741 -0
  4. data/app/assets/config/chat_gem_manifest.js +6 -0
  5. data/app/assets/javascripts/chat_gem/application.js +4 -0
  6. data/app/assets/javascripts/chat_gem/lifecycle_events.js +93 -0
  7. data/app/assets/javascripts/chat_gem/messages.js +442 -0
  8. data/app/assets/javascripts/chat_gem/realtime.js +398 -0
  9. data/app/assets/javascripts/chat_gem/shared.js +488 -0
  10. data/app/assets/stylesheets/chat_gem/application.css +741 -0
  11. data/app/controllers/chat_gem/application_controller.rb +41 -0
  12. data/app/controllers/chat_gem/chat_memberships_controller.rb +81 -0
  13. data/app/controllers/chat_gem/chat_messages_controller.rb +144 -0
  14. data/app/controllers/chat_gem/chats_controller/event_payload_support.rb +58 -0
  15. data/app/controllers/chat_gem/chats_controller/invitation_support.rb +31 -0
  16. data/app/controllers/chat_gem/chats_controller.rb +125 -0
  17. data/app/helpers/chat_gem/application_helper/config_support.rb +41 -0
  18. data/app/helpers/chat_gem/application_helper/mention_support/entry_builder.rb +55 -0
  19. data/app/helpers/chat_gem/application_helper/mention_support/permission_support.rb +28 -0
  20. data/app/helpers/chat_gem/application_helper/mention_support/token_builder.rb +49 -0
  21. data/app/helpers/chat_gem/application_helper/mention_support.rb +80 -0
  22. data/app/helpers/chat_gem/application_helper/message_rendering.rb +165 -0
  23. data/app/helpers/chat_gem/application_helper/participant_support.rb +81 -0
  24. data/app/helpers/chat_gem/application_helper.rb +12 -0
  25. data/app/models/chat_gem/application_record.rb +5 -0
  26. data/app/models/chat_gem/chat.rb +127 -0
  27. data/app/models/chat_gem/chat_membership.rb +136 -0
  28. data/app/models/chat_gem/chat_message/blocked_words_moderation.rb +120 -0
  29. data/app/models/chat_gem/chat_message/body_length_validation.rb +20 -0
  30. data/app/models/chat_gem/chat_message/broadcasting.rb +61 -0
  31. data/app/models/chat_gem/chat_message/formatting.rb +81 -0
  32. data/app/models/chat_gem/chat_message/mention_validation.rb +85 -0
  33. data/app/models/chat_gem/chat_message/signals.rb +61 -0
  34. data/app/models/chat_gem/chat_message.rb +40 -0
  35. data/app/views/chat_gem/chat_messages/_chat_message.html.erb +1 -0
  36. data/app/views/chat_gem/chat_messages/_form.html.erb +22 -0
  37. data/app/views/chat_gem/chat_messages/_message.html.erb +83 -0
  38. data/app/views/chat_gem/chat_messages/_signal.html.erb +3 -0
  39. data/app/views/chat_gem/chat_messages/_signals.html.erb +24 -0
  40. data/app/views/chat_gem/chat_messages/index.html.erb +1 -0
  41. data/app/views/chat_gem/chats/index.html.erb +51 -0
  42. data/app/views/chat_gem/chats/new.html.erb +13 -0
  43. data/app/views/chat_gem/chats/show.html.erb +95 -0
  44. data/app/views/layouts/chat_gem/application.html.erb +20 -0
  45. data/config/routes.rb +16 -0
  46. data/db/migrate/20260215000000_create_chat_gem_chats.rb +8 -0
  47. data/db/migrate/20260215000001_create_chat_gem_chat_memberships.rb +19 -0
  48. data/db/migrate/20260215000002_create_chat_gem_chat_messages.rb +14 -0
  49. data/db/migrate/20260218000011_add_closed_at_to_chat_gem_chats.rb +6 -0
  50. data/db/migrate/20260218000012_add_custom_role_key_to_chat_memberships.rb +6 -0
  51. data/db/migrate/20260218000013_add_invitation_accepted_to_chat_gem_chat_memberships.rb +5 -0
  52. data/lib/chat_gem/configuration.rb +242 -0
  53. data/lib/chat_gem/engine.rb +29 -0
  54. data/lib/chat_gem/model_extensions/chat_participant.rb +45 -0
  55. data/lib/chat_gem/moderation.rb +194 -0
  56. data/lib/chat_gem/permission.rb +193 -0
  57. data/lib/chat_gem/signals.rb +26 -0
  58. data/lib/chat_gem/version.rb +3 -0
  59. data/lib/chat_gem.rb +24 -0
  60. data/lib/generators/chat_gem/install/install_generator.rb +18 -0
  61. data/lib/generators/chat_gem/install/templates/chat_gem.rb +36 -0
  62. data/lib/generators/turbo_chat/install/install_generator.rb +18 -0
  63. data/lib/generators/turbo_chat/install/templates/turbo_chat.rb +36 -0
  64. data/lib/tasks/chat_gem_tasks.rake +1 -0
  65. data/lib/tasks/turbo_chat_tasks.rake +10 -0
  66. data/lib/turbo_chat/version.rb +5 -0
  67. data/lib/turbo_chat.rb +24 -0
  68. data/turbo_chat.gemspec +31 -0
  69. metadata +155 -0
@@ -0,0 +1,95 @@
1
+ <section class="chat-shell <%= "chat-shell--closed" if @chat.closed? %>"
2
+ data-chat-emit-chat-lifecycle-events="<%= chat_emit_chat_lifecycle_events? %>"
3
+ data-chat-lifecycle-event="<%= json_escape(@chat_lifecycle_event.to_json) if @chat_lifecycle_event.present? %>">
4
+ <% current_participant = current_chat_participant %>
5
+ <% current_participant_type = current_participant.class.base_class.name %>
6
+ <% current_participant_id = current_participant.id %>
7
+ <% self_mention_tokens = chat_self_mention_tokens(chat: @chat, participant: current_participant) %>
8
+ <% self_role_mention_token = chat_self_role_mention_token(chat: @chat, participant: current_participant) %>
9
+ <% mention_container_style = chat_mentions_container_inline_style %>
10
+ <header class="chat-header">
11
+ <div class="chat-header-copy">
12
+ <p class="chat-header-kicker"><%= @chat.closed? ? "Conversation closed" : "Conversation active" %></p>
13
+ <div class="chat-header-title-row">
14
+ <h1><%= @chat.title %></h1>
15
+ <span class="chat-status <%= @chat.closed? ? "chat-status--closed" : "chat-status--open" %>">
16
+ <%= @chat.closed? ? "Closed" : "Open" %>
17
+ </span>
18
+ </div>
19
+ </div>
20
+ <div class="chat-header-actions">
21
+ <% if @chat.opened? && @can_close_chat %>
22
+ <%= button_to "Close",
23
+ close_chat_path(@chat),
24
+ method: :patch,
25
+ form_class: "chat-inline-form",
26
+ class: "chat-btn chat-btn--danger",
27
+ data: { turbo_confirm: "Close this chat?" } %>
28
+ <% elsif @chat.closed? && @can_reopen_chat %>
29
+ <%= button_to "Reopen",
30
+ reopen_chat_path(@chat),
31
+ method: :patch,
32
+ form_class: "chat-inline-form",
33
+ class: "chat-btn chat-btn--success" %>
34
+ <% end %>
35
+
36
+ <%= button_to "Leave",
37
+ leave_chat_path(@chat),
38
+ method: :patch,
39
+ form_class: "chat-inline-form",
40
+ class: "chat-btn chat-btn--danger",
41
+ data: { turbo_confirm: "Leave this chat?" } %>
42
+ <%= link_to "Back", chats_path, class: "chat-btn chat-btn--ghost" %>
43
+ </div>
44
+ </header>
45
+
46
+ <% if @can_invite_member %>
47
+ <div class="chat-invite">
48
+ <% if @invitable_participants.present? %>
49
+ <%= form_with url: chat_chat_memberships_path(@chat), scope: :chat_membership, class: "chat-form chat-form--invite" do |f| %>
50
+ <%= f.hidden_field :participant_type, value: @invite_participant_type %>
51
+ <%= f.select :participant_id,
52
+ options_for_select(@invitable_participants.map { |participant| ["#{chat_participant_name(participant)} (##{participant.id})", participant.id] }),
53
+ { prompt: "Invite participant" },
54
+ required: true %>
55
+ <%= f.submit "Invite", class: "chat-btn" %>
56
+ <% end %>
57
+ <% else %>
58
+ <p class="chat-hint">No eligible participants to invite.</p>
59
+ <% end %>
60
+ </div>
61
+ <% end %>
62
+
63
+ <%= turbo_stream_from @chat, :messages %>
64
+
65
+ <section class="chat-window">
66
+ <div id="<%= dom_id(@chat, :messages) %>"
67
+ class="chat-messages"
68
+ data-chat-id="<%= @chat.id %>"
69
+ data-chat-self-participant-type="<%= current_participant_type %>"
70
+ data-chat-self-participant-id="<%= current_participant_id %>"
71
+ data-chat-self-mention-tokens="<%= json_escape(self_mention_tokens.to_json) %>"
72
+ data-chat-self-role-mention-token="<%= self_role_mention_token %>"
73
+ data-chat-mention-filter-exclude-self="<%= chat_mention_filter_exclude_self? %>"
74
+ data-chat-mention-filter-hide-roles="<%= chat_mention_filter_hide_roles? %>"
75
+ data-chat-emit-mention-events="<%= chat_emit_mention_events? %>"
76
+ data-chat-can-edit-own-messages="<%= @can_edit_own_messages %>"
77
+ <%= %(style="#{mention_container_style}") if mention_container_style.present? %>>
78
+ <%= render @chat_messages %>
79
+ </div>
80
+
81
+ <div id="<%= dom_id(@chat, :signals) %>"
82
+ class="chat-signals"
83
+ data-chat-show-self-signals="<%= ChatGem.configuration.show_self_signals %>"
84
+ data-chat-self-participant-type="<%= current_participant_type %>"
85
+ data-chat-self-participant-id="<%= current_participant_id %>">
86
+ <%= render "chat_gem/chat_messages/signals", chat: @chat %>
87
+ </div>
88
+ </section>
89
+
90
+ <% if @can_post_message %>
91
+ <%= render "chat_gem/chat_messages/form", chat: @chat, chat_message: @chat_message, chat_permission: @chat_permission %>
92
+ <% elsif @chat.closed? %>
93
+ <p class="chat-banner chat-banner--muted">This chat is closed.</p>
94
+ <% end %>
95
+ </section>
@@ -0,0 +1,20 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>ChatGem</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+ <%= action_cable_meta_tag %>
8
+ <% if respond_to?(:turbo_include_tags) %>
9
+ <%= turbo_include_tags %>
10
+ <% end %>
11
+ <%= stylesheet_link_tag "chat_gem/application", "data-turbo-track": "reload" %>
12
+ <%= javascript_include_tag "chat_gem/shared", "data-turbo-track": "reload", defer: true %>
13
+ <%= javascript_include_tag "chat_gem/messages", "data-turbo-track": "reload", defer: true %>
14
+ <%= javascript_include_tag "chat_gem/realtime", "data-turbo-track": "reload", defer: true %>
15
+ <%= javascript_include_tag "chat_gem/lifecycle_events", "data-turbo-track": "reload", defer: true %>
16
+ </head>
17
+ <body>
18
+ <%= yield %>
19
+ </body>
20
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,16 @@
1
+ ChatGem::Engine.routes.draw do
2
+ root to: "chats#index"
3
+
4
+ resources :chats, only: %i[index show new create] do
5
+ member do
6
+ patch :accept
7
+ patch :decline
8
+ patch :leave
9
+ patch :close
10
+ patch :reopen
11
+ end
12
+
13
+ resources :chat_memberships, only: :create
14
+ resources :chat_messages, only: %i[index create update]
15
+ end
16
+ end
@@ -0,0 +1,8 @@
1
+ class CreateChatGemChats < ActiveRecord::Migration[7.0]
2
+ def change
3
+ create_table :chat_gem_chats do |t|
4
+ t.string :title, null: false
5
+ t.timestamps
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,19 @@
1
+ class CreateChatGemChatMemberships < ActiveRecord::Migration[7.0]
2
+ def change
3
+ create_table :chat_gem_chat_memberships do |t|
4
+ t.references :chat, null: false, foreign_key: { to_table: :chat_gem_chats }
5
+ t.references :participant, polymorphic: true, null: false
6
+ t.integer :role, null: false, default: 0
7
+ t.boolean :muted, null: false, default: false
8
+ t.datetime :timed_out_until
9
+ t.datetime :removed_at
10
+ t.timestamps
11
+ end
12
+
13
+ add_index :chat_gem_chat_memberships,
14
+ %i[chat_id participant_type participant_id],
15
+ unique: true,
16
+ where: "removed_at IS NULL",
17
+ name: "index_chat_gem_memberships_on_chat_participant_active"
18
+ end
19
+ end
@@ -0,0 +1,14 @@
1
+ class CreateChatGemChatMessages < ActiveRecord::Migration[7.0]
2
+ def change
3
+ create_table :chat_gem_chat_messages do |t|
4
+ t.references :chat, null: false, foreign_key: { to_table: :chat_gem_chats }
5
+ t.references :participant, polymorphic: true, null: false
6
+ t.text :body
7
+ t.integer :kind, null: false, default: 0
8
+ t.integer :signal_type
9
+ t.timestamps
10
+ end
11
+
12
+ add_index :chat_gem_chat_messages, %i[chat_id created_at id], name: "index_chat_gem_messages_order"
13
+ end
14
+ end
@@ -0,0 +1,6 @@
1
+ class AddClosedAtToChatGemChats < ActiveRecord::Migration[7.0]
2
+ def change
3
+ add_column :chat_gem_chats, :closed_at, :datetime
4
+ add_index :chat_gem_chats, :closed_at
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ class AddCustomRoleKeyToChatMemberships < ActiveRecord::Migration[7.0]
2
+ def change
3
+ add_column :chat_gem_chat_memberships, :custom_role_key, :string
4
+ add_index :chat_gem_chat_memberships, :custom_role_key
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ class AddInvitationAcceptedToChatGemChatMemberships < ActiveRecord::Migration[7.0]
2
+ def change
3
+ add_column :chat_gem_chat_memberships, :invitation_accepted, :boolean, null: false, default: true
4
+ end
5
+ end
@@ -0,0 +1,242 @@
1
+ module ChatGem
2
+ class Configuration
3
+ DEFAULT_ROLE_DEFINITIONS = {
4
+ "member" => {
5
+ name: "Member",
6
+ rank: 0,
7
+ permissions: %i[view_chat post_message mention_member]
8
+ },
9
+ "moderator" => {
10
+ name: "Moderator",
11
+ rank: 1,
12
+ permissions: %i[view_chat post_message mention_member mention_all mention_role invite_member mute_member timeout_member ban_member delete_message]
13
+ },
14
+ "admin" => {
15
+ name: "Admin",
16
+ rank: 2,
17
+ permissions: %i[view_chat post_message mention_member mention_all mention_role invite_member mute_member timeout_member ban_member delete_message close_chat reopen_chat]
18
+ }
19
+ }.freeze
20
+ DEFAULT_MESSAGE_HTML_TAGS = %w[a b br code em i li ol p pre strong ul].freeze
21
+ DEFAULT_MESSAGE_HTML_ATTRIBUTES = %w[href target rel class].freeze
22
+ DEFAULT_EMOJI_ALIASES = {
23
+ "smile" => "😄",
24
+ "grin" => "😁",
25
+ "laughing" => "😆",
26
+ "blush" => "😊",
27
+ "wink" => "😉",
28
+ "heart" => "❤️",
29
+ "thumbsup" => "👍",
30
+ "+1" => "👍",
31
+ "thumbsdown" => "👎",
32
+ "-1" => "👎",
33
+ "fire" => "🔥",
34
+ "rocket" => "🚀",
35
+ "thinking" => "🤔",
36
+ "tada" => "🎉",
37
+ "wave" => "👋",
38
+ "eyes" => "👀"
39
+ }.freeze
40
+ DEFAULT_BLOCKED_WORDS_SCRAMBLE_CHARS = %w[# * / ( = ) ! 8].freeze
41
+ DEFAULT_BLOCKED_WORDS_ACTION = "reject".freeze
42
+
43
+ attr_accessor :permission_adapter,
44
+ :max_chat_participants,
45
+ :max_message_length,
46
+ :message_history_limit,
47
+ :enable_mentions,
48
+ :mention_filter_exclude_self,
49
+ :mention_filter_hide_roles,
50
+ :enable_emoji_aliases,
51
+ :emoji_aliases,
52
+ :blocked_words,
53
+ :blocked_words_action,
54
+ :blocked_words_scramble_chars,
55
+ :mention_mark_hex_color,
56
+ :mention_highlight_hex_color,
57
+ :own_message_hex_color,
58
+ :other_message_hex_color,
59
+ :role_message_hex_colors,
60
+ :show_timestamp,
61
+ :show_role,
62
+ :active_chat_window,
63
+ :emit_typing_events,
64
+ :emit_message_events,
65
+ :emit_mention_events,
66
+ :emit_invitation_events,
67
+ :emit_chat_lifecycle_events,
68
+ :emit_moderation_events,
69
+ :emit_blocked_words_events,
70
+ :show_self_signals,
71
+ :replace_signals_on_message_submit,
72
+ :message_css_class_resolver,
73
+ :render_message_html,
74
+ :message_html_tags,
75
+ :message_html_attributes,
76
+ :timestamp_formatter,
77
+ :role_formatter
78
+
79
+ def initialize
80
+ @permission_adapter = ChatGem::Permission
81
+ @max_chat_participants = 10
82
+ @max_message_length = 1000
83
+ @message_history_limit = 200
84
+ @enable_mentions = true
85
+ @mention_filter_exclude_self = true
86
+ @mention_filter_hide_roles = true
87
+ @enable_emoji_aliases = true
88
+ @emoji_aliases = DEFAULT_EMOJI_ALIASES.dup
89
+ @blocked_words = []
90
+ @blocked_words_action = DEFAULT_BLOCKED_WORDS_ACTION
91
+ @blocked_words_scramble_chars = DEFAULT_BLOCKED_WORDS_SCRAMBLE_CHARS.dup
92
+ @mention_mark_hex_color = nil
93
+ @mention_highlight_hex_color = nil
94
+ @own_message_hex_color = nil
95
+ @other_message_hex_color = nil
96
+ @role_message_hex_colors = {}
97
+ @show_timestamp = true
98
+ @show_role = false
99
+ @active_chat_window = 5.minutes
100
+ @emit_typing_events = false
101
+ @emit_message_events = false
102
+ @emit_mention_events = false
103
+ @emit_invitation_events = false
104
+ @emit_chat_lifecycle_events = false
105
+ @emit_moderation_events = false
106
+ @emit_blocked_words_events = false
107
+ @show_self_signals = false
108
+ @replace_signals_on_message_submit = false
109
+ @message_css_class_resolver = nil
110
+ @render_message_html = false
111
+ @message_html_tags = DEFAULT_MESSAGE_HTML_TAGS.dup
112
+ @message_html_attributes = DEFAULT_MESSAGE_HTML_ATTRIBUTES.dup
113
+ @additional_roles = {}
114
+ @timestamp_formatter = lambda { |timestamp, _chat_message = nil|
115
+ I18n.l(timestamp.in_time_zone, format: :long)
116
+ }
117
+ @role_formatter = lambda { |role, _chat_message = nil|
118
+ role.to_s.humanize
119
+ }
120
+ end
121
+
122
+ def add_role(key, name:, permissions:, rank: 0)
123
+ normalized_key = normalize_role_key(key)
124
+ raise ArgumentError, "Role key cannot be blank" if normalized_key.blank?
125
+ raise ArgumentError, "Role #{normalized_key} is reserved" if DEFAULT_ROLE_DEFINITIONS.key?(normalized_key)
126
+
127
+ role_name = name.to_s.strip
128
+ raise ArgumentError, "Role name cannot be blank" if role_name.blank?
129
+
130
+ @additional_roles[normalized_key] = {
131
+ name: role_name,
132
+ rank: rank.to_i,
133
+ permissions: Array(permissions).map { |permission| permission.to_sym }.uniq
134
+ }
135
+ end
136
+
137
+ def remove_role(key)
138
+ @additional_roles.delete(normalize_role_key(key))
139
+ end
140
+
141
+ def clear_additional_roles!
142
+ @additional_roles = {}
143
+ end
144
+
145
+ def additional_roles
146
+ @additional_roles.dup
147
+ end
148
+
149
+ def role_definitions
150
+ DEFAULT_ROLE_DEFINITIONS.merge(@additional_roles)
151
+ end
152
+
153
+ def role_definition(key)
154
+ role_definitions[normalize_role_key(key)]
155
+ end
156
+
157
+ def add_emoji_alias(name, value)
158
+ key = normalize_emoji_alias_key(name)
159
+ raise ArgumentError, "Emoji alias cannot be blank" if key.blank?
160
+
161
+ normalized_value = value.to_s.strip
162
+ raise ArgumentError, "Emoji alias value cannot be blank" if normalized_value.blank?
163
+
164
+ @emoji_aliases = effective_emoji_aliases.merge(key => normalized_value)
165
+ end
166
+
167
+ def remove_emoji_alias(name)
168
+ key = normalize_emoji_alias_key(name)
169
+ return if key.blank?
170
+
171
+ @emoji_aliases = effective_emoji_aliases.except(key)
172
+ end
173
+
174
+ def clear_emoji_aliases!
175
+ @emoji_aliases = {}
176
+ end
177
+
178
+ def reset_emoji_aliases!
179
+ @emoji_aliases = DEFAULT_EMOJI_ALIASES.dup
180
+ end
181
+
182
+ def effective_emoji_aliases
183
+ source = emoji_aliases.is_a?(Hash) ? emoji_aliases : {}
184
+
185
+ source.each_with_object({}) do |(key, value), aliases|
186
+ normalized_key = normalize_emoji_alias_key(key)
187
+ normalized_value = value.to_s.strip
188
+ next if normalized_key.blank? || normalized_value.blank?
189
+
190
+ aliases[normalized_key] = normalized_value
191
+ end
192
+ end
193
+
194
+ def effective_blocked_words
195
+ source = blocked_words
196
+ return [] unless source.respond_to?(:each)
197
+
198
+ source.each_with_object([]) do |value, words|
199
+ normalized_word = normalize_blocked_word(value)
200
+ next if normalized_word.blank?
201
+ next if words.include?(normalized_word)
202
+
203
+ words << normalized_word
204
+ end
205
+ end
206
+
207
+ def effective_blocked_words_action
208
+ action = blocked_words_action.to_s.strip.downcase
209
+ return DEFAULT_BLOCKED_WORDS_ACTION if action.blank?
210
+
211
+ %w[reject scramble].include?(action) ? action : DEFAULT_BLOCKED_WORDS_ACTION
212
+ end
213
+
214
+ def effective_blocked_words_scramble_chars
215
+ source = blocked_words_scramble_chars
216
+ return DEFAULT_BLOCKED_WORDS_SCRAMBLE_CHARS.dup unless source.respond_to?(:each)
217
+
218
+ chars = source.each_with_object([]) do |value, result|
219
+ normalized_char = value.to_s
220
+ next if normalized_char.blank?
221
+
222
+ result << normalized_char
223
+ end
224
+
225
+ chars.presence || DEFAULT_BLOCKED_WORDS_SCRAMBLE_CHARS.dup
226
+ end
227
+
228
+ private
229
+
230
+ def normalize_role_key(key)
231
+ key.to_s.strip
232
+ end
233
+
234
+ def normalize_emoji_alias_key(key)
235
+ key.to_s.strip.downcase
236
+ end
237
+
238
+ def normalize_blocked_word(word)
239
+ word.to_s.strip.downcase
240
+ end
241
+ end
242
+ end
@@ -0,0 +1,29 @@
1
+ module ChatGem
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace ChatGem
4
+
5
+ initializer "chat_gem.helpers" do
6
+ ActiveSupport.on_load(:action_controller_base) do
7
+ helper ChatGem::ApplicationHelper
8
+ end
9
+
10
+ ActiveSupport.on_load(:action_view) do
11
+ include ChatGem::ApplicationHelper
12
+ end
13
+ end
14
+
15
+ initializer "chat_gem.assets.precompile" do |app|
16
+ next unless app.config.respond_to?(:assets)
17
+ next unless app.config.assets.respond_to?(:precompile)
18
+
19
+ app.config.assets.precompile += %w[
20
+ chat_gem/application.css
21
+ chat_gem/application.js
22
+ chat_gem/shared.js
23
+ chat_gem/messages.js
24
+ chat_gem/realtime.js
25
+ chat_gem/lifecycle_events.js
26
+ ]
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,45 @@
1
+ module ChatGem
2
+ module ModelExtensions
3
+ module ChatParticipant
4
+ module ClassMethods
5
+ def acts_as_chat_participant
6
+ return if reflect_on_association(:chat_memberships).present?
7
+
8
+ has_many :chat_memberships,
9
+ -> { order(created_at: :asc, id: :asc) },
10
+ as: :participant,
11
+ class_name: "ChatGem::ChatMembership",
12
+ dependent: :destroy
13
+
14
+ has_many :chat_messages,
15
+ -> { order(created_at: :asc, id: :asc) },
16
+ as: :participant,
17
+ class_name: "ChatGem::ChatMessage",
18
+ dependent: :destroy
19
+
20
+ has_many :chats,
21
+ -> { distinct },
22
+ through: :chat_memberships,
23
+ source: :chat,
24
+ class_name: "ChatGem::Chat"
25
+
26
+ include ChatGem::ModelExtensions::ChatParticipant::InstanceMethods
27
+ end
28
+ end
29
+
30
+ module InstanceMethods
31
+ def active_chat_memberships
32
+ chat_memberships.active
33
+ end
34
+
35
+ def joined_chat?(chat)
36
+ active_chat_memberships.exists?(chat_id: chat.id)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ ActiveSupport.on_load(:active_record) do
44
+ extend ChatGem::ModelExtensions::ChatParticipant::ClassMethods
45
+ end