Skip to content

Instantly share code, notes, and snippets.

@tbcooney
Last active May 3, 2020 13:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tbcooney/198266a6e7cc6672882e15595118fc0b to your computer and use it in GitHub Desktop.
Save tbcooney/198266a6e7cc6672882e15595118fc0b to your computer and use it in GitHub Desktop.
# chatroom_channel.rb
class ChatroomsChannel < ApplicationCable::Channel
def subscribed
current_user.chatrooms.each do |chatroom|
stream_from "chatrooms:#{chatroom.id}"
end
end
def unsubscribed
stop_all_streams
end
def send_message(data)
@chatroom = Chatroom.find(data["chatroom_id"])
@account = Account.find(data["account_id"])
@facility = Facility.find(data["facility_id"])
message = @chatroom.messages.create(
account: @account,
source: @facility,
content: data["message"],
user: current_user
)
MessageRelayJob.perform_later(message, @facility.id, @chatroom.id)
Rails.logger.info data
end
def typing(data)
ActionCable.server.broadcast "chatrooms:#{data["chatroom_id"]}", {
typing: data['typing'],
user_name: current_user.name,
chatroom_id: data["chatroom_id"]
}
end
end
# app/jobs/message_relay_job.rb
class MessageRelayJob < ApplicationJob
queue_as :default
def perform(message, facility_id, chatroom_id)
chatroom = Chatroom.find(chatroom_id)
facility = Facility.find(facility_id)
ActionCable.server.broadcast "chatrooms:#{message.chatroom.id}", {
message_id: message.id,
message: render_message(message, facility, chatroom),
user_name: message.user.name,
user_id: message.user.id,
body: message.content.body.to_plain_text,
chatroom_id: message.chatroom.id
}
end
def render_message(message, facility, chatroom)
ApplicationController.renderer.render(
partial: 'admin/facilities/chatrooms/chatroom/messages/message',
locals: { message: message, facility: facility, chatroom: chatroom }
)
end
end
# chatroom/message.rb
class Chatroom::Message < ApplicationRecord
has_rich_text :content
belongs_to :account
belongs_to :source, polymorphic: true
belongs_to :chatroom
belongs_to :user
validates :content,
presence: true
end
# chatrooms_controller.js
import { Controller } from "stimulus"
import Rails from "@rails/ujs"
import consumer from '../../channels/consumer'
export default class extends Controller {
static targets = ['input', 'form', 'status']
connect() {
this.initChannel()
this.onKeydown = this.onKeydown.bind(this)
this.inputTarget.addEventListener('keydown', this.onKeydown)
}
disconnect() {
this.inputTarget.removeEventListener('keydown', this.onKeydown)
}
initChannel() {
this.lastReadChannel = consumer.subscriptions.create('LastReadChannel')
this.channel = consumer.subscriptions.create('ChatroomsChannel', {
connected: this._cableConnected.bind(this),
disconnected: this._cableDisconnected.bind(this),
received: this._cableReceived.bind(this),
});
this.typingHandler = this.typing.bind(this)
this.inputTarget.addEventListener('keydown', this.typingHandler)
this.stoppedTyping = this.stoppedTyping.bind(this)
this.inputTarget.addEventListener('blur', this.stoppedTyping)
}
_cableConnected() {
// Called when the subscription is ready for use on the server
}
_cableDisconnected() {
// Called when the subscription has been terminated by the server
}
_cableReceived(data) {
// Called when there's incoming data on the websocket for this channel
if (data.typing && data.typing == "started") {
this.statusTarget.classList.add("active")
this.statusTarget.innerHTML = `${data.user_name} is typing...`
} else {
this.statusTarget.classList.remove("active")
this.statusTarget.innerHTML = ""
}
if (!data.typing) {
const active_chatroom = document.querySelectorAll(`[data-behavior='messages'][data-chatroom-id='${data.chatroom_id}'`)
if (active_chatroom.length > 0) {
// Check to see if the user has the chatroom open
// With tabbed browsing, there is a reasonable chance that any given webpage is in the background and thus not visible to the user.
if (document.hidden) {
// 1. Check to see if there is a divider on the page
const strike = document.querySelectorAll(".strike")
if (strike.length > 0) {
// 2. If there is no divider, insert one
active_chatroom.append("<div class='strike'><span>Unread Messages</span></div>")
}
// 3. Send Web Notification
// https://developer.mozilla.org/en-US/docs/Web/API/notification
if (Notification.permission === "granted") {
new Notification(data.user_name, {
body: data.body
})
}
} else {
// 4. Update last_read_at stamp
this.lastReadChannel.perform("update", { chatroom_id: data.chatroom_id })
}
// 5. Insert the message
active_chatroom[0].insertAdjacentHTML('beforeend', data.message)
this.scrollToBottom()
} else {
// 6. Highlight the channel in the sidebar
document.querySelector(`[data-behavior='chatroom-link'][data-chatroom-id='${data.chatroom_id}']`).classList.add("font-bold")
}
}
}
typing() {
// Don't broadcast if we're already typing
if(!this.isTyping) {
this.isTyping = true
const chatroom_id = document.querySelector("[data-behavior='messages'][data-chatroom-id]").dataset.chatroomId
this.channel.perform('typing', { chatroom_id: chatroom_id, typing: 'started' } )
}
// Do this no matter what so it resets the timer
this.startTypingTimer()
}
stoppedTyping() {
this.isTyping = false
this.stopTypingTimer()
const chatroom_id = document.querySelector("[data-behavior='messages'][data-chatroom-id]").dataset.chatroomId
this.channel.perform('typing', { chatroom_id: chatroom_id, typing: 'stopped' } )
}
startTypingTimer() {
// Clear the old timer or it'll still fire after 10 seconds. We're effectively resetting the timer.
this.stopTypingTimer()
// No need to save a reference to bound function since we don't need to reference it to stop the timer.
// After 10 seconds of not typing, don't consider the user to be typing
this.typingTimeoutID = setTimeout(this.stoppedTyping.bind(this), 10000)
}
stopTypingTimer() {
if(this.typingTimeoutID) {
clearTimeout(this.typingTimeoutID)
}
}
send_message(chatroom_id, message) {
const facility_id = document.querySelector(`meta[name="current-facility-id"]`).getAttribute('content')
this.channel.perform('send_message', {
chatroom_id: chatroom_id,
message: message,
facility_id: facility_id
})
}
onKeydown(e) {
if (e.keyCode == 13 && !e.shiftKey) {
if (this.inputTarget.innerHTML.length < 1) {
this.inputTarget.classList.add('opps-shake-it')
// Remove the class after the animation completes
setTimeout(() => {
this.inputTarget.classList.remove('opps-shake-it');
}, 300)
e.preventDefault()
} else {
//Submit the form
this.submit(e)
return false
}
}
}
submit(e) {
e.preventDefault()
const chatroom_id = document.querySelector("[data-behavior='messages'][data-chatroom-id]").dataset.chatroomId
const message = this.inputTarget.value
this.send_message(chatroom_id, message)
this.inputTarget.value = ""
}
}
@javan
Copy link

javan commented May 3, 2020

Assuming this.inputTarget is a <trix-editor>, here's a better way to check for "blank" using Trix's document model:

  onKeydown(e) {
    if (e.keyCode == 13 && !e.shiftKey) {
      if (this.inputTargetIsBlank) {
        // …
      } else {
        // …
      }
    }
  }

  get inputTargetIsBlank() {
    return this.inputTarget.editor.getDocument().toString().trim().length == 0
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment