Live timelines using ActionCable
This commit is contained in:
		
							
								
								
									
										3
									
								
								Gemfile
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								Gemfile
									
									
									
									
									
								
							| @@ -35,7 +35,6 @@ gem 'onebox' | ||||
| gem 'simple_form' | ||||
| gem 'will_paginate' | ||||
| gem 'rack-attack' | ||||
| gem 'turbolinks' | ||||
| gem 'sidekiq' | ||||
| gem 'sinatra', require: nil, github: 'sinatra' | ||||
|  | ||||
| @@ -66,5 +65,5 @@ group :production do | ||||
| end | ||||
|  | ||||
| group :development, :production do | ||||
|   gem 'rack-mini-profiler', require: false | ||||
|   gem 'rack-mini-profiler' | ||||
| end | ||||
|   | ||||
| @@ -321,9 +321,6 @@ GEM | ||||
|     thread_safe (0.3.5) | ||||
|     tilt (2.0.5) | ||||
|     tool (0.2.3) | ||||
|     turbolinks (5.0.1) | ||||
|       turbolinks-source (~> 5) | ||||
|     turbolinks-source (5.0.0) | ||||
|     tzinfo (1.2.2) | ||||
|       thread_safe (~> 0.1) | ||||
|     uglifier (3.0.1) | ||||
| @@ -394,7 +391,6 @@ DEPENDENCIES | ||||
|   simplecov | ||||
|   sinatra! | ||||
|   therubyracer | ||||
|   turbolinks | ||||
|   uglifier (>= 1.3.0) | ||||
|   webmock | ||||
|   will_paginate | ||||
|   | ||||
| @@ -1,3 +0,0 @@ | ||||
| # Place all the behaviors and hooks related to the matching controller here. | ||||
| # All this logic will automatically be available in application.js. | ||||
| # You can use CoffeeScript in this file: http://coffeescript.org/ | ||||
| @@ -1,3 +0,0 @@ | ||||
| # Place all the behaviors and hooks related to the matching controller here. | ||||
| # All this logic will automatically be available in application.js. | ||||
| # You can use CoffeeScript in this file: http://coffeescript.org/ | ||||
| @@ -1,3 +0,0 @@ | ||||
| # Place all the behaviors and hooks related to the matching controller here. | ||||
| # All this logic will automatically be available in application.js. | ||||
| # You can use CoffeeScript in this file: http://coffeescript.org/ | ||||
| @@ -1,3 +0,0 @@ | ||||
| # Place all the behaviors and hooks related to the matching controller here. | ||||
| # All this logic will automatically be available in application.js. | ||||
| # You can use CoffeeScript in this file: http://coffeescript.org/ | ||||
| @@ -12,5 +12,4 @@ | ||||
| // | ||||
| //= require jquery | ||||
| //= require jquery_ujs | ||||
| //= require turbolinks | ||||
| //= require_tree . | ||||
|   | ||||
							
								
								
									
										13
									
								
								app/assets/javascripts/cable.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								app/assets/javascripts/cable.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| // Action Cable provides the framework to deal with WebSockets in Rails. | ||||
