Module: Chats::EngineHelper

Defined in:
app/helpers/chats/engine_helper.rb

Overview

View helpers, available BOTH inside the engine’s own views and in the HOST app’s views (mixed into ActionView via the engine’s on_load hook, the same pattern the moderate gem uses for ‘report_link`).

Instance Method Summary collapse

Instance Method Details

#chat_button_to(other, about: nil, label: nil, **html_options) ⇒ Object

The “message this person” affordance for host pages — a listing, a profile, an order. Renders nothing when there’s no viewer, the viewer IS the target, or policy/blocks forbid the pair, so it’s always safe to drop into a page unconditionally:

<%= chat_button_to @driver, about: @ride, label: "Chat with driver" %>

The recipient/subject travel as SIGNED GlobalIDs (purpose-scoped, minted here, verified in Chats::ConversationsController#create), so the endpoint never trusts raw polymorphic params. ‘expires_in: nil` because these buttons sit on long-lived pages — the default 1-month sgid expiry would quietly break stale tabs.



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'app/helpers/chats/engine_helper.rb', line 20

def chat_button_to(other, about: nil, label: nil, **html_options)
  viewer = chats_viewer
  return if viewer.nil? || other.nil? || viewer == other
  return unless Chats.can_message?(viewer, other)

  params = { recipient_sgid: other.to_sgid(expires_in: nil, for: :chats_recipient).to_s }
  params[:subject_sgid] = about.to_sgid(expires_in: nil, for: :chats_subject).to_s if about

  button_to(
    label || I18n.t("chats.buttons.chat"),
    chats_routes.conversations_path,
    params: params,
    method: :post,
    **html_options
  )
end

#chats_conversation_avatar(conversation, viewer) ⇒ Object

The avatar shown on an inbox row: the counterpart’s (direct) or an initials disc from the group name.



118
119
120
121
122
123
124
125
126
127
# File 'app/helpers/chats/engine_helper.rb', line 118

def chats_conversation_avatar(conversation, viewer)
  if conversation.direct?
    other = conversation.other_participants(viewer).first
    chats_messager_avatar(other&.messager)
  else
    initials = conversation.title_for(viewer).split.first(2).map { |word| word[0] }.join.upcase
    tag.span(initials.presence || "👥", class: "chats-avatar chats-avatar--initials chats-avatar--group",
                                       "aria-hidden": true)
  end
end

#chats_messager_avatar(messager, css_class: "chats-avatar", loading: "eager") ⇒ Object

An avatar for any messager: whatever ‘config.messager_avatar` returns (URL / ActiveStorage attachment / variant) or an initials placeholder.



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'app/helpers/chats/engine_helper.rb', line 52

def chats_messager_avatar(messager, css_class: "chats-avatar", loading: "eager")
  name = Chats.display_name_for(messager).presence || "?"
  source = begin
    avatar = Chats.avatar_for(messager)
    # An ActiveStorage attachment that isn't attached renders as a broken
    # image — treat it as "no avatar" instead.
    avatar.respond_to?(:attached?) && !avatar.attached? ? nil : avatar
  end

  if source
    image_tag chats_avatar_image_source(source), alt: name, class: css_class, loading: loading
  else
    initials = name.split.first(2).map { |word| word[0] }.join.upcase
    tag.span(initials, class: "#{css_class} chats-avatar--initials", "aria-hidden": true)
  end
end

#chats_preview_for(conversation, viewer) ⇒ Object

The inbox-row preview line for a conversation’s latest message.



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'app/helpers/chats/engine_helper.rb', line 86

def chats_preview_for(conversation, viewer)
  message = conversation.last_message
  return I18n.t("chats.inbox.no_messages") if message.nil?

  text =
    if message.deleted?
      I18n.t("chats.message.deleted")
    elsif message.body.present?
      message.body.truncate(90)
    elsif message.attachments?
      "📷 #{I18n.t("chats.message.attachment")}"
    else
      ""
    end

  # Tombstones read as a statement ("Message deleted") — never prefixed.
  # Otherwise: "You:" for own messages, and in GROUPS the sender's first
  # name (WhatsApp-style), since "who said it" is ambiguous there. System
  # messages (no sender) stay bare.
  prefix =
    if message.deleted?
      nil
    elsif message.sent_by?(viewer)
      I18n.t("chats.inbox.you_prefix")
    elsif conversation.group? && message.sender
      "#{Chats.display_name_for(message.sender).split.first}:"
    end
  [prefix, text].compact.join(" ")
end

#chats_routesObject

Engine URL helpers that work from EVERY render context — this is more subtle than it looks, and the reason the broadcast partials use it:

* host views & the broadcast renderer (Turbo broadcasts render through
  the host's ApplicationController renderer — no engine request, no
  SCRIPT_NAME): the mounted proxy (`chats.`) carries the mount prefix
  baked in at mount time, so URLs come out right with no request.
* engine views during requests: the engine controller inherits from
  the host's ApplicationController, so the proxy is available there
  too (and bare helpers would also work — the proxy just works
  everywhere).
* no mount at all (bare view tests): fall back to the engine's own
  url_helpers (prefix-less, but nothing better exists without a mount).

NOTE: assumes the default mount name (‘mount Chats::Engine => “/x”` auto-names the proxy `chats`). Hosts using `as: :something_else` should override this helper.



153
154
155
# File 'app/helpers/chats/engine_helper.rb', line 153

def chats_routes
  respond_to?(:chats) ? chats : Chats::Engine.routes.url_helpers
end

#chats_stylesObject

The gem’s bundled stylesheet (CSS-variable themed — see chats.css). Called from the engine’s own views; hosts that eject + restyle the views with their own framework simply don’t include it.



132
133
134
# File 'app/helpers/chats/engine_helper.rb', line 132

def chats_styles
  stylesheet_link_tag "chats", "data-turbo-track": "reload"
end

#chats_timestamp(time) ⇒ Object

WhatsApp-style compact timestamps, deliberately numeric so they need no date-name translations (many apps don’t bundle rails-i18n; the gem must not require it): today → “14:05”, this week-ish → “9/6”, older → “9/6/25”.



72
73
74
75
76
77
78
79
80
81
82
83
# File 'app/helpers/chats/engine_helper.rb', line 72

def chats_timestamp(time)
  return "" if time.nil?

  local = time.in_time_zone
  if local.today?
    local.strftime("%H:%M")
  elsif local.year == Time.current.year
    local.strftime("%-d/%-m")
  else
    local.strftime("%-d/%-m/%y")
  end
end

#chats_unread_badge(messager = chats_viewer) ⇒ Object

A live unread-conversations badge, embeddable on ANY page (nav bars, tab docks). Subscribes to the messager’s badge stream so it updates in real time without refreshing the page it sits on (see Chats::Broadcasts for why badges get their own stream).



41
42
43
44
45
46
47
48
# File 'app/helpers/chats/engine_helper.rb', line 41

def chats_unread_badge(messager = chats_viewer)
  return if messager.nil?

  safe_join([
              turbo_stream_from(messager, :chats_badge),
              render(partial: "chats/shared/unread_badge", locals: { count: messager.unread_chats_count })
            ])
end