forked from treehouse/mastodon
Live timelines using ActionCable
parent
10ba09f546
commit
6deb9f966e
3
Gemfile
3
Gemfile
|
@ -35,7 +35,6 @@ gem 'onebox'
|
||||||
gem 'simple_form'
|
gem 'simple_form'
|
||||||
gem 'will_paginate'
|
gem 'will_paginate'
|
||||||
gem 'rack-attack'
|
gem 'rack-attack'
|
||||||
gem 'turbolinks'
|
|
||||||
gem 'sidekiq'
|
gem 'sidekiq'
|
||||||
gem 'sinatra', require: nil, github: 'sinatra'
|
gem 'sinatra', require: nil, github: 'sinatra'
|
||||||
|
|
||||||
|
@ -66,5 +65,5 @@ group :production do
|
||||||
end
|
end
|
||||||
|
|
||||||
group :development, :production do
|
group :development, :production do
|
||||||
gem 'rack-mini-profiler', require: false
|
gem 'rack-mini-profiler'
|
||||||
end
|
end
|
||||||
|
|
|
@ -321,9 +321,6 @@ GEM
|
||||||
thread_safe (0.3.5)
|
thread_safe (0.3.5)
|
||||||
tilt (2.0.5)
|
tilt (2.0.5)
|
||||||
tool (0.2.3)
|
tool (0.2.3)
|
||||||
turbolinks (5.0.1)
|
|
||||||
turbolinks-source (~> 5)
|
|
||||||
turbolinks-source (5.0.0)
|
|
||||||
tzinfo (1.2.2)
|
tzinfo (1.2.2)
|
||||||
thread_safe (~> 0.1)
|
thread_safe (~> 0.1)
|
||||||
uglifier (3.0.1)
|
uglifier (3.0.1)
|
||||||
|
@ -394,7 +391,6 @@ DEPENDENCIES
|
||||||
simplecov
|
simplecov
|
||||||
sinatra!
|
sinatra!
|
||||||
therubyracer
|
therubyracer
|
||||||
turbolinks
|
|
||||||
uglifier (>= 1.3.0)
|
uglifier (>= 1.3.0)
|
||||||
webmock
|
webmock
|
||||||
will_paginate
|
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
|
||||||
//= require jquery_ujs
|
//= require jquery_ujs
|
||||||
//= require turbolinks
|
|
||||||
//= require_tree .
|
//= require_tree .
|
||||||
|
|
|
@ -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);
|
|
@ -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/
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
# Profiling
|
||||||
before_action do
|
before_action do
|
||||||
if current_user && current_user.admin?
|
if (current_user && current_user.admin?) || Rails.env == 'development'
|
||||||
Rack::MiniProfiler.authorize_request
|
Rack::MiniProfiler.authorize_request
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,13 +10,13 @@ class FanOutOnWriteService < BaseService
|
||||||
private
|
private
|
||||||
|
|
||||||
def deliver_to_self(status)
|
def deliver_to_self(status)
|
||||||
push(:home, status.account.id, status)
|
push(:home, status.account, status)
|
||||||
end
|
end
|
||||||
|
|
||||||
def deliver_to_followers(status)
|
def deliver_to_followers(status)
|
||||||
status.account.followers.each do |follower|
|
status.account.followers.each do |follower|
|
||||||
next if !follower.local? || FeedManager.filter_status?(status, follower)
|
next if !follower.local? || FeedManager.filter_status?(status, follower)
|
||||||
push(:home, follower.id, status)
|
push(:home, follower, status)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -24,23 +24,38 @@ class FanOutOnWriteService < BaseService
|
||||||
status.mentions.each do |mention|
|
status.mentions.each do |mention|
|
||||||
mentioned_account = mention.account
|
mentioned_account = mention.account
|
||||||
next unless mentioned_account.local?
|
next unless mentioned_account.local?
|
||||||
push(:mentions, mentioned_account.id, status)
|
push(:mentions, mentioned_account, status)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def push(type, receiver_id, status)
|
def push(type, receiver, status)
|
||||||
redis.zadd(FeedManager.key(type, receiver_id), status.id, status.id)
|
redis.zadd(FeedManager.key(type, receiver.id), status.id, status.id)
|
||||||
trim(type, receiver_id)
|
trim(type, receiver)
|
||||||
|
ActionCable.server.broadcast("timeline:#{receiver.id}", message: inline_render(receiver, status))
|
||||||
end
|
end
|
||||||
|
|
||||||
def trim(type, receiver_id)
|
def trim(type, receiver)
|
||||||
return unless redis.zcard(FeedManager.key(type, receiver_id)) > FeedManager::MAX_ITEMS
|
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)
|
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}")
|
redis.zremrangebyscore(FeedManager.key(type, receiver.id), '-inf', "(#{last.last}")
|
||||||
end
|
end
|
||||||
|
|
||||||
def redis
|
def redis
|
||||||
$redis
|
$redis
|
||||||
end
|
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
|
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|
|
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)
|
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
|
instant_return << status unless instant_return.size > limit
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
development:
|
development:
|
||||||
adapter: async
|
adapter: redis
|
||||||
|
url: redis://localhost:6379/1
|
||||||
|
|
||||||
test:
|
test:
|
||||||
adapter: async
|
adapter: async
|
||||||
|
|
|
@ -64,3 +64,6 @@ Rails.application.configure do
|
||||||
Bullet.rails_logger = true
|
Bullet.rails_logger = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
require 'sidekiq/testing'
|
||||||
|
Sidekiq::Testing.inline!
|
||||||
|
|
|
@ -8,4 +8,4 @@ Rails.application.config.assets.version = '1.0'
|
||||||
|
|
||||||
# Precompile additional assets.
|
# Precompile additional assets.
|
||||||
# application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
|
# 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'
|
Rails.application.middleware.swap(Rack::Deflater, Rack::MiniProfiler)
|
||||||
|
Rails.application.middleware.swap(Rack::MiniProfiler, Rack::Deflater)
|
||||||
Rack::MiniProfilerRails.initialize!(Rails.application)
|
|
||||||
|
|
||||||
Rails.application.middleware.delete(Rack::MiniProfiler)
|
|
||||||
Rails.application.middleware.insert_after(Rack::Deflater, Rack::MiniProfiler)
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
require 'sidekiq/web'
|
require 'sidekiq/web'
|
||||||
|
|
||||||
Rails.application.routes.draw do
|
Rails.application.routes.draw do
|
||||||
|
mount ActionCable.server => '/cable'
|
||||||
|
|
||||||
authenticate :user, lambda { |u| u.admin? } do
|
authenticate :user, lambda { |u| u.admin? } do
|
||||||
mount Sidekiq::Web => '/sidekiq'
|
mount Sidekiq::Web => '/sidekiq'
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue