From 9415011b5fd192f1bafeaa9b6eacbb7921382a00 Mon Sep 17 00:00:00 2001 From: David Gay Date: Wed, 19 May 2021 22:53:38 -0400 Subject: Chat --- app/channels/chat_room_channel.rb | 17 ++++++++++ app/controllers/chat_messages_controller.rb | 32 ++++++++++++++++++ app/javascript/channels/chat_room_channel.js | 22 +++++++++++++ app/javascript/controllers/chat_controller.js | 42 ++++++++++++++++++++++++ app/models/character.rb | 1 + app/models/chat_message.rb | 15 +++++++++ app/models/chat_room.rb | 27 +++++++++++++++ app/views/application/_chat.html.erb | 13 +++++--- app/views/chat_messages/_list.html.erb | 3 ++ app/views/chat_messages/_message.html.erb | 47 +++++++++++++++++++++++++++ app/views/layouts/application.html.erb | 1 + 11 files changed, 216 insertions(+), 4 deletions(-) create mode 100644 app/channels/chat_room_channel.rb create mode 100644 app/controllers/chat_messages_controller.rb create mode 100644 app/javascript/channels/chat_room_channel.js create mode 100644 app/javascript/controllers/chat_controller.js create mode 100644 app/models/chat_message.rb create mode 100644 app/models/chat_room.rb create mode 100644 app/views/chat_messages/_list.html.erb create mode 100644 app/views/chat_messages/_message.html.erb (limited to 'app') diff --git a/app/channels/chat_room_channel.rb b/app/channels/chat_room_channel.rb new file mode 100644 index 0000000..1e14b35 --- /dev/null +++ b/app/channels/chat_room_channel.rb @@ -0,0 +1,17 @@ +class ChatRoomChannel < ApplicationCable::Channel + def self.broadcast_chat_message(chat_message) + ActionCable.server.broadcast "chat_room_channel", + html: ApplicationController.render(partial: "chat_messages/message", + locals: { + chat: chat_message + }) + end + + def subscribed + stream_from "chat_room_channel" + end + + def unsubscribed + # Any cleanup needed when channel is unsubscribed + end +end diff --git a/app/controllers/chat_messages_controller.rb b/app/controllers/chat_messages_controller.rb new file mode 100644 index 0000000..9112bb7 --- /dev/null +++ b/app/controllers/chat_messages_controller.rb @@ -0,0 +1,32 @@ +class ChatMessagesController < ApplicationController + def index + # TODO: Let's rename this method to #list + @chat_messages = ChatMessage.order(created_at: :desc).limit(100).reverse + render partial: "chat_messages/list" + end + + def create + @chat_message = ChatMessage.new(body: chat_message_params[:body], + chat_room_id: chat_message_params[:chat_room_id], + sender: current_char) + # TODO: Make this block less bad + respond_to do |format| + if @chat_message.save + ActionCable.server.broadcast "chat_room_channel", + html: render_to_string(partial: "message", + locals: { + chat_message: @chat_message + }) + format.html { redirect_to character_path(current_char) } + format.js + else + format.html { redirect_to character_path(current_char), alert: "Failed to send chat message." } + end + end + end + + private + def chat_message_params + params.require(:chat_message).permit(:body, :chat_room_id) + end +end diff --git a/app/javascript/channels/chat_room_channel.js b/app/javascript/channels/chat_room_channel.js new file mode 100644 index 0000000..8dfa47f --- /dev/null +++ b/app/javascript/channels/chat_room_channel.js @@ -0,0 +1,22 @@ +import consumer from "./consumer" + +consumer.subscriptions.create("ChatRoomChannel", { + connected() { + // Called when the subscription is ready for use on the server + }, + + disconnected() { + // Called when the subscription has been terminated by the server + }, + + received(data) { + // Called when there's incoming data on the websocket for this channel + var node = document.createElement("P"); + node.innerHTML = data.html; + var chatOutputElement = document.getElementById("chat_output"); + chatOutputElement.appendChild(node); + chatOutputElement.scrollTo({ + top: chatOutputElement.scrollHeight, left: 0, behavior: 'smooth' + }); + } +}); diff --git a/app/javascript/controllers/chat_controller.js b/app/javascript/controllers/chat_controller.js new file mode 100644 index 0000000..d9cf1cb --- /dev/null +++ b/app/javascript/controllers/chat_controller.js @@ -0,0 +1,42 @@ +import { Controller } from "stimulus"; + +export default class extends Controller { + static targets = [ "message", "output" ]; + + connect() { + this.load(); + } + + send() { + let vm = this; + // TODO: Temporary hack. Should just run this after default event. + setTimeout(function() { + vm.messageTarget.value = ""; + vm.smoothScrollToBottom(); + }, 100); + } + + load() { + this.scrollToBottom(); + if (this.outputTarget.innerHTML.trim() === "") { + fetch("/chat_messages") + .then(response => response.text()) + .then(html => { + this.outputTarget.innerHTML = html; + this.scrollToBottom(); + }); + } + } + + scrollToBottom() { + this.outputTarget.scrollTop = this.outputTarget.scrollHeight; + } + + smoothScrollToBottom() { + this.outputTarget.scrollTo({ + top: this.outputTarget.scrollHeight, + left: 0, + behavior: 'smooth' + }); + } +} diff --git a/app/models/character.rb b/app/models/character.rb index 55ea7ba..9a3569f 100644 --- a/app/models/character.rb +++ b/app/models/character.rb @@ -5,6 +5,7 @@ class Character < ApplicationRecord has_many :character_items has_many :items, through: :character_items has_many :character_skills + has_many :chat_messages validates :name, presence: true validates_length_of :name, maximum: 15, message: "can't be longer than 15 characters" validates_uniqueness_of :name, message: "is already being used" diff --git a/app/models/chat_message.rb b/app/models/chat_message.rb new file mode 100644 index 0000000..e1aae6c --- /dev/null +++ b/app/models/chat_message.rb @@ -0,0 +1,15 @@ +class ChatMessage < ApplicationRecord + belongs_to :chat_room + belongs_to :sender, class_name: "Character", optional: true + belongs_to :target, class_name: "Character", optional: true + + validates :body, length: { minimum: 1, maximum: 255 } + validate :sender_has_access + + private + def sender_has_access + if sender && !sender.user.has_permission_or_higher?(self.chat_room.permission_level) + errors.add(:chat, "You don't have permission to send that chat.") + end + end +end diff --git a/app/models/chat_room.rb b/app/models/chat_room.rb new file mode 100644 index 0000000..17e3f19 --- /dev/null +++ b/app/models/chat_room.rb @@ -0,0 +1,27 @@ +class ChatRoom < ApplicationRecord + has_many :chat_messages + validates :gid, :name, presence: true, uniqueness: true + + attribute :permission_level, :integer, default: 1 + enum permission_level: { + banned: 0, + player: 1, + helper: 2, + builder: 3, + admin: 4, + developer: 5, + owner: 6 + } + + def self.accessible_to(user) + all.select { |x| x.accessible_to?(user) } + end + + def accessible_to?(user) + user.has_permission_or_higher?(self.permission_level) + end + + def short_name + self.name[0..6] + end +end diff --git a/app/views/application/_chat.html.erb b/app/views/application/_chat.html.erb index 868986b..8abe73c 100644 --- a/app/views/application/_chat.html.erb +++ b/app/views/application/_chat.html.erb @@ -1,8 +1,13 @@ -
-
- Chat output will go here. +
+
- Chat input will go here. + <%= form_with model: ChatMessage.new, html: { autocomplete: "off" }, local: false, + data: { action: "chat#send" }, class: "flex" do |f| %> + <%= f.collection_select :chat_room_id, ChatRoom.accessible_to(current_char.user), + :id, :short_name, class: "flex-none" %> + <%= f.text_field :body, size: "1", maxlength: 255, required: true, + data: { chat_target: "message" }, class: "flex-grow inline-flex" %> + <% end %>
diff --git a/app/views/chat_messages/_list.html.erb b/app/views/chat_messages/_list.html.erb new file mode 100644 index 0000000..8b12fd2 --- /dev/null +++ b/app/views/chat_messages/_list.html.erb @@ -0,0 +1,3 @@ +<% @chat_messages.each do |chat_message| %> + <%= render "message", chat_message: chat_message %> +<% end %> diff --git a/app/views/chat_messages/_message.html.erb b/app/views/chat_messages/_message.html.erb new file mode 100644 index 0000000..e9663ec --- /dev/null +++ b/app/views/chat_messages/_message.html.erb @@ -0,0 +1,47 @@ +<% chat_room_text_class = case chat_message.chat_room.gid + when "cosmic" then "text-gray-400" + when "trade" then "text-blue-400" + when "help" then "text-pink-300" + when "system" then "text-red-500" + when "achievement" then "text-purple-400" + when "news" then "text-yellow-400" + else "text-gray-400" + end + + chat_room_prefix = case chat_message.chat_room.gid + when "cosmic" then "[C]" + when "trade" then "[T]" + when "help" then "[H]" + when "system" then "[S]" + when "achievement" then "[A]" + when "news" then "[N]" + else nil + end +%> +

+ + <%= chat_message.created_at.strftime("%H:%M") %> + <%= chat_room_prefix %> + + <% if chat_message.chat_room.gid == "system" %> + <%= chat_message.body %> + <% elsif chat_message.chat_room.gid == "news" %> + <%= chat_message.body %> + <% elsif chat_message.chat_room.gid == "achievement" %> + <%#= render "components/text/title", title: chat_message.target.current_title %> + + <%# TODO: Sort out this subject/target stuff that I just half-blindly ported over from old Esoterra. %> + <%= chat_message.target&.name %> <%= chat_message.body %> + + <% else %> + <% if chat_message.sender %> + <%#= render "components/text/title", title: chat_message.sender.current_title %> + <% end %> + + <% if chat_message.sender %> + <%= chat_message.sender.name %>: + <% end %> + <%= chat_message.body %> + + <% end %> +

diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 3fe6f28..7ae44a9 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -30,6 +30,7 @@ data-turbolinks-permanent> <%= render "chat" %>
+
<% else %>