import { Controller } from "@hotwired/stimulus"
import consumer from "../channels/consumer"
export default class extends Controller {
static values = { roomId: Number }
connect() {
this.lastSentAt = 0
this.channel = consumer.subscriptions.create(
{ channel: "TypingChannel", room_id: this.roomIdValue },
{ received: () => {} }
)
}
ping() {
const now = Date.now()
if (now - this.lastSentAt < 1000) return
this.lastSentAt = now
this.channel.perform("typing")
}
}
class TypingChannel < ApplicationCable::Channel
def subscribed
@room = Room.find(params[:room_id])
stream_for @room
end
def typing
Turbo::StreamsChannel.broadcast_replace_to(
@room,
target: 'typing_indicator',
partial: 'rooms/typing',
locals: { member: current_member }
)
end
end
<div id="typing_indicator" class="text-sm text-gray-500">
<%= member.nickname %> is typing…
</div>
Typing indicators can be done without a complex protocol. I broadcast a small turbo stream replace to a typing_indicator target when a user starts typing, and another replace to clear it after a timeout. On the client, a Stimulus controller sends “typing” pings over a simple ActionCable channel. The server tracks last-typed timestamps (in-memory or Redis) and broadcasts to the room stream. This keeps the UI server-rendered and avoids building custom JSON renderers. The key is throttling: don’t send a message per keystroke—send at most once every second. Also be mindful of privacy: only show typing indicators in small groups (like a chat room) and don’t store typing events permanently.