Skip to content

Commit

Permalink
Merge pull request #26 from sad16/11-action-cable
Browse files Browse the repository at this point in the history
action cable
  • Loading branch information
sad16 committed Jun 18, 2021
2 parents 959a0d2 + 0f16060 commit 38be6d8
Show file tree
Hide file tree
Showing 50 changed files with 690 additions and 23 deletions.
4 changes: 2 additions & 2 deletions Gemfile
Expand Up @@ -43,6 +43,7 @@ gem 'jquery-rails'
gem 'aws-sdk-s3', require: false
gem 'validate_url'
gem 'cocoon'
gem 'gon'

group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
Expand All @@ -66,8 +67,7 @@ group :test do
# Adds support for Capybara system testing and selenium driver
gem 'capybara', '>= 2.15'
gem 'selenium-webdriver'
# Easy installation and use of chromedriver to run system tests with Chrome
gem 'chromedriver-helper'
gem 'webdrivers', '~> 4.0', require: false

gem 'shoulda-matchers'
gem 'rails-controller-testing'
Expand Down
20 changes: 13 additions & 7 deletions Gemfile.lock
Expand Up @@ -44,8 +44,6 @@ GEM
tzinfo (~> 1.1)
addressable (2.6.0)
public_suffix (>= 2.0.2, < 4.0)
archive-zip (0.12.0)
io-like (~> 0.3.0)
arel (9.0.0)
aws-eventstream (1.1.1)
aws-partitions (1.439.0)
Expand Down Expand Up @@ -79,9 +77,6 @@ GEM
xpath (~> 3.2)
childprocess (0.9.0)
ffi (~> 1.0, >= 1.0.11)
chromedriver-helper (2.1.0)
archive-zip (~> 0.10)
nokogiri (~> 1.8)
cocoon (1.2.15)
coderay (1.1.3)
coffee-rails (4.2.2)
Expand Down Expand Up @@ -111,9 +106,13 @@ GEM
ffi (1.10.0)
globalid (0.4.2)
activesupport (>= 4.2.0)
gon (6.4.0)
actionpack (>= 3.0.20)
i18n (>= 0.7)
multi_json
request_store (>= 1.0)
i18n (1.6.0)
concurrent-ruby (~> 1.0)
io-like (0.3.0)
jbuilder (2.8.0)
activesupport (>= 4.2.0)
multi_json (>= 1.2)
Expand Down Expand Up @@ -197,6 +196,8 @@ GEM
rb-inotify (0.10.0)
ffi (~> 1.0)
regexp_parser (1.3.0)
request_store (1.5.0)
rack (>= 1.4)
responders (3.0.1)
actionpack (>= 5.0)
railties (>= 5.0)
Expand Down Expand Up @@ -280,6 +281,10 @@ GEM
activemodel (>= 5.0)
bindex (>= 0.4.0)
railties (>= 5.0)
webdrivers (4.1.2)
nokogiri (~> 1.6)
rubyzip (~> 1.0)
selenium-webdriver (>= 3.0, < 4.0)
websocket-driver (0.7.0)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.3)
Expand All @@ -296,11 +301,11 @@ DEPENDENCIES
bootsnap (>= 1.1.0)
byebug
capybara (>= 2.15)
chromedriver-helper
cocoon
coffee-rails (~> 4.2)
devise
factory_bot_rails
gon
jbuilder (~> 2.5)
jquery-rails
launchy
Expand All @@ -323,6 +328,7 @@ DEPENDENCIES
uglifier (>= 1.3.0)
validate_url
web-console (>= 3.3.0)
webdrivers (~> 4.0)
with_model

RUBY VERSION
Expand Down
27 changes: 25 additions & 2 deletions app/assets/javascripts/answers.js
@@ -1,8 +1,31 @@
$(document).on('turbolinks:load', function(){
$('.answers').on('click', '.edit-answer-link', function(e) {
$(document).on('turbolinks:load', function() {
var answerList = $('.answers');

answerList.on('click', '.edit-answer-link', function(e) {
e.preventDefault();
$(this).addClass('hidden');
var answerId = $(this).data('answerId');
$('form#edit-answer-form-' + answerId).removeClass('hidden');
})

App.cable.subscriptions.create('AnswersChannel', {
connected() {
if (gon.question_id) {
this.perform('follow', { question_id: gon.question_id })
}
},
received(answer) {
if (answer.author_id !== gon.current_user_id) {
var answer_template = answer.templates.answer;
answerList.append(answer_template);

if (gon.current_user_id) {
$(`#answer-id-${answer.id} .vote-actions`).append(answer.templates.vote_links);
onAjaxSuccessVoteLink($(`#answer-id-${answer.id} .vote-actions .vote-link`));
}

loadGists();
}
}
});
});
62 changes: 62 additions & 0 deletions app/assets/javascripts/comments.js
@@ -0,0 +1,62 @@
$(document).on('turbolinks:load', function() {
onAjaxCommentForm($('.new-comment-form form'));

App.cable.subscriptions.create('CommentsChannel', {
connected() {
if (gon.question_id) {
this.perform('follow', { question_id: gon.question_id })
}
},
received(comment) {
if (comment.author_id !== gon.current_user_id) {
var comment_template = comment.template;

if (comment.answer_id) {
$(`#answer-id-${comment.answer_id}`).find('.answer-comments').append(comment_template);
} else {
$('.question-comments').append(comment_template);
}
}
}
});
});

function onAjaxCommentForm(elem) {
$(elem)
.on('ajax:success', function(response) {
clearCommentError(response.currentTarget);
clearCommentForm(response.currentTarget);
showComment(response.detail[0]);
})
.on('ajax:error', function(response) {
clearCommentError(response.currentTarget);

var error_block = $(response.currentTarget).closest('.new-comment-form').find('.comment-errors');
$.each(response.detail[0].errors, function(index, error) {
error_block.append(`<p>${error}</p>`)
});
})
}

function clearCommentError(form) {
var error_block = $(form).closest('.new-comment-form').find('.comment-errors');
error_block.html('');
}

function clearCommentForm(form) {
var inputs = $(form).find('input')
inputs.val('');
}

function showComment(comment) {
var template = `<div className="comment" id="comment-id-${comment.id}"><p>${comment.text}</p></div>`

switch (comment.commentable_type) {
case 'Question':
$('.question-comments').append(template);
break;
case 'Answer':
$(`#answer-id-${comment.commentable_id}`).find('.answer-comments').append(template);
break;
}
}
3 changes: 2 additions & 1 deletion app/assets/javascripts/load_gists.js
Expand Up @@ -6,13 +6,14 @@ function loadGists() {
$.each($('.gist'), function(index, value) {
$(value).removeClass('gist');
var gistId = $(value).data('gistId');
var linkId = $(value).data('linkId');

$.ajax({
url: `https://api.github.com/gists/${gistId}`,
type: 'GET',
success: function(result) {
$.each(result.files, function(index, file) {
$(`#${gistId}`).append(`<p>${file.filename}</p><p>${file.content}</p>`)
$(`#${linkId}_${gistId}`).append(`<p>${file.filename}</p><p>${file.content}</p>`)
});
},
error: function(error) {
Expand Down
12 changes: 12 additions & 0 deletions app/assets/javascripts/questions.js
Expand Up @@ -5,4 +5,16 @@ $(document).on('turbolinks:load', function(){
var questionId = $(this).data('questionId');
$('form#edit-question-form-' + questionId).removeClass('hidden');
})

var questionsList = $('.questions-list');

App.cable.subscriptions.create('QuestionsChannel', {
connected() {
this.perform('follow')
},
received(question) {
var template = question.template
questionsList.append(template)
}
});
});
5 changes: 5 additions & 0 deletions app/channels/answers_channel.rb
@@ -0,0 +1,5 @@
class AnswersChannel < ApplicationCable::Channel
def follow(params)
stream_from "questions/#{params['question_id']}/answers"
end
end
5 changes: 5 additions & 0 deletions app/channels/comments_channel.rb
@@ -0,0 +1,5 @@
class CommentsChannel < ApplicationCable::Channel
def follow(params)
stream_from "questions/#{params['question_id']}/comments"
end
end
5 changes: 5 additions & 0 deletions app/channels/questions_channel.rb
@@ -0,0 +1,5 @@
class QuestionsChannel < ApplicationCable::Channel
def follow
stream_from 'questions'
end
end
24 changes: 24 additions & 0 deletions app/controllers/answers_controller.rb
Expand Up @@ -3,6 +3,8 @@ class AnswersController < ApplicationController
before_action :find_question, only: [:create]
before_action :find_answer, only: [:update, :destroy, :mark_as_best]

after_action :publish_answer, only: [:create]

def create
@answer = @question.answers.new(answer_params)
@answer.user = current_user
Expand Down Expand Up @@ -52,4 +54,26 @@ def find_question
def find_answer
@answer = Answer.with_attached_files.find(params[:id])
end

def publish_answer
return if @answer.errors.present?

ActionCable.server.broadcast(
"questions/#{@answer.question_id}/answers",
{
id: @answer.id,
author_id: @answer.user_id,
templates: {
answer: render_template(
partial: 'websockets/answers/answer',
locals: { answer: @answer }
),
vote_links: render_template(
partial: 'websockets/shared/votes/vote_links',
locals: { voteable: @answer, vote: @answer.votes.new }
)
}
}
)
end
end
6 changes: 6 additions & 0 deletions app/controllers/application_controller.rb
@@ -1,3 +1,9 @@
class ApplicationController < ActionController::Base
include ApplicationHelper

private

def render_template(options)
ApplicationController.render(options)
end
end
42 changes: 42 additions & 0 deletions app/controllers/comments_controller.rb
@@ -0,0 +1,42 @@
class CommentsController < ApplicationController
before_action :authenticate_user!
before_action :set_commentable, only: :create

after_action :publish_comment, only: [:create]

def create
@comment = @commentable.comments.new(user: current_user, text: comment_params[:text])

if @comment.save
render json: @comment
else
render json: { errors: @comment.errors.full_messages }, status: :unprocessable_entity
end
end

private

def set_commentable
@commentable = params[:commentable_type].classify.constantize.find(params[:commentable_id])
end

def comment_params
params.require(:comment)
end

def publish_comment
return if @comment.errors.present?

ActionCable.server.broadcast(
@comment.ws_stream_name,
{
author_id: @comment.user_id,
answer_id: @comment.commentable_type == 'Answer' ? @comment.commentable_id : nil,
template: render_template(
partial: 'websockets/comments/comment',
locals: { comment: @comment }
)
}
)
end
end
23 changes: 23 additions & 0 deletions app/controllers/questions_controller.rb
Expand Up @@ -2,6 +2,8 @@ class QuestionsController < ApplicationController
before_action :authenticate_user!, only: [:new, :create, :update, :destroy]
before_action :find_question, only: [:show, :update, :destroy]

after_action :publish_question, only: [:create]

def index
@questions = Question.all
end
Expand All @@ -10,6 +12,7 @@ def show
@answer = @question.answers.new
@answer.links.new
@vote = current_user&.vote_by(@question) || @question.votes.new
set_gon
end

def new
Expand Down Expand Up @@ -56,4 +59,24 @@ def question_params
def find_question
@question = Question.with_attached_files.find(params[:id])
end

def set_gon
gon.question_id = @question.id
gon.current_user_id = current_user&.id
end

def publish_question
return if @question.errors.present?

ActionCable.server.broadcast(
'questions',
{
template: render_template(
partial: 'websockets/questions/list_item',
locals: { question: @question }
)
}
)
end
end

1 change: 1 addition & 0 deletions app/models/answer.rb
@@ -1,6 +1,7 @@
class Answer < ApplicationRecord
include Linkable
include Voteable
include Commentable

belongs_to :user
belongs_to :question
Expand Down
14 changes: 14 additions & 0 deletions app/models/comment.rb
@@ -0,0 +1,14 @@
class Comment < ApplicationRecord
belongs_to :user
belongs_to :commentable, polymorphic: true

validates :text, presence: true

def ws_stream_name
case commentable_type
when 'Question' then "questions/#{commentable_id}/comments"
when 'Answer' then "questions/#{commentable.question_id}/comments"
else nil
end
end
end

0 comments on commit 38be6d8

Please sign in to comment.