diff options
-rw-r--r-- | app/channels/chat_room_channel.rb | 17 | ||||
-rw-r--r-- | app/controllers/chat_messages_controller.rb | 32 | ||||
-rw-r--r-- | app/javascript/channels/chat_room_channel.js | 22 | ||||
-rw-r--r-- | app/javascript/controllers/chat_controller.js | 42 | ||||
-rw-r--r-- | app/models/character.rb | 1 | ||||
-rw-r--r-- | app/models/chat_message.rb | 15 | ||||
-rw-r--r-- | app/models/chat_room.rb | 27 | ||||
-rw-r--r-- | app/views/application/_chat.html.erb | 13 | ||||
-rw-r--r-- | app/views/chat_messages/_list.html.erb | 3 | ||||
-rw-r--r-- | app/views/chat_messages/_message.html.erb | 47 | ||||
-rw-r--r-- | app/views/layouts/application.html.erb | 1 | ||||
-rw-r--r-- | config/routes.rb | 2 | ||||
-rw-r--r-- | data/chat_rooms.yml | 18 | ||||
-rw-r--r-- | db/migrate/20210520013553_create_chat_rooms.rb | 11 | ||||
-rw-r--r-- | db/migrate/20210520014637_create_chat_messages.rb | 12 | ||||
-rw-r--r-- | db/schema.rb | 25 | ||||
-rw-r--r-- | db/seeds.rb | 5 | ||||
-rw-r--r-- | test/fixtures/chat_messages.yml | 7 | ||||
-rw-r--r-- | test/fixtures/chat_rooms.yml | 9 | ||||
-rw-r--r-- | test/models/chat_message_test.rb | 7 | ||||
-rw-r--r-- | test/models/chat_room_test.rb | 7 |
21 files changed, 318 insertions, 5 deletions
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 @@ -<div class="flex flex-col h-full text-sm"> - <div id="chat_output" class="overflow-auto flex-grow"> - Chat output will go here. +<div data-controller="chat" class="flex flex-col h-full text-sm"> + <div data-chat-target="output" id="chat_output" class="overflow-auto flex-grow"> </div> <div class="flex-none"> - 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 %> </div> </div> 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 +%> +<p> + <span class="<%= chat_room_text_class %>"> + <span class="text-xs"><%= chat_message.created_at.strftime("%H:%M") %></span> + <%= chat_room_prefix %> + </span> + <% if chat_message.chat_room.gid == "system" %> + <span class="text-glow <%= chat_room_text_class %>"><%= chat_message.body %></span> + <% elsif chat_message.chat_room.gid == "news" %> + <span class="<%= chat_room_text_class %>"><%= chat_message.body %></span> + <% elsif chat_message.chat_room.gid == "achievement" %> + <%#= render "components/text/title", title: chat_message.target.current_title %> + <span class="<%= chat_room_text_class %>"> + <%# TODO: Sort out this subject/target stuff that I just half-blindly ported over from old Esoterra. %> + <%= chat_message.target&.name %> <%= chat_message.body %> + </span> + <% else %> + <% if chat_message.sender %> + <%#= render "components/text/title", title: chat_message.sender.current_title %> + <% end %> + <span class="<%= chat_room_text_class %>"> + <% if chat_message.sender %> + <span class="font-bold"><%= chat_message.sender.name %>:</span> + <% end %> + <%= chat_message.body %> + </span> + <% end %> +</p> 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" %> </div> + </div> <% else %> <div id="game_info_box" class="game-container-box side-box col-span-12 sm:col-span-4" data-turbolinks-permanent> diff --git a/config/routes.rb b/config/routes.rb index f3096f7..8f0513e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -11,6 +11,8 @@ Rails.application.routes.draw do put "users", to: "devise/registrations#update", as: "user_registration" end + resources :chat_messages, only: [:index, :create] + resources :activities, only: [:show] resources :characters, only: [:show, :new, :create] do diff --git a/data/chat_rooms.yml b/data/chat_rooms.yml new file mode 100644 index 0000000..e1b2bb0 --- /dev/null +++ b/data/chat_rooms.yml @@ -0,0 +1,18 @@ +system: + name: "System" + permission_level: 5 +achievement: + name: "Achievement" + permission_level: 5 +news: + name: "News" + permission_level: 5 +cosmic: + name: "Cosmic" + permission_level: 1 +trade: + name: "Trade" + permission_level: 1 +help: + name: "Help" + permission_level: 1 diff --git a/db/migrate/20210520013553_create_chat_rooms.rb b/db/migrate/20210520013553_create_chat_rooms.rb new file mode 100644 index 0000000..fadba64 --- /dev/null +++ b/db/migrate/20210520013553_create_chat_rooms.rb @@ -0,0 +1,11 @@ +class CreateChatRooms < ActiveRecord::Migration[6.1] + def change + create_table :chat_rooms do |t| + t.string :gid + t.string :name + t.integer :permission_level + + t.timestamps + end + end +end diff --git a/db/migrate/20210520014637_create_chat_messages.rb b/db/migrate/20210520014637_create_chat_messages.rb new file mode 100644 index 0000000..a020104 --- /dev/null +++ b/db/migrate/20210520014637_create_chat_messages.rb @@ -0,0 +1,12 @@ +class CreateChatMessages < ActiveRecord::Migration[6.1] + def change + create_table :chat_messages do |t| + t.references :chat_room, null: false, foreign_key: true + t.references :sender, foreign_key: { to_table: :characters } + t.references :target, foreign_key: { to_table: :characters } + t.text :body + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 4f75f04..ee68bb9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2021_05_20_013124) do +ActiveRecord::Schema.define(version: 2021_05_20_014637) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -69,6 +69,26 @@ ActiveRecord::Schema.define(version: 2021_05_20_013124) do t.index ["user_id"], name: "index_characters_on_user_id" end + create_table "chat_messages", force: :cascade do |t| + t.bigint "chat_room_id", null: false + t.bigint "sender_id" + t.bigint "target_id" + t.text "body" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["chat_room_id"], name: "index_chat_messages_on_chat_room_id" + t.index ["sender_id"], name: "index_chat_messages_on_sender_id" + t.index ["target_id"], name: "index_chat_messages_on_target_id" + end + + create_table "chat_rooms", force: :cascade do |t| + t.string "gid" + t.string "name" + t.integer "permission_level" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + end + create_table "hearth_amenities", force: :cascade do |t| t.string "gid" t.string "name" @@ -154,6 +174,9 @@ ActiveRecord::Schema.define(version: 2021_05_20_013124) do add_foreign_key "character_skills", "skills" add_foreign_key "characters", "activities" add_foreign_key "characters", "users" + add_foreign_key "chat_messages", "characters", column: "sender_id" + add_foreign_key "chat_messages", "characters", column: "target_id" + add_foreign_key "chat_messages", "chat_rooms" add_foreign_key "hearths", "characters" add_foreign_key "users", "characters", column: "active_character_id" end diff --git a/db/seeds.rb b/db/seeds.rb index cf4caa6..7adf0fc 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -10,6 +10,11 @@ def load_data_file(path) YAML.load(File.read(path)).deep_transform_keys(&:to_sym) end +load_data_file("data/chat_rooms.yml").map do |gid, hash| + chat_room = ChatRoom.find_or_create_by(gid: gid) + chat_room.update(hash) +end + load_data_file("data/skills.yml").map do |gid, hash| skill = Skill.find_or_create_by(gid: gid) skill.update(hash) diff --git a/test/fixtures/chat_messages.yml b/test/fixtures/chat_messages.yml new file mode 100644 index 0000000..0ce7cf8 --- /dev/null +++ b/test/fixtures/chat_messages.yml @@ -0,0 +1,7 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + chat_room: one + +two: + chat_room: two diff --git a/test/fixtures/chat_rooms.yml b/test/fixtures/chat_rooms.yml new file mode 100644 index 0000000..8476a5b --- /dev/null +++ b/test/fixtures/chat_rooms.yml @@ -0,0 +1,9 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + name: MyString + permission_level: 1 + +two: + name: MyString + permission_level: 1 diff --git a/test/models/chat_message_test.rb b/test/models/chat_message_test.rb new file mode 100644 index 0000000..78428cf --- /dev/null +++ b/test/models/chat_message_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class ChatMessageTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/chat_room_test.rb b/test/models/chat_room_test.rb new file mode 100644 index 0000000..3451e5c --- /dev/null +++ b/test/models/chat_room_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class ChatRoomTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end |