| // You can generate new channels where WebSocket features live using the rails generate channel command. | ||||
| // | ||||
| //= require action_cable | ||||
| //= require_self | ||||
| //= require_tree ./channels | ||||
|  | ||||
| (function() { | ||||
|   this.App || (this.App = {}); | ||||
|  | ||||
|   App.cable = ActionCable.createConsumer(); | ||||
|  | ||||
| }).call(this); | ||||
							
								
								
									
										13
									
								
								app/assets/javascripts/channels/timeline.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								app/assets/javascripts/channels/timeline.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| App.timeline = App.cable.subscriptions.create("TimelineChannel", { | ||||
|   connected: function() { | ||||
|     console.log('Connected'); | ||||
|   }, | ||||
|  | ||||
|   disconnected: function() { | ||||
|     console.log('Disconnected'); | ||||
|   }, | ||||
|  | ||||
|   received: function(data) { | ||||
|     console.log(JSON.parse(data.message)); | ||||
|   } | ||||
| }); | ||||
| @@ -1,3 +0,0 @@ | ||||
| # Place all the behaviors and hooks related to the matching controller here. | ||||
| # All this logic will automatically be available in application.js. | ||||
| # You can use CoffeeScript in this file: http://coffeescript.org/ | ||||
| @@ -1,5 +0,0 @@ | ||||
| $ -> | ||||
|   $(document).on 'turbolinks:load', -> | ||||
|     unless typeof window.MiniProfiler == 'undefined' | ||||
|       window.MiniProfiler.init() | ||||
|       window.MiniProfiler.pageTransition() | ||||
| @@ -1,3 +0,0 @@ | ||||
| # Place all the behaviors and hooks related to the matching controller here. | ||||
| # All this logic will automatically be available in application.js. | ||||
| # You can use CoffeeScript in this file: http://coffeescript.org/ | ||||
| @@ -1,3 +0,0 @@ | ||||
| # Place all the behaviors and hooks related to the matching controller here. | ||||
| # All this logic will automatically be available in application.js. | ||||
| # You can use CoffeeScript in this file: http://coffeescript.org/ | ||||
							
								
								
									
										5
									
								
								app/channels/application_cable/channel.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/channels/application_cable/channel.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| # Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading. | ||||
| module ApplicationCable | ||||
|   class Channel < ActionCable::Channel::Base | ||||
|   end | ||||
| end | ||||
							
								
								
									
										20
									
								
								app/channels/application_cable/connection.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								app/channels/application_cable/connection.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| # Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading. | ||||
| module ApplicationCable | ||||
|   class Connection < ActionCable::Connection::Base | ||||
|     identified_by :current_user | ||||
|  | ||||
|     def connect | ||||
|       self.current_user = find_verified_user | ||||
|     end | ||||
|  | ||||
|     protected | ||||
|  | ||||
|     def find_verified_user | ||||
|       if verified_user = env['warden'].user | ||||
|         verified_user | ||||
|       else | ||||
|         reject_unauthorized_connection | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
							
								
								
									
										10
									
								
								app/channels/timeline_channel.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								app/channels/timeline_channel.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| # Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading. | ||||
| class TimelineChannel < ApplicationCable::Channel | ||||
|   def subscribed | ||||
|     stream_from "timeline:#{current_user.id}" | ||||
|   end | ||||
|  | ||||
|   def unsubscribed | ||||
|     # Any cleanup needed when channel is unsubscribed | ||||
|   end | ||||
| end | ||||
| @@ -5,7 +5,7 @@ class ApplicationController < ActionController::Base | ||||
|  | ||||
|   # Profiling | ||||
|   before_action do | ||||
|     if current_user && current_user.admin? | ||||
|     if (current_user && current_user.admin?) || Rails.env == 'development' | ||||
|       Rack::MiniProfiler.authorize_request | ||||
|     end | ||||
|   end | ||||
|   | ||||
| @@ -10,13 +10,13 @@ class FanOutOnWriteService < BaseService | ||||
|   private | ||||
|  | ||||
|   def deliver_to_self(status) | ||||
|     push(:home, status.account.id, status) | ||||
|     push(:home, status.account, status) | ||||
|   end | ||||
|  | ||||
|   def deliver_to_followers(status) | ||||
|     status.account.followers.each do |follower| | ||||
|       next if !follower.local? || FeedManager.filter_status?(status, follower) | ||||
|       push(:home, follower.id, status) | ||||
|       push(:home, follower, status) | ||||
|     end | ||||
|   end | ||||
|  | ||||
| @@ -24,23 +24,38 @@ class FanOutOnWriteService < BaseService | ||||
|     status.mentions.each do |mention| | ||||
|       mentioned_account = mention.account | ||||
|       next unless mentioned_account.local? | ||||
|       push(:mentions, mentioned_account.id, status) | ||||
|       push(:mentions, mentioned_account, status) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def push(type, receiver_id, status) | ||||
|     redis.zadd(FeedManager.key(type, receiver_id), status.id, status.id) | ||||
|     trim(type, receiver_id) | ||||
|   def push(type, receiver, status) | ||||
|     redis.zadd(FeedManager.key(type, receiver.id), status.id, status.id) | ||||
|     trim(type, receiver) | ||||
|     ActionCable.server.broadcast("timeline:#{receiver.id}", message: inline_render(receiver, status)) | ||||
|   end | ||||
|  | ||||
|   def trim(type, receiver_id) | ||||
|     return unless redis.zcard(FeedManager.key(type, receiver_id)) > FeedManager::MAX_ITEMS | ||||
|   def trim(type, receiver) | ||||
|     return unless redis.zcard(FeedManager.key(type, receiver.id)) > FeedManager::MAX_ITEMS | ||||
|  | ||||
|     last = redis.zrevrange(FeedManager.key(type, receiver_id), FeedManager::MAX_ITEMS - 1, FeedManager::MAX_ITEMS - 1) | ||||
|     redis.zremrangebyscore(FeedManager.key(type, receiver_id), '-inf', "(#{last.last}") | ||||
|     last = redis.zrevrange(FeedManager.key(type, receiver.id), FeedManager::MAX_ITEMS - 1, FeedManager::MAX_ITEMS - 1) | ||||
|     redis.zremrangebyscore(FeedManager.key(type, receiver.id), '-inf', "(#{last.last}") | ||||
|   end | ||||
|  | ||||
|   def redis | ||||
|     $redis | ||||
|   end | ||||
|  | ||||
|   def inline_render(receiver, status) | ||||
|     rabl_scope = Class.new(BaseService) do | ||||
|       def initialize(account) | ||||
|         @account = account | ||||
|       end | ||||
|  | ||||
|       def current_user | ||||
|         @account.user | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     Rabl::Renderer.new('api/statuses/show', status,  view_path: 'app/views', format: :json, scope: rabl_scope.new(receiver)).render | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -8,7 +8,7 @@ class PrecomputeFeedService < BaseService | ||||
|  | ||||
|     Status.send("as_#{type}_timeline", account).order('created_at desc').limit(FeedManager::MAX_ITEMS).each do |status| | ||||
|       next if type == :home && FeedManager.filter_status?(status, account) | ||||
|       redis.zadd(FeedManager.key(type, receiver_id), status.id, status.id) | ||||
|       redis.zadd(FeedManager.key(type, account.id), status.id, status.id) | ||||
|       instant_return << status unless instant_return.size > limit | ||||
|     end | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| development: | ||||
|   adapter: async | ||||
|   adapter: redis | ||||
|   url: redis://localhost:6379/1 | ||||
|  | ||||
| test: | ||||
|   adapter: async | ||||
|   | ||||
| @@ -64,3 +64,6 @@ Rails.application.configure do | ||||
|     Bullet.rails_logger = true | ||||
|   end | ||||
| end | ||||
|  | ||||
| require 'sidekiq/testing' | ||||
| Sidekiq::Testing.inline! | ||||
|   | ||||
| @@ -8,4 +8,4 @@ Rails.application.config.assets.version = '1.0' | ||||
|  | ||||
| # Precompile additional assets. | ||||
| # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. | ||||
| # Rails.application.config.assets.precompile += %w( search.js ) | ||||
| Rails.application.config.assets.precompile += %w( cable.js ) | ||||
|   | ||||
| @@ -1,6 +1,2 @@ | ||||
| require 'rack-mini-profiler' | ||||
|  | ||||
| Rack::MiniProfilerRails.initialize!(Rails.application) | ||||
|  | ||||
| Rails.application.middleware.delete(Rack::MiniProfiler) | ||||
| Rails.application.middleware.insert_after(Rack::Deflater, Rack::MiniProfiler) | ||||
| Rails.application.middleware.swap(Rack::Deflater, Rack::MiniProfiler) | ||||
| Rails.application.middleware.swap(Rack::MiniProfiler, Rack::Deflater) | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| require 'sidekiq/web' | ||||
|  | ||||
| Rails.application.routes.draw do | ||||
|   mount ActionCable.server => '/cable' | ||||
|  | ||||
|   authenticate :user, lambda { |u| u.admin? } do | ||||
|     mount Sidekiq::Web => '/sidekiq' | ||||
|   end | ||||
|   | ||||
		Reference in New Issue
	
	Block a user