diff --git a/Gemfile b/Gemfile index f5a39a4d9..338c45e1d 100644 --- a/Gemfile +++ b/Gemfile @@ -27,6 +27,7 @@ gem "coffee-rails" # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder gem "jbuilder" +gem 'redis' # bundle exec rake doc:rails generates the API under doc/api. gem "sdoc", group: :doc diff --git a/Gemfile.lock b/Gemfile.lock index 4839f1b35..233be69f4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -206,6 +206,7 @@ GEM foreman rails (>= 3.2) rainbow (~> 2.1) + redis (3.3.0) rspec-core (3.5.4) rspec-support (~> 3.5.0) rspec-expectations (3.5.0) @@ -330,6 +331,7 @@ DEPENDENCIES rails-html-sanitizer rainbow react_on_rails (~> 6.1) + redis rspec-rails (~> 3) rspec-retry rubocop diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb new file mode 100644 index 000000000..d67269728 --- /dev/null +++ b/app/channels/application_cable/channel.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Channel < ActionCable::Channel::Base + end +end diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb new file mode 100644 index 000000000..0ff5442f4 --- /dev/null +++ b/app/channels/application_cable/connection.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Connection < ActionCable::Connection::Base + end +end diff --git a/app/channels/comments_channel.rb b/app/channels/comments_channel.rb new file mode 100644 index 000000000..cf1a1c535 --- /dev/null +++ b/app/channels/comments_channel.rb @@ -0,0 +1,5 @@ +class CommentsChannel < ApplicationCable::Channel + def subscribed + stream_from "comments" + end +end diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 000000000..a009ace51 --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,2 @@ +class ApplicationJob < ActiveJob::Base +end diff --git a/app/jobs/comment_relay_job.rb b/app/jobs/comment_relay_job.rb new file mode 100644 index 000000000..bce0317e5 --- /dev/null +++ b/app/jobs/comment_relay_job.rb @@ -0,0 +1,5 @@ +class CommentRelayJob < ApplicationJob + def perform(comment) + ActionCable.server.broadcast "comments", comment unless comment.destroyed? + end +end diff --git a/app/models/comment.rb b/app/models/comment.rb index c86b86c6b..90faf1e93 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -1,4 +1,4 @@ class Comment < ActiveRecord::Base - validates_presence_of :author - validates_presence_of :text + validates :author, :text, presence: true + after_commit { CommentRelayJob.perform_later(self) } end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 841075b77..ef1b805db 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -22,7 +22,6 @@ <%= react_component "NavigationBarApp" %> -
<%= yield %>
diff --git a/client/app/bundles/comments/actions/commentsActionCreators.js b/client/app/bundles/comments/actions/commentsActionCreators.js index ba87b1b21..b02788a2d 100644 --- a/client/app/bundles/comments/actions/commentsActionCreators.js +++ b/client/app/bundles/comments/actions/commentsActionCreators.js @@ -27,6 +27,13 @@ export function fetchCommentsFailure(error) { }; } +export function messageReceived(comment) { + return { + type: actionTypes.MESSAGE_RECEIVED, + comment, + }; +} + export function submitCommentSuccess(comment) { return { type: actionTypes.SUBMIT_COMMENT_SUCCESS, diff --git a/client/app/bundles/comments/components/CommentBox/CommentBox.jsx b/client/app/bundles/comments/components/CommentBox/CommentBox.jsx index 9ffa6a64a..1c6ad9a50 100644 --- a/client/app/bundles/comments/components/CommentBox/CommentBox.jsx +++ b/client/app/bundles/comments/components/CommentBox/CommentBox.jsx @@ -4,6 +4,8 @@ import React, { PropTypes } from 'react'; import CommentForm from './CommentForm/CommentForm'; import CommentList, { CommentPropTypes } from './CommentList/CommentList'; import css from './CommentBox.scss'; +import Immutable from 'immutable'; +import ActionCable from 'actioncable'; export default class CommentBox extends BaseComponent { static propTypes = { @@ -19,14 +21,39 @@ export default class CommentBox extends BaseComponent { }).isRequired, }; + constructor() { + super(); + _.bindAll(this, [ + 'refreshComments', + ]); + } + + subscribeChannel() { + const { messageReceived } = this.props.actions; + const protocol = window.location.protocol === "https:" ? "wss://" : "ws://" + const cable = ActionCable.createConsumer(protocol+window.location.hostname+":"+window.location.port+"/cable"); + cable.subscriptions.create({channel: "CommentsChannel"}, { + connected: () => { + console.log("connected") + }, + disconnected: () => { + console.log("disconnected") + }, + received: (comment) => { + messageReceived(Immutable.fromJS(comment)); + } + }); + } + componentDidMount() { const { fetchComments } = this.props.actions; fetchComments(); - this.intervalId = setInterval(fetchComments, this.props.pollInterval); + this.subscribeChannel(); } - componentWillUnmount() { - clearInterval(this.intervalId); + refreshComments() { + const { fetchComments } = this.props.actions; + fetchComments(); } render() { @@ -43,6 +70,7 @@ export default class CommentBox extends BaseComponent {

Comments {data.get('isFetching') && 'Loading...'}

+ Refresh

Text supports Github Flavored Markdown. Comments older than 24 hours are deleted.
diff --git a/client/app/bundles/comments/constants/commentsConstants.js b/client/app/bundles/comments/constants/commentsConstants.js index db3c55a87..03749319a 100644 --- a/client/app/bundles/comments/constants/commentsConstants.js +++ b/client/app/bundles/comments/constants/commentsConstants.js @@ -3,6 +3,7 @@ export const FETCH_COMMENTS_FAILURE = 'FETCH_COMMENTS_FAILURE'; export const SUBMIT_COMMENT_SUCCESS = 'SUBMIT_COMMENT_SUCCESS'; export const SUBMIT_COMMENT_FAILURE = 'SUBMIT_COMMENT_FAILURE'; +export const MESSAGE_RECEIVED = 'MESSAGE_RECEIVED'; export const SET_IS_FETCHING = 'SET_IS_FETCHING'; export const SET_IS_SAVING = 'SET_IS_SAVING'; diff --git a/client/app/bundles/comments/reducers/commentsReducer.js b/client/app/bundles/comments/reducers/commentsReducer.js index 91d4a9a06..91cbc5a62 100644 --- a/client/app/bundles/comments/reducers/commentsReducer.js +++ b/client/app/bundles/comments/reducers/commentsReducer.js @@ -32,6 +32,16 @@ export default function commentsReducer($$state = $$initialState, action = null) }); } + case actionTypes.MESSAGE_RECEIVED: { + return $$state.withMutations(state => ( + state + .updateIn( + ['$$comments'], + $$comments => ($$comments.findIndex(com => com.get('id') === comment.get('id')) === -1 ? $$comments.unshift(Immutable.fromJS(comment)) : $$comments), + ) + )); + } + case actionTypes.SUBMIT_COMMENT_SUCCESS: { return $$state.withMutations(state => ( state diff --git a/client/npm-shrinkwrap.json b/client/npm-shrinkwrap.json index 58cce0a98..9db0f9cbd 100644 --- a/client/npm-shrinkwrap.json +++ b/client/npm-shrinkwrap.json @@ -44,6 +44,11 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", "dev": true }, + "actioncable": { + "version": "5.0.1", + "from": "actioncable@latest", + "resolved": "https://registry.npmjs.org/actioncable/-/actioncable-5.0.1.tgz" + }, "ajv": { "version": "4.9.0", "from": "ajv@>=4.7.0 <5.0.0", diff --git a/client/package.json b/client/package.json index d197a1160..253b253c0 100644 --- a/client/package.json +++ b/client/package.json @@ -39,6 +39,7 @@ "lint": "eslint --ext .js,.jsx ." }, "dependencies": { + "actioncable": "^5.0.1", "autoprefixer": "^6.5.3", "axios": "^0.15.2", "babel": "^6.5.2", diff --git a/client/webpack.client.base.config.js b/client/webpack.client.base.config.js index 1a41918c3..6d29af922 100644 --- a/client/webpack.client.base.config.js +++ b/client/webpack.client.base.config.js @@ -27,6 +27,7 @@ module.exports = { // vendor-bundle.js. Note, if we added some library here, but don't use it in the // app-bundle.js, then we just wasted a bunch of space. 'axios', + 'actioncable', 'classnames', 'immutable', 'lodash', diff --git a/config/application.rb b/config/application.rb index 57d8053f1..c0d8ea141 100644 --- a/config/application.rb +++ b/config/application.rb @@ -11,5 +11,6 @@ class Application < Rails::Application # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. + config.action_cable.allowed_request_origins = [Rails.application.secrets.action_cable_url] end end diff --git a/config/cable.yml b/config/cable.yml index aa4e83274..c29230ed7 100644 --- a/config/cable.yml +++ b/config/cable.yml @@ -1,10 +1,11 @@ # Action Cable uses Redis by default to administer connections, channels, and sending/receiving messages over the WebSocket. production: adapter: redis - url: redis://localhost:6379/1 + url: <%= ENV["REDISCLOUD_URL"] %> development: - adapter: async + adapter: redis + url: redis://localhost:6379/1 test: adapter: async diff --git a/config/database.yml b/config/database.yml index 0662a56ac..c96d90929 100644 --- a/config/database.yml +++ b/config/database.yml @@ -26,21 +26,21 @@ # database: db/production.sqlite3 # Uncomment below for a setup with just postgres and change your Gemfile to reflect this - default: &default - adapter: postgresql - username: - password: +default: &default + adapter: postgresql + username: + password: - development: - <<: *default - database: react_webpack_dev +development: + <<: *default + database: react_webpack_dev # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. - test: - <<: *default - database: react_webpack_test +test: + <<: *default + database: react_webpack_test production: <<: *default diff --git a/config/environments/production.rb b/config/environments/production.rb index c146c7dc3..3be9a24fb 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -40,7 +40,6 @@ # Action Cable endpoint configuration # config.action_cable.url = 'wss://example.com/cable' - # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. config.force_ssl = true diff --git a/config/routes.rb b/config/routes.rb index 1ceda4475..83bc46408 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -13,4 +13,5 @@ get "react-router(/*all)", to: "pages#index" resources :comments + mount ActionCable.server => "/cable" end diff --git a/config/secrets.yml b/config/secrets.yml index bad5014a8..3a014e1f2 100644 --- a/config/secrets.yml +++ b/config/secrets.yml @@ -12,11 +12,14 @@ development: secret_key_base: 231bf79489c63f8c8facd7bf27db1c2582a42a7f4302fccdb74ef35bc5dc91fb4e19dbf167f3003bdb4073818dfab4a9916890d193d535a7be458dbef1609800 + action_cable_url : http://localhost:3000 test: secret_key_base: 1ab8adbcf8410aebbce9b6dd6db7b5d090297bd22cf789b91ff44ae02711e8c128453d3e5c97eadf9066efe1a1e0dc1921faf7314d566c114d3ed60ae7ea614c + action_cable_url : http://localhost:3000 # Do not keep production secrets in the repository, # instead read values from the environment. production: secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> + action_cable_url : <%= ENV["SERVER_PORT"] %>