Module: Protege::MessagesHelper

Included in:
ApplicationHelper
Defined in:
app/helpers/protege/messages_helper.rb

Overview

View helpers for rendering a Message within a thread — sender, timestamp, recipients, subject, and expandable raw headers. Liveness (processing/reasoning/tool use) is not shown on the message; it lives in the console's introspection panel.

Instance Method Summary collapse

Instance Method Details

#message_attachments(message) ⇒ ActiveSupport::SafeBuffer

Render a message's attachments as a row of download-link chips, or nothing when there are none.

Each chip links to the blob via Active Storage's host-app route (+main_app.rails_blob_path+, since those routes live on the host, not the engine) and shows the filename and a human-readable size. link_to escapes the filename, which is copied verbatim from attacker-controlled inbound headers.

The link forces a download two ways: disposition: "attachment" makes the service set Content-Disposition: attachment (so even an .html blob downloads rather than rendering), and the download attribute both hints the filename and opts the anchor out of Turbo Drive — without it Turbo intercepts the click and renders an HTML attachment as a page instead of downloading it.

Parameters:

Returns:

  • (ActiveSupport::SafeBuffer)

    the chips row, or an empty buffer



103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'app/helpers/protege/messages_helper.rb', line 103

def message_attachments(message)
  return ''.html_safe unless message.attachments.attached?

  chips = message.attachments.map do |attachment|
    label = "#{attachment.filename} (#{number_to_human_size(attachment.byte_size)})"
    link_to(label,
            main_app.rails_blob_path(attachment, disposition: 'attachment', only_path: true),
            download: attachment.filename.to_s,
            class:    'inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-xs font-medium hover:underline',
            style:    'background: var(--surface-subtle); border: 1px solid var(--border)')
  end

  tag.div(class: 'mt-3 flex flex-wrap gap-2') { safe_join(chips) }
end

#message_headers(message) ⇒ ActiveSupport::SafeBuffer

Render an expandable <details> block listing the message's raw headers.

Parameters:

Returns:

  • (ActiveSupport::SafeBuffer)

    the collapsible headers block



83
84
85
86
87
88
# File 'app/helpers/protege/messages_helper.rb', line 83

def message_headers(message)
  tag.details(class: 'mt-1') do
    tag.summary('Headers', class: 'cursor-pointer text-xs', style: 'color: var(--text-faint)') +
      message_headers_list(message)
  end
end

#message_recipients(message) ⇒ ActiveSupport::SafeBuffer

Render the To line, appending a Cc line only when Cc recipients exist.

Parameters:

Returns:

  • (ActiveSupport::SafeBuffer)

    the recipients line



56
57
58
59
60
61
62
63
64
65
# File 'app/helpers/protege/messages_helper.rb', line 56

def message_recipients(message)
  to = tag.span("To: #{Array(message.to_addresses).join(', ')}")
  cc = if Array(message.cc_addresses).any?
         tag.span("Cc: #{message.cc_addresses.join(', ')}", class: 'ml-2')
       else
         ''.html_safe
       end

  tag.div(class: 'mt-1 text-xs', style: 'color: var(--text-muted)') { safe_join([to, cc]) }
end

#message_sender(message) ⇒ ActiveSupport::SafeBuffer

Render the sender line — persona name for outbound mail, local part of the address for inbound mail — followed by the full address.

Parameters:

Returns:

  • (ActiveSupport::SafeBuffer)

    the bold "name <address>" span



29
30
31
32
33
34
35
36
37
# File 'app/helpers/protege/messages_helper.rb', line 29

def message_sender(message)
  name = if message.outbound?
           message.persona.name
         else
           message.from_address.to_s.split('@').first
         end

  tag.span("#{name} <#{message.from_address}>", class: 'font-semibold')
end

#message_subject(message) ⇒ ActiveSupport::SafeBuffer

Render the subject line, but only when it differs from the thread subject.

Parameters:

Returns:

  • (ActiveSupport::SafeBuffer)

    the subject line, or an empty buffer



71
72
73
74
75
76
77
# File 'app/helpers/protege/messages_helper.rb', line 71

def message_subject(message)
  if message.subject.present? && message.subject != message.email_thread.subject
    tag.div("Subject: #{message.subject}", class: 'mt-1 text-xs font-medium')
  else
    ''.html_safe
  end
end

#message_timestamp(message) ⇒ ActiveSupport::SafeBuffer

Render the message's send time as a <time> element.

Parameters:

Returns:

  • (ActiveSupport::SafeBuffer)

    a formatted <time> tag



43
44
45
46
47
48
49
50
# File 'app/helpers/protege/messages_helper.rb', line 43

def message_timestamp(message)
  tag.time(
    message.sent_at.strftime('%b %-d, %Y %H:%M'),
    datetime: message.sent_at.iso8601,
    class:    'flex-shrink-0 text-xs',
    style:    'color: var(--text-faint)'
  )
end

#search_result_date(message) ⇒ String?

The search-result row's right-aligned date — the email's sent time in short m/d/yy form.

Parameters:

Returns:

  • (String, nil)

    the short date, or nil when unsent



20
21
22
# File 'app/helpers/protege/messages_helper.rb', line 20

def search_result_date(message)
  message.sent_at&.strftime('%-m/%-d/%y')
end

#search_result_title(message) ⇒ String

The search-result row title: the email subject, or a placeholder when blank.

Parameters:

Returns:

  • (String)

    the subject or "(no subject)"



12
13
14
# File 'app/helpers/protege/messages_helper.rb', line 12

def search_result_title(message)
  message.subject.presence || '(no subject)'
end