Class: Chats::ConversationsController
- Inherits:
-
ApplicationController
- Object
- ApplicationController
- Chats::ConversationsController
- Defined in:
- app/controllers/chats/conversations_controller.rb
Overview
The inbox (index), the thread (show), starting conversations from host pages (create), and the per-member actions (read/typing/leave/mute).
Instance Method Summary collapse
-
#create ⇒ Object
Start (or resume) a direct conversation from a host page.
-
#index ⇒ Object
The inbox.
- #leave ⇒ Object
- #mute ⇒ Object
-
#read ⇒ Object
Advance the viewer’s read horizon.
-
#refresh ⇒ Object
Stale-thread catch-up: appends messages created — and replaces ones edited/tombstoned — since the newest ‘updated_at` the client has rendered (`?since=` in ms).
-
#show ⇒ Object
The thread.
-
#typing ⇒ Object
Ephemeral typing ping (client throttles to ~1 every 3s while typing).
- #unmute ⇒ Object
Instance Method Details
#create ⇒ Object
Start (or resume) a direct conversation from a host page. The recipient/subject arrive as SIGNED GlobalIDs minted by the ‘chat_button_to` helper — unforgeable and purpose-scoped, so raw polymorphic params never reach `GlobalID::Locator`. Policy and block checks still run inside `chat_with` (defense in depth).
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'app/controllers/chats/conversations_controller.rb', line 96 def create recipient = locate_signed!(params.require(:recipient_sgid), purpose: :chats_recipient) raise ActiveRecord::RecordNotFound unless Chats.(recipient.class) subject = params[:subject_sgid].presence && locate_signed!(params[:subject_sgid], purpose: :chats_subject) conversation = .chat_with(recipient, about: subject) redirect_to conversation_path(conversation) rescue Chats::BlockedError # Fallback is the INBOX (engine root) — inside the engine, `root_path` # already resolves there, never to the host root. redirect_back fallback_location: conversations_path, alert: t("chats.flashes.blocked") rescue Chats::NotAllowedError redirect_back fallback_location: conversations_path, alert: t("chats.flashes.not_allowed") end |
#index ⇒ Object
The inbox. Everything is preloaded/batched so rendering N rows costs a constant number of queries (conversations + last messages + participants + one grouped unread-count query — see Conversation.unread_counts_for).
12 13 14 15 16 17 18 |
# File 'app/controllers/chats/conversations_controller.rb', line 12 def index @conversations = .chats .includes(:last_message, :subject, participants: :messager) .limit(200) @conversations = apply_search(@conversations) @unread_counts = Chats::Conversation.unread_counts_for(, @conversations) end |
#leave ⇒ Object
129 130 131 132 133 134 135 136 137 |
# File 'app/controllers/chats/conversations_controller.rb', line 129 def leave # Direct threads can't be left (mute or block instead) — leaving would # strand a 1:1 thread in a weird half-state. raise ActiveRecord::RecordNotFound if @conversation.direct? title = @conversation.title_for() @conversation.participant_for()&.leave! redirect_to conversations_path, notice: t("chats.flashes.left", title: title) end |
#mute ⇒ Object
139 140 141 142 |
# File 'app/controllers/chats/conversations_controller.rb', line 139 def mute @conversation.participant_for()&.mute! redirect_to conversation_path(@conversation), notice: t("chats.flashes.muted") end |
#read ⇒ Object
Advance the viewer’s read horizon. Called by the thread Stimulus controller (debounced) when new messages arrive while the thread is visible. Side effects (read-state broadcast, badge refresh) live in Participant#read!.
117 118 119 120 |
# File 'app/controllers/chats/conversations_controller.rb', line 117 def read @conversation.participant_for()&.read! head :no_content end |
#refresh ⇒ Object
Stale-thread catch-up: appends messages created — and replaces ones edited/tombstoned — since the newest ‘updated_at` the client has rendered (`?since=` in ms). The thread controller calls this when the tab wakes from a long sleep or its Turbo Stream subscription reconnects, i.e. whenever broadcasts may have been missed. Mobile WebViews suspend WebSockets aggressively, so without this a backgrounded chat silently loses messages until a manual reload. Pattern from Basecamp’s Campfire (Rooms::RefreshesController): github.com/basecamp/once-campfire
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'app/controllers/chats/conversations_controller.rb', line 67 def refresh head :no_content and return if params[:since].blank? since = Time.zone.at(0, params[:since].to_i, :millisecond) scope = @conversation..includes(:sender, :reactions) scope = scope.with_attached_files if scope.respond_to?(:with_attached_files) @new_messages = scope.created_since(since).oldest_first.limit(Chats.config. + 1).to_a # A backlog deeper than one page would mean splicing an arbitrary # amount of history through surgical appends; a Turbo 8 page refresh # (morph + scroll preservation) re-renders the latest page + frame # chain correctly instead. Raw tag rather than `turbo_stream.refresh` # so we don't depend on turbo-rails ≥ 2.0 helpers. if @new_messages.size > Chats.config. render html: '<turbo-stream action="refresh"></turbo-stream>'.html_safe, content_type: "text/vnd.turbo-stream.html" return end @updated_messages = scope.updated_since(since) render "chats/conversations/refresh", formats: :turbo_stream end |
#show ⇒ Object
The thread. Renders the LATEST page of messages; older pages stream in through a lazy Turbo Frame chain (keyset-paginated — see Message.before_message and _messages_page.html.erb).
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
# File 'app/controllers/chats/conversations_controller.rb', line 23 def show @participant = @conversation.participant_for() anchor = params[:before].present? ? @conversation..find_by(id: params[:before]) : nil scope = @conversation..includes(:sender, :reactions) scope = scope.with_attached_files if scope.respond_to?(:with_attached_files) scope = scope.(anchor) if anchor # Fetch newest-first + reverse so "the last N messages" render in # chronological order. One extra record peeks whether older pages exist. page = scope.recent_first.limit(Chats.config. + 1).to_a @more_messages = page.size > Chats.config. @messages = page.first(Chats.config.).reverse if anchor # Older-page request from the pagination frame: render just the page. render partial: "chats/conversations/messages_page", locals: { conversation: @conversation, messages: @messages, more: @more_messages } else # The «new messages» divider: computed BEFORE read! advances the # horizon (after it, nothing is unread anymore). Anchored to the # oldest unread bubble on the rendered page; when the backlog runs # deeper than one page it pins to the top of the page instead — # the scroll-up frame chain holds the rest. if @participant&.unread? @first_unread_id = @participant..where(id: @messages.map(&:id)).oldest_first.pick(:id) || @messages.first&.id end # Opening the thread reads it. (Live appends while the thread stays # open are read via the thread controller's POST to #read.) @participant&.read! end end |
#typing ⇒ Object
Ephemeral typing ping (client throttles to ~1 every 3s while typing). Nothing is persisted; see Chats::Broadcasts.typing.
124 125 126 127 |
# File 'app/controllers/chats/conversations_controller.rb', line 124 def typing Chats::Broadcasts.typing(@conversation, ) if Chats.config.typing_indicators head :no_content end |
#unmute ⇒ Object
144 145 146 147 |
# File 'app/controllers/chats/conversations_controller.rb', line 144 def unmute @conversation.participant_for()&.unmute! redirect_to conversation_path(@conversation), notice: t("chats.flashes.unmuted") end |