Class: Chats::Message
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- Chats::Message
- Defined in:
- lib/chats/models/message.rb
Overview
A message in a conversation. Two kinds:
"text" a human message from a +sender+ (any acts_as_messager model)
"system" posted by the HOST APP via Conversation#post_system_message!
("Your ride was cancelled") — no sender, centered rendering
Soft deletion (“delete for everyone”)
With ‘config.deletion = :soft` (the default), deleting clears the body, purges attachments and stamps deleted_at — the row stays as a tombstone (“Message deleted”) exactly like WhatsApp/Telegram, which keeps thread continuity AND leaves an auditable trail for Trust & Safety. Note that moderation evidence is still safe: the moderate gem snapshots reported content AT REPORT TIME, so removing the body later never destroys the evidence a moderator needs.
Real-time
All Turbo Stream fan-out lives in Chats::Broadcasts (one place to read the whole live behavior). Broadcasts run in ‘after_*_commit` callbacks —never inside the transaction — and use the `_later` job variants so rendering never blocks the request.
Constant Summary collapse
- KINDS =
%w[text system].freeze
Instance Method Summary collapse
- #attachments? ⇒ Boolean
- #deleted? ⇒ Boolean
-
#edit!(new_body) ⇒ Object
Edit the body (sender-only — controllers enforce who; the model enforces that editing is enabled).
- #edited? ⇒ Boolean
- #moderation_content_type ⇒ Object
- #moderation_field_changed_for_commit?(field) ⇒ Boolean
-
#moderation_field_value(field) ⇒ Object
Moderate::ContentFilterable seam: when the host declares ‘moderates :files, mode: :flag, with: :some_image_adapter`, the default `public_send(:files)` would hand the adapter an ActiveStorage proxy.
- #moderation_label ⇒ Object
- #moderation_snapshot(field) ⇒ Object
-
#read_by?(messager) ⇒ Boolean
Whether
messagerhas read this message, derived from their read horizon (see Chats::Participant for why there’s no receipts table). - #removable_reported_field?(field) ⇒ Boolean
-
#remove_reported_field!(field) ⇒ Object
A moderator removing a reported message body = the soft-delete tombstone path, so the thread shows “Message deleted” instead of a hole, and attachments are purged with it.
-
#report_visible_to?(viewer, field: nil) ⇒ Boolean
Only people in the conversation may report a message (a message isn’t public content), and you can’t report your own.
-
#reported_owner ⇒ Object
— Moderation contract (duck-typed, zero coupling) ————————.
- #sender_key ⇒ Object
- #sent_by?(messager) ⇒ Boolean
-
#soft_delete! ⇒ Object
Delete according to ‘config.deletion` (see class comment).
- #system? ⇒ Boolean
- #text? ⇒ Boolean
-
#visible_body ⇒ Object
The body as the UI should show it (tombstones render a localized “Message deleted” placeholder straight from the view, not from here — this just guards against showing stale bodies by accident).
Instance Method Details
#attachments? ⇒ Boolean
157 158 159 |
# File 'lib/chats/models/message.rb', line 157 def respond_to?(:files) && files.attached? end |
#deleted? ⇒ Boolean
104 |
# File 'lib/chats/models/message.rb', line 104 def deleted? = deleted_at.present? |
#edit!(new_body) ⇒ Object
Edit the body (sender-only — controllers enforce who; the model enforces that editing is enabled). Stamps edited_at so the UI can show “edited”, and broadcasts the replacement bubble.
132 133 134 135 136 137 |
# File 'lib/chats/models/message.rb', line 132 def edit!(new_body) raise Chats::NotAllowedError, "editing is disabled" unless Chats.config.editing raise Chats::NotAllowedError, "can't edit a deleted message" if deleted? update!(body: new_body, edited_at: Time.current) end |
#edited? ⇒ Boolean
105 |
# File 'lib/chats/models/message.rb', line 105 def edited? = edited_at.present? |
#moderation_content_type ⇒ Object
203 204 205 |
# File 'lib/chats/models/message.rb', line 203 def moderation_content_type "message" end |
#moderation_field_changed_for_commit?(field) ⇒ Boolean
220 221 222 223 224 225 226 227 228 229 230 |
# File 'lib/chats/models/message.rb', line 220 def moderation_field_changed_for_commit?(field) if field.to_s == "files" && respond_to?(:files) # Attachments don't have a column; detect "files changed in this # commit" via the attachment records created in this transaction. files..any?(&:previously_new_record?) elsif respond_to?(:saved_change_to_attribute?) saved_change_to_attribute?(field) else true end end |
#moderation_field_value(field) ⇒ Object
Moderate::ContentFilterable seam: when the host declares ‘moderates :files, mode: :flag, with: :some_image_adapter`, the default `public_send(:files)` would hand the adapter an ActiveStorage proxy. Most image adapters (like a custo ImageReviewAdapter, or an AWS Rekognition adapter fed via ClassifyJob) don’t read the value anyway —they re-fetch the blob — but we make the value meaningful and change-detection correct regardless.
214 215 216 217 218 |
# File 'lib/chats/models/message.rb', line 214 def moderation_field_value(field) return files if field.to_s == "files" && respond_to?(:files) public_send(field) end |
#moderation_label ⇒ Object
174 175 176 |
# File 'lib/chats/models/message.rb', line 174 def moderation_label "Chat message #{id}" end |
#moderation_snapshot(field) ⇒ Object
178 179 180 |
# File 'lib/chats/models/message.rb', line 178 def moderation_snapshot(field) body if field.to_s == "body" end |
#read_by?(messager) ⇒ Boolean
Whether messager has read this message, derived from their read horizon (see Chats::Participant for why there’s no receipts table).
124 125 126 127 |
# File 'lib/chats/models/message.rb', line 124 def read_by?() participant = conversation.participant_for() participant&.last_read_at.present? && participant.last_read_at >= created_at end |
#removable_reported_field?(field) ⇒ Boolean
182 183 184 |
# File 'lib/chats/models/message.rb', line 182 def removable_reported_field?(field) field.to_s == "body" && body.present? && !deleted? end |
#remove_reported_field!(field) ⇒ Object
A moderator removing a reported message body = the soft-delete tombstone path, so the thread shows “Message deleted” instead of a hole, and attachments are purged with it.
189 190 191 192 193 |
# File 'lib/chats/models/message.rb', line 189 def remove_reported_field!(field) return false unless field.to_s == "body" soft_delete! end |
#report_visible_to?(viewer, field: nil) ⇒ Boolean
Only people in the conversation may report a message (a message isn’t public content), and you can’t report your own.
197 198 199 200 201 |
# File 'lib/chats/models/message.rb', line 197 def report_visible_to?(viewer, field: nil) return false if viewer.nil? || sent_by?(viewer) conversation.participant?(viewer) end |
#reported_owner ⇒ Object
— Moderation contract (duck-typed, zero coupling) ————————
Plain-Ruby methods that make a message a first-class citizen of the moderate gem the moment the HOST wires it up (see README “Trust & Safety”): they satisfy Moderate::Reportable’s contract (owner, label, snapshot, removal, visibility) and Moderate::ContentFilterable’s field seams for attachment filtering. Without moderate installed they’re inert and cost nothing.
170 171 172 |
# File 'lib/chats/models/message.rb', line 170 def reported_owner sender end |
#sender_key ⇒ Object
107 108 109 |
# File 'lib/chats/models/message.rb', line 107 def sender_key sender && Chats.(sender) end |
#sent_by?(messager) ⇒ Boolean
111 112 113 |
# File 'lib/chats/models/message.rb', line 111 def sent_by?() sender.present? && sender == end |
#soft_delete! ⇒ Object
Delete according to ‘config.deletion` (see class comment). Returns false when deletion is disabled.
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
# File 'lib/chats/models/message.rb', line 141 def soft_delete! case Chats.config.deletion when :soft transaction do files.purge_later if respond_to?(:files) && files.attached? update!(body: nil, deleted_at: Time.current) end true when :hard destroy! true else false end end |
#system? ⇒ Boolean
102 |
# File 'lib/chats/models/message.rb', line 102 def system? = kind == "system" |
#text? ⇒ Boolean
103 |
# File 'lib/chats/models/message.rb', line 103 def text? = kind == "text" |
#visible_body ⇒ Object
The body as the UI should show it (tombstones render a localized “Message deleted” placeholder straight from the view, not from here —this just guards against showing stale bodies by accident).
118 119 120 |
# File 'lib/chats/models/message.rb', line 118 def visible_body deleted? ? nil : body end |