Module: Chats

Defined in:
lib/chats.rb,
lib/chats/engine.rb,
lib/chats/errors.rb,
lib/chats/macros.rb,
lib/chats/version.rb,
lib/chats/broadcasts.rb,
lib/chats/configuration.rb,
lib/chats/models/message.rb,
lib/chats/models/reaction.rb,
lib/chats/models/participant.rb,
lib/chats/models/conversation.rb,
app/helpers/chats/engine_helper.rb,
lib/chats/models/concerns/messager.rb,
lib/chats/models/application_record.rb,
lib/generators/chats/views_generator.rb,
lib/chats/models/concerns/chat_subject.rb,
lib/generators/chats/install_generator.rb,
app/controllers/chats/messages_controller.rb,
app/controllers/chats/reactions_controller.rb,
app/controllers/chats/application_controller.rb,
app/controllers/chats/conversations_controller.rb

Overview

Chats

A drop-in, real-time messaging engine for Rails: DMs, group chats, reactions, attachments, read receipts — Hotwire-native, polymorphic, adapter-driven.

The public surface is intentionally tiny:

Chats.configure { |config| ... }   # one block, in an initializer
acts_as_messager                   # on any model that can converse
acts_as_chat_subject               # on any model conversations can be about

user.chat_with(other)              # find-or-create a direct conversation
user.message!(other, "hello!")     # ...and say something in one line

