Cleaning up format of broadcast real-time messages, removing
redis-backed "mentions" timeline as redundant (given notifications)
This commit is contained in:
		
							parent
							
								
									1da0ce5c7c
								
							
						
					
					
						commit
						d9ca46b464
					
				
					 17 changed files with 26 additions and 110 deletions
				
			
		| 
						 | 
				
			
			@ -23,7 +23,6 @@ import GettingStarted from '../features/getting_started';
 | 
			
		|||
import PublicTimeline from '../features/public_timeline';
 | 
			
		||||
import AccountTimeline from '../features/account_timeline';
 | 
			
		||||
import HomeTimeline from '../features/home_timeline';
 | 
			
		||||
import MentionsTimeline from '../features/mentions_timeline';
 | 
			
		||||
import Compose from '../features/compose';
 | 
			
		||||
import Followers from '../features/followers';
 | 
			
		||||
import Following from '../features/following';
 | 
			
		||||
| 
						 | 
				
			
			@ -68,15 +67,15 @@ const Mastodon = React.createClass({
 | 
			
		|||
      this.subscription = App.cable.subscriptions.create('TimelineChannel', {
 | 
			
		||||
 | 
			
		||||
        received (data) {
 | 
			
		||||
          switch(data.type) {
 | 
			
		||||
          switch(data.event) {
 | 
			
		||||
          case 'update':
 | 
			
		||||
            store.dispatch(updateTimeline(data.timeline, JSON.parse(data.message)));
 | 
			
		||||
            store.dispatch(updateTimeline('home', JSON.parse(data.payload)));
 | 
			
		||||
            break;
 | 
			
		||||
          case 'delete':
 | 
			
		||||
            store.dispatch(deleteFromTimelines(data.id));
 | 
			
		||||
            store.dispatch(deleteFromTimelines(data.payload));
 | 
			
		||||
            break;
 | 
			
		||||
          case 'notification':
 | 
			
		||||
            store.dispatch(updateNotifications(JSON.parse(data.message), getMessagesForLocale(locale), locale));
 | 
			
		||||
            store.dispatch(updateNotifications(JSON.parse(data.payload), getMessagesForLocale(locale), locale));
 | 
			
		||||
            break;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			@ -108,7 +107,6 @@ const Mastodon = React.createClass({
 | 
			
		|||
 | 
			
		||||
              <Route path='getting-started' component={GettingStarted} />
 | 
			
		||||
              <Route path='timelines/home' component={HomeTimeline} />
 | 
			
		||||
              <Route path='timelines/mentions' component={MentionsTimeline} />
 | 
			
		||||
              <Route path='timelines/public' component={PublicTimeline} />
 | 
			
		||||
              <Route path='timelines/tag/:id' component={HashtagTimeline} />
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,11 +26,13 @@ const HashtagTimeline = React.createClass({
 | 
			
		|||
      }, {
 | 
			
		||||
 | 
			
		||||
        received (data) {
 | 
			
		||||
          switch(data.type) {
 | 
			
		||||
          switch(data.event) {
 | 
			
		||||
          case 'update':
 | 
			
		||||
            return dispatch(updateTimeline('tag', JSON.parse(data.message)));
 | 
			
		||||
            dispatch(updateTimeline('tag', JSON.parse(data.payload)));
 | 
			
		||||
            break;
 | 
			
		||||
          case 'delete':
 | 
			
		||||
            return dispatch(deleteFromTimelines(data.id));
 | 
			
		||||
            dispatch(deleteFromTimelines(data.payload));
 | 
			
		||||
            break;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,36 +0,0 @@
 | 
			
		|||
import { connect }         from 'react-redux';
 | 
			
		||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
 | 
			
		||||
import StatusListContainer from '../ui/containers/status_list_container';
 | 
			
		||||
import Column from '../ui/components/column';
 | 
			
		||||
import { refreshTimeline } from '../../actions/timelines';
 | 
			
		||||
import { defineMessages, injectIntl } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
const messages = defineMessages({
 | 
			
		||||
  title: { id: 'column.mentions', defaultMessage: 'Mentions' }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const MentionsTimeline = React.createClass({
 | 
			
		||||
 | 
			
		||||
  propTypes: {
 | 
			
		||||
    dispatch: React.PropTypes.func.isRequired
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  mixins: [PureRenderMixin],
 | 
			
		||||
 | 
			
		||||
  componentWillMount () {
 | 
			
		||||
    this.props.dispatch(refreshTimeline('mentions'));
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { intl } = this.props;
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <Column icon='at' heading={intl.formatMessage(messages.title)}>
 | 
			
		||||
        <StatusListContainer {...this.props} type='mentions' />
 | 
			
		||||
      </Column>
 | 
			
		||||
    );
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default connect()(injectIntl(MentionsTimeline));
 | 
			
		||||
| 
						 | 
				
			
			@ -32,11 +32,13 @@ const PublicTimeline = React.createClass({
 | 
			
		|||
      this.subscription = App.cable.subscriptions.create('PublicChannel', {
 | 
			
		||||
 | 
			
		||||
        received (data) {
 | 
			
		||||
          switch(data.type) {
 | 
			
		||||
          switch(data.event) {
 | 
			
		||||
          case 'update':
 | 
			
		||||
            return dispatch(updateTimeline('public', JSON.parse(data.message)));
 | 
			
		||||
            dispatch(updateTimeline('public', JSON.parse(data.payload)));
 | 
			
		||||
            break;
 | 
			
		||||
          case 'delete':
 | 
			
		||||
            return dispatch(deleteFromTimelines(data.id));
 | 
			
		||||
            dispatch(deleteFromTimelines(data.payload));
 | 
			
		||||
            break;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,10 +7,10 @@ module ApplicationCable
 | 
			
		|||
    def hydrate_status(encoded_message)
 | 
			
		||||
      message = ActiveSupport::JSON.decode(encoded_message)
 | 
			
		||||
 | 
			
		||||
      return [nil, message] if message['type'] == 'delete'
 | 
			
		||||
      return [nil, message] if message['event'] == 'delete'
 | 
			
		||||
 | 
			
		||||
      status             = Status.find_by(id: message['id'])
 | 
			
		||||
      message['message'] = FeedManager.instance.inline_render(current_user.account, 'api/v1/statuses/show', status)
 | 
			
		||||
      status             = Status.find_by(id: message['payload'])
 | 
			
		||||
      message['payload'] = FeedManager.instance.inline_render(current_user.account, 'api/v1/statuses/show', status)
 | 
			
		||||
 | 
			
		||||
      [status, message]
 | 
			
		||||
    end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,22 +22,6 @@ class Api::V1::TimelinesController < ApiController
 | 
			
		|||
    render action: :index
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def mentions
 | 
			
		||||
    @statuses = Feed.new(:mentions, current_account).get(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id])
 | 
			
		||||
    @statuses = cache_collection(@statuses)
 | 
			
		||||
 | 
			
		||||
    set_maps(@statuses)
 | 
			
		||||
    set_counters_maps(@statuses)
 | 
			
		||||
    set_account_counters_maps(@statuses.flat_map { |s| [s.account, s.reblog? ? s.reblog.account : nil] }.compact.uniq)
 | 
			
		||||
 | 
			
		||||
    next_path = api_v1_mentions_timeline_url(max_id: @statuses.last.id)    if @statuses.size == limit_param(DEFAULT_STATUSES_LIMIT)
 | 
			
		||||
    prev_path = api_v1_mentions_timeline_url(since_id: @statuses.first.id) unless @statuses.empty?
 | 
			
		||||
 | 
			
		||||
    set_pagination_headers(next_path, prev_path)
 | 
			
		||||
 | 
			
		||||
    render action: :index
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def public
 | 
			
		||||
    @statuses = Status.as_public_timeline(current_account).paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id])
 | 
			
		||||
    @statuses = cache_collection(@statuses)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,7 +26,7 @@ class FeedManager
 | 
			
		|||
  def push(timeline_type, account, status)
 | 
			
		||||
    redis.zadd(key(timeline_type, account.id), status.id, status.reblog? ? status.reblog_of_id : status.id)
 | 
			
		||||
    trim(timeline_type, account.id)
 | 
			
		||||
    broadcast(account.id, type: 'update', timeline: timeline_type, message: inline_render(account, 'api/v1/statuses/show', status))
 | 
			
		||||
    broadcast(account.id, event: 'update', payload: inline_render(account, 'api/v1/statuses/show', status))
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def broadcast(timeline_id, options = {})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -102,10 +102,6 @@ class Status < ApplicationRecord
 | 
			
		|||
      where(account: [account] + account.following)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def as_mentions_timeline(account)
 | 
			
		||||
      where(id: Mention.where(account: account).select(:status_id))
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def as_public_timeline(account = nil)
 | 
			
		||||
      query = joins('LEFT OUTER JOIN accounts ON statuses.account_id = accounts.id')
 | 
			
		||||
              .where(visibility: :public)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,6 @@ class FanOutOnWriteService < BaseService
 | 
			
		|||
  def call(status)
 | 
			
		||||
    deliver_to_self(status) if status.account.local?
 | 
			
		||||
    deliver_to_followers(status)
 | 
			
		||||
    deliver_to_mentioned(status)
 | 
			
		||||
 | 
			
		||||
    return if status.account.silenced? || !status.public_visibility? || status.reblog?
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -33,25 +32,15 @@ class FanOutOnWriteService < BaseService
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def deliver_to_mentioned(status)
 | 
			
		||||
    Rails.logger.debug "Delivering status #{status.id} to mentioned accounts"
 | 
			
		||||
 | 
			
		||||
    status.mentions.includes(:account).each do |mention|
 | 
			
		||||
      mentioned_account = mention.account
 | 
			
		||||
      next if !mentioned_account.local? || FeedManager.instance.filter?(:mentions, status, mentioned_account)
 | 
			
		||||
      FeedManager.instance.push(:mentions, mentioned_account, status)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def deliver_to_hashtags(status)
 | 
			
		||||
    Rails.logger.debug "Delivering status #{status.id} to hashtags"
 | 
			
		||||
    status.tags.find_each do |tag|
 | 
			
		||||
      FeedManager.instance.broadcast("hashtag:#{tag.name}", type: 'update', id: status.id)
 | 
			
		||||
      FeedManager.instance.broadcast("hashtag:#{tag.name}", event: 'update', payload: status.id)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def deliver_to_public(status)
 | 
			
		||||
    Rails.logger.debug "Delivering status #{status.id} to public timeline"
 | 
			
		||||
    FeedManager.instance.broadcast(:public, type: 'update', id: status.id)
 | 
			
		||||
    FeedManager.instance.broadcast(:public, event: 'update', payload: status.id)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -51,7 +51,7 @@ class NotifyService < BaseService
 | 
			
		|||
  def create_notification
 | 
			
		||||
    @notification.save!
 | 
			
		||||
    return unless @notification.browserable?
 | 
			
		||||
    FeedManager.instance.broadcast(@recipient.id, type: 'notification', message: FeedManager.instance.inline_render(@recipient, 'api/v1/notifications/show', @notification))
 | 
			
		||||
    FeedManager.instance.broadcast(@recipient.id, event: 'notification', payload: FeedManager.instance.inline_render(@recipient, 'api/v1/notifications/show', @notification))
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def send_email
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -59,17 +59,17 @@ class RemoveStatusService < BaseService
 | 
			
		|||
      redis.zremrangebyscore(FeedManager.instance.key(type, receiver.id), status.id, status.id)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    FeedManager.instance.broadcast(receiver.id, type: 'delete', id: status.id)
 | 
			
		||||
    FeedManager.instance.broadcast(receiver.id, event: 'delete', payload: status.id)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def remove_from_hashtags(status)
 | 
			
		||||
    status.tags.each do |tag|
 | 
			
		||||
      FeedManager.instance.broadcast("hashtag:#{tag.name}", type: 'delete', id: status.id)
 | 
			
		||||
      FeedManager.instance.broadcast("hashtag:#{tag.name}", event: 'delete', payload: status.id)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def remove_from_public(status)
 | 
			
		||||
    FeedManager.instance.broadcast(:public, type: 'delete', id: status.id)
 | 
			
		||||
    FeedManager.instance.broadcast(:public, event: 'delete', payload: status.id)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def redis
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -108,7 +108,6 @@ Rails.application.routes.draw do
 | 
			
		|||
      end
 | 
			
		||||
 | 
			
		||||
      get '/timelines/home',     to: 'timelines#home', as: :home_timeline
 | 
			
		||||
      get '/timelines/mentions', to: 'timelines#mentions', as: :mentions_timeline
 | 
			
		||||
      get '/timelines/public',   to: 'timelines#public', as: :public_timeline
 | 
			
		||||
      get '/timelines/tag/:id',  to: 'timelines#tag', as: :hashtag_timeline
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -65,7 +65,6 @@ Returns a media object with an ID that can be attached when creating a status (s
 | 
			
		|||
### Retrieving a timeline
 | 
			
		||||
 | 
			
		||||
**GET /api/v1/timelines/home**
 | 
			
		||||
**GET /api/v1/timelines/mentions**
 | 
			
		||||
**GET /api/v1/timelines/public**
 | 
			
		||||
**GET /api/v1/timelines/tag/:hashtag**
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,8 @@
 | 
			
		|||
Push notifications
 | 
			
		||||
==================
 | 
			
		||||
 | 
			
		||||
**Note: This push notification design turned out to not be fully operational on the side of Firebase. A different approach is in consideration**
 | 
			
		||||
 | 
			
		||||
Mastodon can communicate with the Firebase Cloud Messaging API to send push notifications to apps on users' devices. For this to work, these conditions must be met:
 | 
			
		||||
 | 
			
		||||
* Responsibility of an instance owner: `FCM_API_KEY` set on the instance. This can be obtained on the Firebase dashboard, in project settings, under Cloud Messaging, as "server key"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -36,7 +36,6 @@ namespace :mastodon do
 | 
			
		|||
    task clear: :environment do
 | 
			
		||||
      User.where('current_sign_in_at < ?', 14.days.ago).find_each do |user|
 | 
			
		||||
        Redis.current.del(FeedManager.instance.key(:home, user.account_id))
 | 
			
		||||
        Redis.current.del(FeedManager.instance.key(:mentions, user.account_id))
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,13 +19,6 @@ RSpec.describe Api::V1::TimelinesController, type: :controller do
 | 
			
		|||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    describe 'GET #mentions' do
 | 
			
		||||
      it 'returns http success' do
 | 
			
		||||
        get :mentions
 | 
			
		||||
        expect(response).to have_http_status(:success)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    describe 'GET #public' do
 | 
			
		||||
      it 'returns http success' do
 | 
			
		||||
        get :public
 | 
			
		||||
| 
						 | 
				
			
			@ -55,13 +48,6 @@ RSpec.describe Api::V1::TimelinesController, type: :controller do
 | 
			
		|||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    describe 'GET #mentions' do
 | 
			
		||||
      it 'returns http unprocessable entity' do
 | 
			
		||||
        get :mentions
 | 
			
		||||
        expect(response).to have_http_status(:unprocessable_entity)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    describe 'GET #public' do
 | 
			
		||||
      it 'returns http success' do
 | 
			
		||||
        get :public
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,10 +26,6 @@ RSpec.describe FanOutOnWriteService do
 | 
			
		|||
    expect(Feed.new(:home, follower).get(10).map(&:id)).to include status.id
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  it 'delivers status to mentioned users' do
 | 
			
		||||
    expect(Feed.new(:mentions, alice).get(10).map(&:id)).to include status.id
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  it 'delivers status to hashtag' do
 | 
			
		||||
    expect(Tag.find_by!(name: 'test').statuses.pluck(:id)).to include status.id
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue