Class: Livechat::Conversation

Inherits:
ApplicationRecord show all
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

Instance Method Summary collapse

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!(author_type)
  Rails.cache.delete(typing_cache_key(author_type))
end

#display_nameObject



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!
  messages.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!
  messages.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 post_agent_message!(body:, agent_id:, agent_label:, files: nil)
  message = messages.new(author_type: 'agent', body: body,
                         agent_id: agent_id.to_s, agent_label: agent_label)
  attach(message, files)
  message.save!
  clear_typing!('agent')
  message
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 post_visitor_message!(body, files: nil)
  transaction do
    update!(status: 'open') if resolved?
    message = messages.new(author_type: 'visitor', body: body)
    attach(message, files)
    message.save!
    clear_typing!('visitor')
    message
  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 register_message(message)
  updates = { last_activity_at: message.created_at }
  updates[:last_message_preview] = message.preview unless message.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')
    messages.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')
    messages.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!(author_type)
  Rails.cache.write(typing_cache_key(author_type), true, expires_in: TYPING_TTL)
end

#typing?(author_type) ⇒ Boolean

Returns:

  • (Boolean)


124
125
126
# File 'app/models/livechat/conversation.rb', line 124

def typing?(author_type)
  Rails.cache.exist?(typing_cache_key(author_type))
end

#unread_from_visitor_countObject



106
# File 'app/models/livechat/conversation.rb', line 106

def unread_from_visitor_count = messages.from_visitor.unread.count