Everything else (controllers, views, broadcasts) ships with the engine and is overridable the Devise way (‘rails g chats:views`).

Defined Under Namespace

Modules: Broadcasts, ChatSubject, EngineHelper, Generators, Macros, Messager Classes: ApplicationController, ApplicationRecord, BlockedError, Configuration, ConfigurationError, Conversation, ConversationsController, Engine, Error, Message, MessagesController, NotAllowedError, Participant, Reaction, ReactionsController

Constant Summary collapse

VERSION =
"0.1.1"

Class Method Summary collapse

Class Method Details

.avatar_for(messager) ⇒ Object



153
154
155
156
157
# File 'lib/chats.rb', line 153

def avatar_for(messager)
  return nil if messager.nil?

  config.messager_avatar.call(messager)
end

.blocked_between?(a, b) ⇒ Boolean

True when a and b can’t message each other (either one blocked the other — blocking is enforced bidirectionally, like every serious messaging product). Only meaningful between messagers of the same class (a User blocks a User); cross-class pairs are never considered blocked.

Returns:

  • (Boolean)


104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/chats.rb', line 104

def blocked_between?(a, b)
  return false if a.nil? || b.nil?
  return false unless a.class.base_class == b.class.base_class

  blocked_ids = blocked_ids_for(a)
  if blocked_ids.respond_to?(:exists?)
    # An AR relation: resolve with one indexed query instead of loading ids.
    blocked_ids.exists?(b.id)
  else
    blocked_ids.include?(b.id)
  end
end

.blocked_ids_for(messager) ⇒ Object

The single source of truth for “who can’t talk to whom”. Wraps the host-provided ‘config.blocked_messager_ids` proc (no-op by default, or `Moderate.blocked_ids_for(user)` when the moderate gem is wired in) and always returns something usable in a `WHERE id IN (…)` — an Array of ids or an AR relation selecting ids.



94
95
96
97
98
# File 'lib/chats.rb', line 94

def blocked_ids_for(messager)
  return [] if messager.nil?

  config.blocked_messager_ids.call(messager) || []
end

.can_message?(sender, recipient) ⇒ Boolean

Host policy on top of (never instead of) block enforcement. The blocked check is hardcoded in the models so a host overriding ‘can_message` cannot accidentally disable Trust & Safety guarantees.

Returns:

  • (Boolean)


120
121
122
123
124
# File 'lib/chats.rb', line 120

def can_message?(sender, recipient)
  return false if blocked_between?(sender, recipient)

  config.can_message.call(sender, recipient)
end

.chat_subject_class?(klass) ⇒ Boolean

Returns:

  • (Boolean)


83
84
85
# File 'lib/chats.rb', line 83

def chat_subject_class?(klass)
  registered_class?(subject_class_names, klass)
end

.configObject Also known as: configuration

— Configuration ——————————————————–



33
34
35
# File 'lib/chats.rb', line 33

def config
  @config ||= Configuration.new
end

.configure {|config| ... } ⇒ Object

Yields:



39
40
41
42
43
# File 'lib/chats.rb', line 39

def configure
  yield config if block_given?
  config.validate!
  config
end

.display_name_for(messager) ⇒ Object

— Display helpers (used by the bundled views) ————————–



147
148
149
150
151
# File 'lib/chats.rb', line 147

def display_name_for(messager)
  return "" if messager.nil?

  config.messager_display_name.call(messager).to_s
end

.loggerObject

— Internals ————————————————————



161
162
163
# File 'lib/chats.rb', line 161

def logger
  defined?(::Rails) ? ::Rails.logger : nil
end

.messager_class?(klass) ⇒ Boolean

Whether klass (a Class or class name) is a registered messager. Ancestor-aware so an STI subclass of a messager is accepted too.

Returns:

  • (Boolean)


79
80
81
# File 'lib/chats.rb', line 79

def messager_class?(klass)
  registered_class?(messager_class_names, klass)
end

.messager_class_namesObject



69
70
71
# File 'lib/chats.rb', line 69

def messager_class_names
  @messager_class_names ||= Set.new
end

.messager_key(messager) ⇒ Object

A stable, URL-safe, non-guessy key for a messager, used in DOM data attributes (the Stimulus thread controller compares it to decide own-vs-other bubble alignment) and in direct-conversation keys. GlobalID params are opaque-ish (Base64) and already encode class + id.



169
170
171
# File 'lib/chats.rb', line 169

def messager_key(messager)
  messager.to_global_id.to_param
end

.notify(event, **payload) ⇒ Object

Fire a domain event through the host’s notifier hook (no-op by default). Events (see Chats::Configuration#notifier):

:message_created      message:      (every persisted, non-system message)
:participant_added    participant:  (someone added to a group)

Hosts typically point this at a Noticed notifier or a mailer job:

config.notifier = ->(event, **payload) {
  NewMessageNotifier.with(**payload).deliver if event == :message_created
}


135
136
137
138
139
140
141
142
143
# File 'lib/chats.rb', line 135

def notify(event, **payload)
  config.notifier.call(event, **payload)
rescue StandardError => e
  # A broken notifier must never break message delivery itself — the
  # message is already committed; notifications are best-effort fan-out.
  # Same error-isolation philosophy as pricing_plans' lifecycle callbacks.
  logger&.error("[chats] notifier raised on #{event}: #{e.class}: #{e.message}")
  nil
end

.register_chat_subject(klass) ⇒ Object



65
66
67
# File 'lib/chats.rb', line 65

def register_chat_subject(klass)
  subject_class_names << klass.name if klass.name
end

.register_messager(klass) ⇒ Object

— Registries ———————————————————–

‘acts_as_messager` / `acts_as_chat_subject` self-register the calling class here. We store class NAMES (strings), not Class objects, so the registry survives Zeitwerk code reloading in development (a reloaded class is a brand-new object; its name is stable).



61
62
63
# File 'lib/chats.rb', line 61

def register_messager(klass)
  messager_class_names << klass.name if klass.name
end

.reset!Object

Reset all global state. Used by the test suite to keep examples isolated; also handy in a console when experimenting with configuration.



47
48
49
50
51
52
# File 'lib/chats.rb', line 47

def reset!
  @config = Configuration.new
  @messager_classes = nil
  @subject_classes = nil
  self
end

.subject_class_namesObject



73
74
75
# File 'lib/chats.rb', line 73

def subject_class_names
  @subject_class_names ||= Set.new
end