Class: Livechat::Conversation
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- Livechat::Conversation
- Defined in:
- app/models/livechat/conversation.rb
Overview
One thread per visitor — like a phone line, not a ticket queue. A visitor writing into a resolved conversation reopens it; history stays in place. Visitor identity is loose (no foreign key to the host's user table): signed-in visitors are keyed by visitor_id, guests by a cookie token.
Constant Summary collapse
- STATUSES =
%w[open resolved].freeze
- TYPING_TTL =
8.seconds
Class Method Summary collapse
-
.claim!(visitor_token:, visitor_id:, visitor_label: nil) ⇒ Object
A guest who signs in keeps their conversation: threads started under the cookie token are adopted by the user, once, permanently.
-
.for_visitor(visitor_id: nil, visitor_token: nil) ⇒ Object
The visitor's ongoing thread.
-
.purge_resolved(older_than:) ⇒ Object
Host-controlled retention.
Instance Method Summary collapse
- #clear_typing!(author_type) ⇒ Object
- #display_name ⇒ Object
- #mark_read_for_agent! ⇒ Object
- #mark_read_for_visitor! ⇒ Object
- #post_agent_message!(body:, agent_id:, agent_label:, files: nil) ⇒ Object
- #post_visitor_message!(body, files: nil) ⇒ Object
-
#register_message(message) ⇒ Object
Denormalized inbox-list fields, refreshed on every message.
- #reopen_by!(agent_label) ⇒ Object
- #resolve_by!(agent_label) ⇒ Object
- #typing!(author_type) ⇒ Object
- #typing?(author_type) ⇒ Boolean
- #unread_from_visitor_count ⇒ Object
Class Method Details
.claim!(visitor_token:, visitor_id:, visitor_label: nil) ⇒ Object
A guest who signs in keeps their conversation: threads started under the cookie token are adopted by the user, once, permanently.
53 54 55 56 57 58 |
# File 'app/models/livechat/conversation.rb', line 53 def self.claim!(visitor_token:, visitor_id:, visitor_label: nil) return if visitor_token.blank? || visitor_id.blank? where(visitor_token: visitor_token, visitor_id: nil) .update_all(visitor_id: visitor_id.to_s, visitor_label: visitor_label) end |
.for_visitor(visitor_id: nil, visitor_token: nil) ⇒ Object
The visitor's ongoing thread. Signed-in identity wins; guests are found by their cookie token (only unclaimed threads — a token that has been adopted by a user belongs to that user now).
43 44 45 46 47 48 49 |
# File 'app/models/livechat/conversation.rb', line 43 def self.for_visitor(visitor_id: nil, visitor_token: nil) if visitor_id.present? where(visitor_id: visitor_id.to_s).order(id: :desc).first elsif visitor_token.present? where(visitor_token: visitor_token, visitor_id: nil).order(id: :desc).first end end |
.purge_resolved(older_than:) ⇒ Object
Host-controlled retention. Only resolved threads whose last activity is older than the supplied cutoff are removed; open conversations are never deleted automatically. Destroying (rather than delete_all) lets Rails run the message and Active Storage attachment lifecycle. Returns the number of conversations destroyed so a scheduled job can report what it did.
27 28 29 30 31 32 33 34 |
# File 'app/models/livechat/conversation.rb', line 27 def self.purge_resolved(older_than:) destroyed = 0 where(status: 'resolved', last_activity_at: ...older_than).find_each do |conversation| conversation.destroy! destroyed += 1 end destroyed end |
Instance Method Details
#clear_typing!(author_type) ⇒ Object
128 129 130 |
# File 'app/models/livechat/conversation.rb', line 128 def clear_typing!() Rails.cache.delete(typing_cache_key()) end |
#display_name ⇒ Object
116 117 118 |
# File 'app/models/livechat/conversation.rb', line 116 def display_name visitor_label.presence || visitor_email.presence || "Visitor ##{id}" end |
#mark_read_for_agent! ⇒ Object
108 109 110 |
# File 'app/models/livechat/conversation.rb', line 108 def mark_read_for_agent! .from_visitor.unread.update_all(read_at: Time.current) end |
#mark_read_for_visitor! ⇒ Object
112 113 114 |
# File 'app/models/livechat/conversation.rb', line 112 def mark_read_for_visitor! .from_agent.unread.update_all(read_at: Time.current) end |
#post_agent_message!(body:, agent_id:, agent_label:, files: nil) ⇒ Object
71 72 73 74 75 76 77 78 |
# File 'app/models/livechat/conversation.rb', line 71 def (body:, agent_id:, agent_label:, files: nil) = .new(author_type: 'agent', body: body, agent_id: agent_id.to_s, agent_label: agent_label) attach(, files) .save! clear_typing!('agent') end |
#post_visitor_message!(body, files: nil) ⇒ Object
60 61 62 63 64 65 66 67 68 69 |
# File 'app/models/livechat/conversation.rb', line 60 def (body, files: nil) transaction do update!(status: 'open') if resolved? = .new(author_type: 'visitor', body: body) attach(, files) .save! clear_typing!('visitor') end end |
#register_message(message) ⇒ Object
Denormalized inbox-list fields, refreshed on every message. Written with update_columns: cheap, no callbacks, no updated_at churn.
100 101 102 103 104 |
# File 'app/models/livechat/conversation.rb', line 100 def () updates = { last_activity_at: .created_at } updates[:last_message_preview] = .preview unless .system? update_columns(updates) end |
#reopen_by!(agent_label) ⇒ Object
89 90 91 92 93 94 95 96 |
# File 'app/models/livechat/conversation.rb', line 89 def reopen_by!(agent_label) return if open? transaction do update!(status: 'open') .create!(author_type: 'system', event: 'reopened', agent_label: agent_label) end end |
#resolve_by!(agent_label) ⇒ Object
80 81 82 83 84 85 86 87 |
# File 'app/models/livechat/conversation.rb', line 80 def resolve_by!(agent_label) return if resolved? transaction do update!(status: 'resolved') .create!(author_type: 'system', event: 'resolved', agent_label: agent_label) end end |
#typing!(author_type) ⇒ Object
120 121 122 |
# File 'app/models/livechat/conversation.rb', line 120 def typing!() Rails.cache.write(typing_cache_key(), true, expires_in: TYPING_TTL) end |
#typing?(author_type) ⇒ Boolean
124 125 126 |
# File 'app/models/livechat/conversation.rb', line 124 def typing?() Rails.cache.exist?(typing_cache_key()) end |
#unread_from_visitor_count ⇒ Object
106 |
# File 'app/models/livechat/conversation.rb', line 106 def unread_from_visitor_count = .from_visitor.unread.count |