Commit 3c652688 by Mai Hoang Thai Ha

Created app chat

parent 44bc096d
Pipeline #1507 failed with stages
in 0 seconds
...@@ -27,6 +27,10 @@ gem 'jbuilder', '~> 2.7' ...@@ -27,6 +27,10 @@ gem 'jbuilder', '~> 2.7'
# Reduces boot times through caching; required in config/boot.rb # Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', '>= 1.4.4', require: false gem 'bootsnap', '>= 1.4.4', require: false
gem 'redis'
gem 'slim-rails', '~> 3.3'
gem 'pry', '~> 0.14.1'
gem 'pry-rails', '~> 0.3.9'
group :development, :test do group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console # Call 'byebug' anywhere in the code to stop execution and get a debugger console
......
...@@ -77,6 +77,7 @@ GEM ...@@ -77,6 +77,7 @@ GEM
regexp_parser (>= 1.5, < 3.0) regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2) xpath (~> 3.2)
childprocess (4.1.0) childprocess (4.1.0)
coderay (1.1.3)
concurrent-ruby (1.1.9) concurrent-ruby (1.1.9)
crass (1.0.6) crass (1.0.6)
erubi (1.10.0) erubi (1.10.0)
...@@ -104,6 +105,11 @@ GEM ...@@ -104,6 +105,11 @@ GEM
nio4r (2.5.8) nio4r (2.5.8)
nokogiri (1.12.5-x86_64-linux) nokogiri (1.12.5-x86_64-linux)
racc (~> 1.4) racc (~> 1.4)
pry (0.14.1)
coderay (~> 1.1)
method_source (~> 1.0)
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (4.0.6) public_suffix (4.0.6)
puma (5.5.2) puma (5.5.2)
nio4r (~> 2.0) nio4r (~> 2.0)
...@@ -145,6 +151,7 @@ GEM ...@@ -145,6 +151,7 @@ GEM
rb-fsevent (0.11.0) rb-fsevent (0.11.0)
rb-inotify (0.10.1) rb-inotify (0.10.1)
ffi (~> 1.0) ffi (~> 1.0)
redis (4.5.1)
regexp_parser (2.1.1) regexp_parser (2.1.1)
rexml (3.2.5) rexml (3.2.5)
rubyzip (2.3.2) rubyzip (2.3.2)
...@@ -163,6 +170,13 @@ GEM ...@@ -163,6 +170,13 @@ GEM
rexml (~> 3.2, >= 3.2.5) rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2) rubyzip (>= 1.2.2)
semantic_range (3.0.0) semantic_range (3.0.0)
slim (4.1.0)
temple (>= 0.7.6, < 0.9)
tilt (>= 2.0.6, < 2.1)
slim-rails (3.3.0)
actionpack (>= 3.1)
railties (>= 3.1)
slim (>= 3.0, < 5.0)
spring (3.0.0) spring (3.0.0)
sprockets (4.0.2) sprockets (4.0.2)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
...@@ -172,6 +186,7 @@ GEM ...@@ -172,6 +186,7 @@ GEM
activesupport (>= 4.0) activesupport (>= 4.0)
sprockets (>= 3.0.0) sprockets (>= 3.0.0)
sqlite3 (1.4.2) sqlite3 (1.4.2)
temple (0.8.2)
thor (1.1.0) thor (1.1.0)
tilt (2.0.10) tilt (2.0.10)
turbolinks (5.2.1) turbolinks (5.2.1)
...@@ -209,11 +224,15 @@ DEPENDENCIES ...@@ -209,11 +224,15 @@ DEPENDENCIES
capybara (>= 3.26) capybara (>= 3.26)
jbuilder (~> 2.7) jbuilder (~> 2.7)
listen (~> 3.3) listen (~> 3.3)
pry (~> 0.14.1)
pry-rails (~> 0.3.9)
puma (~> 5.0) puma (~> 5.0)
rack-mini-profiler (~> 2.0) rack-mini-profiler (~> 2.0)
rails (~> 6.1.4, >= 6.1.4.1) rails (~> 6.1.4, >= 6.1.4.1)
redis
sass-rails (>= 6) sass-rails (>= 6)
selenium-webdriver selenium-webdriver
slim-rails (~> 3.3)
spring spring
sqlite3 (~> 1.4) sqlite3 (~> 1.4)
turbolinks (~> 5) turbolinks (~> 5)
......
// Place all the styles related to the Messages controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: https://sass-lang.com/
// Place all the styles related to the Rooms controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: https://sass-lang.com/
class RoomChannel < ApplicationCable::Channel
def subscribed
stream_from "room_channel_#{params[:room_id]}"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end
class ApplicationController < ActionController::Base class ApplicationController < ActionController::Base
helper_method :current_user
def current_user
if session[:user_id].present?
@current_user = User.find(session[:user_id])
else
@current_user = User.generate
session[:user_id] = @current_user.id
@current_user
end
end
end end
class MessagesController < ApplicationController
before_action :set_message, only: %i[ show edit update destroy ]
# GET /messages or /messages.json
def index
@messages = Message.all
end
# GET /messages/1 or /messages/1.json
def show
end
# GET /messages/new
def new
@message = Message.new
end
# GET /messages/1/edit
def edit
end
# POST /messages or /messages.json
def create
@message = Message.new(message_params)
@message.user = current_user
@message.save
SendMessageJob.perform_later(@message)
end
# PATCH/PUT /messages/1 or /messages/1.json
def update
respond_to do |format|
if @message.update(message_params)
format.html { redirect_to @message, notice: "Message was successfully updated." }
format.json { render :show, status: :ok, location: @message }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @message.errors, status: :unprocessable_entity }
end
end
end
# DELETE /messages/1 or /messages/1.json
def destroy
@message.destroy
respond_to do |format|
format.html { redirect_to messages_url, notice: "Message was successfully destroyed." }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_message
@message = Message.find(params[:id])
end
# Only allow a list of trusted parameters through.
def message_params
params.require(:message).permit(:content, :user_id, :room_id)
end
end
class RoomsController < ApplicationController
before_action :set_room, only: %i[ show edit update destroy ]
# GET /rooms or /rooms.json
def index
@rooms = Room.all
end
# GET /rooms/1 or /rooms/1.json
def show
@rooms = Room.all
render 'index'
end
# GET /rooms/new
def new
@room = Room.new
end
# GET /rooms/1/edit
def edit
end
# POST /rooms or /rooms.json
def create
@room = Room.new(room_params)
respond_to do |format|
if @room.save
format.html { redirect_to @room, notice: "Room was successfully created." }
format.json { render :show, status: :created, location: @room }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @room.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /rooms/1 or /rooms/1.json
def update
respond_to do |format|
if @room.update(room_params)
format.html { redirect_to @room, notice: "Room was successfully updated." }
format.json { render :show, status: :ok, location: @room }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @room.errors, status: :unprocessable_entity }
end
end
end
# DELETE /rooms/1 or /rooms/1.json
def destroy
@room.destroy
respond_to do |format|
format.html { redirect_to rooms_url, notice: "Room was successfully destroyed." }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_room
@room = Room.find(params[:id])
end
# Only allow a list of trusted parameters through.
def room_params
params.require(:room).permit(:name)
end
end
module MessagesHelper
end
module RoomsHelper
end
import consumer from "./consumer"
document.addEventListener('turbolinks:load', () => {
const room_element = document.getElementById('room-id');
const room_id = Number(room_element.getAttribute('data-room-id'));
console.log(consumer.subscriptions)
consumer.subscriptions.subscriptions.forEach((subscription) => {
consumer.subscriptions.remove(subscription)
})
consumer.subscriptions.create({ channel: "RoomChannel", room_id: room_id}, {
connected() {
console.log('connected to ' + room_id)
// 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
console.log(data)
const user_element = document.getElementById('user-id');
const user_id = Number(user_element.getAttribute('data-user-id'));
let html;
if (user_id === data.message.user_id) {
html = data.mine
} else {
html = data.theirs
}
const messageContainer = document.getElementById('messages')
messageContainer.innerHTML = messageContainer.innerHTML + html
}
});
})
...@@ -11,3 +11,5 @@ import "channels" ...@@ -11,3 +11,5 @@ import "channels"
Rails.start() Rails.start()
Turbolinks.start() Turbolinks.start()
ActiveStorage.start() ActiveStorage.start()
import '../stylesheets/application.scss'
\ No newline at end of file
.card-left {
height: 100vh;
padding-right: 0px;
.room-list {
height: calc(100vh - 250px);
overflow-y: scroll;
.card {
border-radius: unset;
border: unset;
border-left: 1px solid rgb(219, 219, 219);
.card-body {
color: black;
&:hover {
text-decoration: none;
background-color: rgba(var(--bb2,239,239,239),0.3);
}
}
}
}
}
.card-right {
padding-left: 0px;
.head {
text-align: center;
}
}
.chat-room {
height: calc(100vh - 150px);
overflow-y: scroll;
padding: 100px 24px;
padding-bottom: 100px;
position: relative;
.message {
min-height: 59px;
.content-container {
display: inline-block;
// max-width: 70%;
.content{
background-color: #eee;
padding: 10px;
border-radius: 18px;
}
.author {
font-size: 0.8rem;
color: #777;
margin-left: 10px;
}
}
&.me {
.content-container {
float: right;
.content {
background-color: #007bff;
color: white;
}
}
}
}
}
.chat-box {
position: absolute;
bottom: 0;
padding: 20px;
width: calc(100% - 15px);
background-color: white;
border-top: 1px solid #eeeeee;
input[type=text] {
height: 45px;
font-size: 18px;
padding: 8px;
}
.btn {
height: 45px;
}
}
\ No newline at end of file
class SendMessageJob < ApplicationJob
queue_as :default
def perform(message)
mine = ApplicationController.render(partial: 'messages/mine', locals: { message: message })
theirs = ApplicationController.render(partial: 'messages/theirs', locals: { message: message })
ActionCable.server.broadcast "room_channel_#{message.room_id}", { mine: mine, theirs: theirs, message: message }
end
end
class Message < ApplicationRecord
belongs_to :user
belongs_to :room
end
class Room < ApplicationRecord
has_many :messages
has_many :users, through: :messages
end
class User < ApplicationRecord
validates_uniqueness_of :username
def self.generate
adjtives = %w[Acient Broken Creative Dangerous Flying Gilded]
nouns = %w[Highway Intern Jackhammer Lion Master]
number = rand.to_s[2..6]
username = "#{adjtives.sample}-#{nouns.sample}-#{number}"
create(username: username)
end
end
...@@ -5,8 +5,8 @@ ...@@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %> <%= csrf_meta_tags %>
<%= csp_meta_tag %> <%= csp_meta_tag %>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> <%= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
</head> </head>
......
= form_with(model: message, local: true) do |f|
- if message.errors.any?
#error_explanation
h2 = "#{pluralize(message.errors.count, 'error')} prohibited this message from being saved:"
ul
- message.errors.full_messages.each do |message|
li = message
= f.hidden_field :room_id, value: room.id
.input-group
= f.text_field :content, placeholder: 'type your message', class: 'form-control'
.input-group-append
= f.submit "Send", class: 'btn btn-primary', data: { disable_with: false }
\ No newline at end of file
div class=("message mb-2 #{'me' if message.user == current_user}")
.content-container
.author
/ - if message.user == current_user
/ = 'me'
/ - else
= message.user.username
.content
= message.content
\ No newline at end of file
json.extract! message, :id, :content, :user_id, :room_id, :created_at, :updated_at
json.url message_url(message, format: :json)
div class=("message mb-2 me")
.content-container
.author
/ - if message.user == current_user
/ = 'me'
/ - else
= message.user.username
.content
= message.content
\ No newline at end of file
div class=("message mb-2")
.content-container
.author
/ - if message.user == current_user
/ = 'me'
/ - else
= message.user.username
.content
= message.content
\ No newline at end of file
h1 Editing message
== render 'form'
=> link_to 'Show', @message
'|
=< link_to 'Back', messages_path
h1 Listing messages
table
thead
tr
th Content
th User
th Room
th
th
th
tbody
- @messages.each do |message|
tr
td = message.content
td = message.user
td = message.room
td = link_to 'Show', message
td = link_to 'Edit', edit_message_path(message)
td = link_to 'Destroy', message, data: { confirm: 'Are you sure?' }, method: :delete
br
= link_to 'New Message', new_message_path
json.array! @messages, partial: "messages/message", as: :message
h1 New message
== render 'form'
= link_to 'Back', messages_path
p#notice = notice
p
strong Content:
= @message.content
p
strong User:
= @message.user
p
strong Room:
= @message.room
=> link_to 'Edit', edit_message_path(@message)
'|
=< link_to 'Back', messages_path
json.partial! "messages/message", message: @message
= form_with(model: room, local: true) do |f|
- if room.errors.any?
#error_explanation
h2 = "#{pluralize(room.errors.count, 'error')} prohibited this room from being saved:"
ul
- room.errors.full_messages.each do |message|
li = message
.field
= f.text_field :name, class: 'form-control'
.actions
= f.submit 'Add room', class: 'btn btn-primary btn-block'
json.extract! room, :id, :name, :created_at, :updated_at
json.url room_url(room, format: :json)
h1 Editing room
== render 'form'
=> link_to 'Show', @room
'|
=< link_to 'Back', rooms_path
#room-id data-room-id="#{@room.try(:id)}"
#user-id data-user-id="#{current_user.id}"
.container
.row
.col-3.card-left
.card
.card-body
b = current_user.username
.room.my-4
p.mb-2 Add a new room
= render 'form', room: Room.new
hr
.room-list
- @rooms.each do |room|
= link_to room
.card
.card-body
b = room.name
br
small = "#{room.users.uniq.count} member"
.col-9.card-right
-if @room.present?
.card.head
.card-body
b = @room.name
.chat-room
#messages
- @room.messages.each do |message|
= render 'messages/message', message: message
.chat-box
= render 'messages/form', message: Message.new, room: @room
/ -binding.pry
\ No newline at end of file
json.array! @rooms, partial: "rooms/room", as: :room
h1 New room
== render 'form'
= link_to 'Back', rooms_path
p#notice = notice
p
strong Name:
= @room.name
=> link_to 'Edit', edit_room_path(@room)
'|
=< link_to 'Back', rooms_path
json.partial! "rooms/room", room: @room
...@@ -8,6 +8,9 @@ Bundler.require(*Rails.groups) ...@@ -8,6 +8,9 @@ Bundler.require(*Rails.groups)
module ChatApp module ChatApp
class Application < Rails::Application class Application < Rails::Application
config.generators do |g|
g.template_engine = :slim
end
# Initialize configuration defaults for originally generated Rails version. # Initialize configuration defaults for originally generated Rails version.
config.load_defaults 6.1 config.load_defaults 6.1
......
development: development:
adapter: async adapter: redis
ulr: redis://localhost:6379/1
test: test:
adapter: test adapter: test
......
Rails.application.routes.draw do Rails.application.routes.draw do
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html resources :messages
resources :rooms
root 'rooms#index'
end end
class CreateRooms < ActiveRecord::Migration[6.1]
def change
create_table :rooms do |t|
t.string :name
t.timestamps
end
end
end
class CreateUsers < ActiveRecord::Migration[6.1]
def change
create_table :users do |t|
t.string :username
t.timestamps
end
end
end
class CreateMessages < ActiveRecord::Migration[6.1]
def change
create_table :messages do |t|
t.text :content
t.references :user, null: false, foreign_key: true
t.references :room, null: false, foreign_key: true
t.timestamps
end
end
end
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# This file is the source Rails uses to define your schema when running `bin/rails
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
# be faster and is potentially less error prone than running all of your
# migrations from scratch. Old migrations may fail to apply correctly if those
# migrations use external dependencies or application code.
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2021_11_09_030317) do
create_table "messages", force: :cascade do |t|
t.text "content"
t.integer "user_id", null: false
t.integer "room_id", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["room_id"], name: "index_messages_on_room_id"
t.index ["user_id"], name: "index_messages_on_user_id"
end
create_table "rooms", force: :cascade do |t|
t.string "name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "users", force: :cascade do |t|
t.string "username"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
add_foreign_key "messages", "rooms"
add_foreign_key "messages", "users"
end
require "test_helper"
class RoomChannelTest < ActionCable::Channel::TestCase
# test "subscribes" do
# subscribe
# assert subscription.confirmed?
# end
end
require "test_helper"
class MessagesControllerTest < ActionDispatch::IntegrationTest
setup do
@message = messages(:one)
end
test "should get index" do
get messages_url
assert_response :success
end
test "should get new" do
get new_message_url
assert_response :success
end
test "should create message" do
assert_difference('Message.count') do
post messages_url, params: { message: { content: @message.content, room_id: @message.room_id, user_id: @message.user_id } }
end
assert_redirected_to message_url(Message.last)
end
test "should show message" do
get message_url(@message)
assert_response :success
end
test "should get edit" do
get edit_message_url(@message)
assert_response :success
end
test "should update message" do
patch message_url(@message), params: { message: { content: @message.content, room_id: @message.room_id, user_id: @message.user_id } }
assert_redirected_to message_url(@message)
end
test "should destroy message" do
assert_difference('Message.count', -1) do
delete message_url(@message)
end
assert_redirected_to messages_url
end
end
require "test_helper"
class RoomsControllerTest < ActionDispatch::IntegrationTest
setup do
@room = rooms(:one)
end
test "should get index" do
get rooms_url
assert_response :success
end
test "should get new" do
get new_room_url
assert_response :success
end
test "should create room" do
assert_difference('Room.count') do
post rooms_url, params: { room: { name: @room.name } }
end
assert_redirected_to room_url(Room.last)
end
test "should show room" do
get room_url(@room)
assert_response :success
end
test "should get edit" do
get edit_room_url(@room)
assert_response :success
end
test "should update room" do
patch room_url(@room), params: { room: { name: @room.name } }
assert_redirected_to room_url(@room)
end
test "should destroy room" do
assert_difference('Room.count', -1) do
delete room_url(@room)
end
assert_redirected_to rooms_url
end
end
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
content: MyText
user: one
room: one
two:
content: MyText
user: two
room: two
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
name: MyString
two:
name: MyString
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
username: MyString
two:
username: MyString
require "test_helper"
class SendMessageJobTest < ActiveJob::TestCase
# test "the truth" do
# assert true
# end
end
require "test_helper"
class MessageTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end
require "test_helper"
class RoomTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end
require "test_helper"
class UserTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end
require "application_system_test_case"
class MessagesTest < ApplicationSystemTestCase
setup do
@message = messages(:one)
end
test "visiting the index" do
visit messages_url
assert_selector "h1", text: "Messages"
end
test "creating a Message" do
visit messages_url
click_on "New Message"
fill_in "Content", with: @message.content
fill_in "Room", with: @message.room_id
fill_in "User", with: @message.user_id
click_on "Create Message"
assert_text "Message was successfully created"
click_on "Back"
end
test "updating a Message" do
visit messages_url
click_on "Edit", match: :first
fill_in "Content", with: @message.content
fill_in "Room", with: @message.room_id
fill_in "User", with: @message.user_id
click_on "Update Message"
assert_text "Message was successfully updated"
click_on "Back"
end
test "destroying a Message" do
visit messages_url
page.accept_confirm do
click_on "Destroy", match: :first
end
assert_text "Message was successfully destroyed"
end
end
require "application_system_test_case"
class RoomsTest < ApplicationSystemTestCase
setup do
@room = rooms(:one)
end
test "visiting the index" do
visit rooms_url
assert_selector "h1", text: "Rooms"
end
test "creating a Room" do
visit rooms_url
click_on "New Room"
fill_in "Name", with: @room.name
click_on "Create Room"
assert_text "Room was successfully created"
click_on "Back"
end
test "updating a Room" do
visit rooms_url
click_on "Edit", match: :first
fill_in "Name", with: @room.name
click_on "Update Room"
assert_text "Room was successfully updated"
click_on "Back"
end
test "destroying a Room" do
visit rooms_url
page.accept_confirm do
click_on "Destroy", match: :first
end
assert_text "Room was successfully destroyed"
end
end
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment