Added a timeline for Direct statuses
* Lists all Direct statuses you've sent and received * Displayed in Getting Started * Streaming server support for direct TL
This commit is contained in:
		
							
								
								
									
										60
									
								
								app/controllers/api/v1/timelines/direct_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								app/controllers/api/v1/timelines/direct_controller.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,60 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class Api::V1::Timelines::DirectController < Api::BaseController
 | 
			
		||||
  before_action -> { doorkeeper_authorize! :read }, only: [:show]
 | 
			
		||||
  before_action :require_user!, only: [:show]
 | 
			
		||||
  after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
 | 
			
		||||
 | 
			
		||||
  respond_to :json
 | 
			
		||||
 | 
			
		||||
  def show
 | 
			
		||||
    @statuses = load_statuses
 | 
			
		||||
    render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def load_statuses
 | 
			
		||||
    cached_direct_statuses
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def cached_direct_statuses
 | 
			
		||||
    cache_collection direct_statuses, Status
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def direct_statuses
 | 
			
		||||
    direct_timeline_statuses.paginate_by_max_id(
 | 
			
		||||
      limit_param(DEFAULT_STATUSES_LIMIT),
 | 
			
		||||
      params[:max_id],
 | 
			
		||||
      params[:since_id]
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def direct_timeline_statuses
 | 
			
		||||
    Status.as_direct_timeline(current_account)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def insert_pagination_headers
 | 
			
		||||
    set_pagination_headers(next_path, prev_path)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def pagination_params(core_params)
 | 
			
		||||
    params.permit(:local, :limit).merge(core_params)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def next_path
 | 
			
		||||
    api_v1_timelines_direct_url pagination_params(max_id: pagination_max_id)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def prev_path
 | 
			
		||||
    api_v1_timelines_direct_url pagination_params(since_id: pagination_since_id)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def pagination_max_id
 | 
			
		||||
    @statuses.last.id
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def pagination_since_id
 | 
			
		||||
    @statuses.first.id
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -128,6 +128,8 @@ export function submitCompose() {
 | 
			
		||||
      if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
 | 
			
		||||
        insertOrRefresh('community', refreshCommunityTimeline);
 | 
			
		||||
        insertOrRefresh('public', refreshPublicTimeline);
 | 
			
		||||
      } else if (response.data.visibility === 'direct') {
 | 
			
		||||
        dispatch(updateTimeline('direct', { ...response.data }));
 | 
			
		||||
      }
 | 
			
		||||
    }).catch(function (error) {
 | 
			
		||||
      dispatch(submitComposeFail(error));
 | 
			
		||||
 
 | 
			
		||||
@@ -92,3 +92,4 @@ export const connectCommunityStream = () => connectTimelineStream('community', '
 | 
			
		||||
export const connectMediaStream = () => connectTimelineStream('community', 'public:local');
 | 
			
		||||
export const connectPublicStream = () => connectTimelineStream('public', 'public');
 | 
			
		||||
export const connectHashtagStream = (tag) => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`);
 | 
			
		||||
export const connectDirectStream = () => connectTimelineStream('direct', 'direct');
 | 
			
		||||
 
 | 
			
		||||
@@ -115,6 +115,7 @@ export function refreshTimeline(timelineId, path, params = {}) {
 | 
			
		||||
export const refreshHomeTimeline         = () => refreshTimeline('home', '/api/v1/timelines/home');
 | 
			
		||||
export const refreshPublicTimeline       = () => refreshTimeline('public', '/api/v1/timelines/public');
 | 
			
		||||
export const refreshCommunityTimeline    = () => refreshTimeline('community', '/api/v1/timelines/public', { local: true });
 | 
			
		||||
export const refreshDirectTimeline       = () => refreshTimeline('direct', '/api/v1/timelines/direct');
 | 
			
		||||
export const refreshAccountTimeline      = accountId => refreshTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`);
 | 
			
		||||
export const refreshAccountMediaTimeline = accountId => refreshTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
 | 
			
		||||
export const refreshHashtagTimeline      = hashtag => refreshTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);
 | 
			
		||||
@@ -155,6 +156,7 @@ export function expandTimeline(timelineId, path, params = {}) {
 | 
			
		||||
export const expandHomeTimeline         = () => expandTimeline('home', '/api/v1/timelines/home');
 | 
			
		||||
export const expandPublicTimeline       = () => expandTimeline('public', '/api/v1/timelines/public');
 | 
			
		||||
export const expandCommunityTimeline    = () => expandTimeline('community', '/api/v1/timelines/public', { local: true });
 | 
			
		||||
export const expandDirectTimeline       = () => expandTimeline('direct', '/api/v1/timelines/direct');
 | 
			
		||||
export const expandAccountTimeline      = accountId => expandTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`);
 | 
			
		||||
export const expandAccountMediaTimeline = accountId => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
 | 
			
		||||
export const expandHashtagTimeline      = hashtag => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,17 @@
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
import ColumnSettings from '../../community_timeline/components/column_settings';
 | 
			
		||||
import { changeSetting } from '../../../actions/settings';
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = state => ({
 | 
			
		||||
  settings: state.getIn(['settings', 'direct']),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const mapDispatchToProps = dispatch => ({
 | 
			
		||||
 | 
			
		||||
  onChange (key, checked) {
 | 
			
		||||
    dispatch(changeSetting(['direct', ...key], checked));
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);
 | 
			
		||||
							
								
								
									
										107
									
								
								app/javascript/mastodon/features/direct_timeline/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								app/javascript/mastodon/features/direct_timeline/index.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,107 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import StatusListContainer from '../ui/containers/status_list_container';
 | 
			
		||||
import Column from '../../components/column';
 | 
			
		||||
import ColumnHeader from '../../components/column_header';
 | 
			
		||||
import {
 | 
			
		||||
  refreshDirectTimeline,
 | 
			
		||||
  expandDirectTimeline,
 | 
			
		||||
} from '../../actions/timelines';
 | 
			
		||||
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
 | 
			
		||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
			
		||||
import ColumnSettingsContainer from './containers/column_settings_container';
 | 
			
		||||
import { connectDirectStream } from '../../actions/streaming';
 | 
			
		||||
 | 
			
		||||
const messages = defineMessages({
 | 
			
		||||
  title: { id: 'column.direct', defaultMessage: 'Direct messages' },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = state => ({
 | 
			
		||||
  hasUnread: state.getIn(['timelines', 'direct', 'unread']) > 0,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@connect(mapStateToProps)
 | 
			
		||||
@injectIntl
 | 
			
		||||
export default class DirectTimeline extends React.PureComponent {
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    dispatch: PropTypes.func.isRequired,
 | 
			
		||||
    columnId: PropTypes.string,
 | 
			
		||||
    intl: PropTypes.object.isRequired,
 | 
			
		||||
    hasUnread: PropTypes.bool,
 | 
			
		||||
    multiColumn: PropTypes.bool,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handlePin = () => {
 | 
			
		||||
    const { columnId, dispatch } = this.props;
 | 
			
		||||
 | 
			
		||||
    if (columnId) {
 | 
			
		||||
      dispatch(removeColumn(columnId));
 | 
			
		||||
    } else {
 | 
			
		||||
      dispatch(addColumn('DIRECT', {}));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleMove = (dir) => {
 | 
			
		||||
    const { columnId, dispatch } = this.props;
 | 
			
		||||
    dispatch(moveColumn(columnId, dir));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleHeaderClick = () => {
 | 
			
		||||
    this.column.scrollTop();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidMount () {
 | 
			
		||||
    const { dispatch } = this.props;
 | 
			
		||||
 | 
			
		||||
    dispatch(refreshDirectTimeline());
 | 
			
		||||
    this.disconnect = dispatch(connectDirectStream());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentWillUnmount () {
 | 
			
		||||
    if (this.disconnect) {
 | 
			
		||||
      this.disconnect();
 | 
			
		||||
      this.disconnect = null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setRef = c => {
 | 
			
		||||
    this.column = c;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleLoadMore = () => {
 | 
			
		||||
    this.props.dispatch(expandDirectTimeline());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { intl, hasUnread, columnId, multiColumn } = this.props;
 | 
			
		||||
    const pinned = !!columnId;
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <Column ref={this.setRef}>
 | 
			
		||||
        <ColumnHeader
 | 
			
		||||
          icon='envelope'
 | 
			
		||||
          active={hasUnread}
 | 
			
		||||
          title={intl.formatMessage(messages.title)}
 | 
			
		||||
          onPin={this.handlePin}
 | 
			
		||||
          onMove={this.handleMove}
 | 
			
		||||
          onClick={this.handleHeaderClick}
 | 
			
		||||
          pinned={pinned}
 | 
			
		||||
          multiColumn={multiColumn}
 | 
			
		||||
        >
 | 
			
		||||
          <ColumnSettingsContainer />
 | 
			
		||||
        </ColumnHeader>
 | 
			
		||||
 | 
			
		||||
        <StatusListContainer
 | 
			
		||||
          trackScroll={!pinned}
 | 
			
		||||
          scrollKey={`direct_timeline-${columnId}`}
 | 
			
		||||
          timelineId='direct'
 | 
			
		||||
          loadMore={this.handleLoadMore}
 | 
			
		||||
          emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}
 | 
			
		||||
        />
 | 
			
		||||
      </Column>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -16,6 +16,7 @@ const messages = defineMessages({
 | 
			
		||||
  navigation_subheading: { id: 'column_subheading.navigation', defaultMessage: 'Navigation' },
 | 
			
		||||
  settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' },
 | 
			
		||||
  community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
 | 
			
		||||
  direct: { id: 'navigation_bar.direct', defaultMessage: 'Direct messages' },
 | 
			
		||||
  preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
 | 
			
		||||
  follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
 | 
			
		||||
  sign_out: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
 | 
			
		||||
@@ -65,18 +66,22 @@ export default class GettingStarted extends ImmutablePureComponent {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    navItems = navItems.concat([
 | 
			
		||||
      <ColumnLink key='4' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
 | 
			
		||||
      <ColumnLink key='5' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    if (me.get('locked')) {
 | 
			
		||||
      navItems.push(<ColumnLink key='6' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />);
 | 
			
		||||
    if (!multiColumn || !columns.find(item => item.get('id') === 'DIRECT')) {
 | 
			
		||||
      navItems.push(<ColumnLink key='4' icon='envelope' text={intl.formatMessage(messages.direct)} to='/timelines/direct' />);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    navItems = navItems.concat([
 | 
			
		||||
      <ColumnLink key='7' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />,
 | 
			
		||||
      <ColumnLink key='8' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />,
 | 
			
		||||
      <ColumnLink key='5' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
 | 
			
		||||
      <ColumnLink key='6' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    if (me.get('locked')) {
 | 
			
		||||
      navItems.push(<ColumnLink key='7' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    navItems = navItems.concat([
 | 
			
		||||
      <ColumnLink key='8' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />,
 | 
			
		||||
      <ColumnLink key='9' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ import BundleContainer from '../containers/bundle_container';
 | 
			
		||||
import ColumnLoading from './column_loading';
 | 
			
		||||
import DrawerLoading from './drawer_loading';
 | 
			
		||||
import BundleColumnError from './bundle_column_error';
 | 
			
		||||
import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, FavouritedStatuses } from '../../ui/util/async-components';
 | 
			
		||||
import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses } from '../../ui/util/async-components';
 | 
			
		||||
 | 
			
		||||
import detectPassiveEvents from 'detect-passive-events';
 | 
			
		||||
import { scrollRight } from '../../../scroll';
 | 
			
		||||
@@ -23,6 +23,7 @@ const componentMap = {
 | 
			
		||||
  'PUBLIC': PublicTimeline,
 | 
			
		||||
  'COMMUNITY': CommunityTimeline,
 | 
			
		||||
  'HASHTAG': HashtagTimeline,
 | 
			
		||||
  'DIRECT': DirectTimeline,
 | 
			
		||||
  'FAVOURITES': FavouritedStatuses,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -28,6 +28,7 @@ import {
 | 
			
		||||
  Following,
 | 
			
		||||
  Reblogs,
 | 
			
		||||
  Favourites,
 | 
			
		||||
  DirectTimeline,
 | 
			
		||||
  HashtagTimeline,
 | 
			
		||||
  Notifications,
 | 
			
		||||
  FollowRequests,
 | 
			
		||||
@@ -350,6 +351,7 @@ export default class UI extends React.Component {
 | 
			
		||||
              <WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} />
 | 
			
		||||
              <WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} />
 | 
			
		||||
              <WrappedRoute path='/timelines/public/local' component={CommunityTimeline} content={children} />
 | 
			
		||||
              <WrappedRoute path='/timelines/direct' component={DirectTimeline} content={children} />
 | 
			
		||||
              <WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} />
 | 
			
		||||
 | 
			
		||||
              <WrappedRoute path='/notifications' component={Notifications} content={children} />
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,10 @@ export function HashtagTimeline () {
 | 
			
		||||
  return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag_timeline');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function DirectTimeline() {
 | 
			
		||||
  return import(/* webpackChunkName: "features/direct_timeline" */'../../direct_timeline');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function Status () {
 | 
			
		||||
  return import(/* webpackChunkName: "features/status" */'../../status');
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -755,6 +755,19 @@
 | 
			
		||||
    ],
 | 
			
		||||
    "path": "app/javascript/mastodon/features/compose/index.json"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "descriptors": [
 | 
			
		||||
      {
 | 
			
		||||
        "defaultMessage": "Direct messages",
 | 
			
		||||
        "id": "column.direct"
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "defaultMessage": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
 | 
			
		||||
        "id": "empty_column.direct"
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "path": "app/javascript/mastodon/features/direct_timeline/index.json"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "descriptors": [
 | 
			
		||||
      {
 | 
			
		||||
@@ -816,6 +829,10 @@
 | 
			
		||||
        "defaultMessage": "Local timeline",
 | 
			
		||||
        "id": "navigation_bar.community_timeline"
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "defaultMessage": "Direct messages",
 | 
			
		||||
        "id": "navigation_bar.direct"
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "defaultMessage": "Preferences",
 | 
			
		||||
        "id": "navigation_bar.preferences"
 | 
			
		||||
 
 | 
			
		||||
@@ -28,6 +28,7 @@
 | 
			
		||||
  "bundle_modal_error.retry": "Try again",
 | 
			
		||||
  "column.blocks": "Blocked users",
 | 
			
		||||
  "column.community": "Local timeline",
 | 
			
		||||
  "column.direct": "Direct messages",
 | 
			
		||||
  "column.favourites": "Favourites",
 | 
			
		||||
  "column.follow_requests": "Follow requests",
 | 
			
		||||
  "column.home": "Home",
 | 
			
		||||
@@ -80,6 +81,7 @@
 | 
			
		||||
  "emoji_button.symbols": "Symbols",
 | 
			
		||||
  "emoji_button.travel": "Travel & Places",
 | 
			
		||||
  "empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
 | 
			
		||||
  "empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
 | 
			
		||||
  "empty_column.hashtag": "There is nothing in this hashtag yet.",
 | 
			
		||||
  "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other users.",
 | 
			
		||||
  "empty_column.home.public_timeline": "the public timeline",
 | 
			
		||||
@@ -106,6 +108,7 @@
 | 
			
		||||
  "missing_indicator.label": "Not found",
 | 
			
		||||
  "navigation_bar.blocks": "Blocked users",
 | 
			
		||||
  "navigation_bar.community_timeline": "Local timeline",
 | 
			
		||||
  "navigation_bar.direct": "Direct messages",
 | 
			
		||||
  "navigation_bar.edit_profile": "Edit profile",
 | 
			
		||||
  "navigation_bar.favourites": "Favourites",
 | 
			
		||||
  "navigation_bar.follow_requests": "Follow requests",
 | 
			
		||||
 
 | 
			
		||||
@@ -57,6 +57,12 @@ const initialState = ImmutableMap({
 | 
			
		||||
      body: '',
 | 
			
		||||
    }),
 | 
			
		||||
  }),
 | 
			
		||||
 | 
			
		||||
  direct: ImmutableMap({
 | 
			
		||||
    regex: ImmutableMap({
 | 
			
		||||
      body: '',
 | 
			
		||||
    }),
 | 
			
		||||
  }),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const defaultColumns = fromJS([
 | 
			
		||||
 
 | 
			
		||||
@@ -154,6 +154,14 @@ class Status < ApplicationRecord
 | 
			
		||||
      where(account: [account] + account.following).where(visibility: [:public, :unlisted, :private])
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def as_direct_timeline(account)
 | 
			
		||||
      query = joins("LEFT OUTER JOIN mentions ON statuses.id = mentions.status_id AND mentions.account_id = #{account.id}")
 | 
			
		||||
              .where("mentions.account_id = #{account.id} OR statuses.account_id = #{account.id}")
 | 
			
		||||
              .where(visibility: [:direct])
 | 
			
		||||
 | 
			
		||||
      apply_timeline_filters(query, account, false)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def as_public_timeline(account = nil, local_only = false)
 | 
			
		||||
      query = timeline_scope(local_only).without_replies
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -40,6 +40,7 @@ class BatchedRemoveStatusService < BaseService
 | 
			
		||||
    # Cannot be batched
 | 
			
		||||
    statuses.each do |status|
 | 
			
		||||
      unpush_from_public_timelines(status)
 | 
			
		||||
      unpush_from_direct_timelines(status) if status.direct_visibility?
 | 
			
		||||
      batch_salmon_slaps(status) if status.local?
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
@@ -100,6 +101,16 @@ class BatchedRemoveStatusService < BaseService
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def unpush_from_direct_timelines(status)
 | 
			
		||||
    payload = @json_payloads[status.id]
 | 
			
		||||
    redis.pipelined do
 | 
			
		||||
      @mentions[status.id].each do |mention|
 | 
			
		||||
        redis.publish("timeline:direct:#{mention.account.id}", payload) if mention.account.local?
 | 
			
		||||
      end
 | 
			
		||||
      redis.publish("timeline:direct:#{status.account.id}", payload) if status.account.local?
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def batch_salmon_slaps(status)
 | 
			
		||||
    return if @mentions[status.id].empty?
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -10,15 +10,17 @@ class FanOutOnWriteService < BaseService
 | 
			
		||||
 | 
			
		||||
    deliver_to_self(status) if status.account.local?
 | 
			
		||||
 | 
			
		||||
    render_anonymous_payload(status)
 | 
			
		||||
 | 
			
		||||
    if status.direct_visibility?
 | 
			
		||||
      deliver_to_mentioned_followers(status)
 | 
			
		||||
      deliver_to_direct_timelines(status)
 | 
			
		||||
    else
 | 
			
		||||
      deliver_to_followers(status)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    return if status.account.silenced? || !status.public_visibility? || status.reblog?
 | 
			
		||||
 | 
			
		||||
    render_anonymous_payload(status)
 | 
			
		||||
    deliver_to_hashtags(status)
 | 
			
		||||
 | 
			
		||||
    return if status.reply? && status.in_reply_to_account_id != status.account_id
 | 
			
		||||
@@ -73,4 +75,13 @@ class FanOutOnWriteService < BaseService
 | 
			
		||||
    Redis.current.publish('timeline:public', @payload)
 | 
			
		||||
    Redis.current.publish('timeline:public:local', @payload) if status.local?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def deliver_to_direct_timelines(status)
 | 
			
		||||
    Rails.logger.debug "Delivering status #{status.id} to direct timelines"
 | 
			
		||||
 | 
			
		||||
    status.mentions.includes(:account).each do |mention|
 | 
			
		||||
      Redis.current.publish("timeline:direct:#{mention.account.id}", @payload) if mention.account.local?
 | 
			
		||||
    end
 | 
			
		||||
    Redis.current.publish("timeline:direct:#{status.account.id}", @payload) if status.account.local?
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ class RemoveStatusService < BaseService
 | 
			
		||||
    remove_reblogs
 | 
			
		||||
    remove_from_hashtags
 | 
			
		||||
    remove_from_public
 | 
			
		||||
    remove_from_direct if status.direct_visibility?
 | 
			
		||||
 | 
			
		||||
    @status.destroy!
 | 
			
		||||
 | 
			
		||||
@@ -121,6 +122,13 @@ class RemoveStatusService < BaseService
 | 
			
		||||
    Redis.current.publish('timeline:public:local', @payload) if @status.local?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def remove_from_direct
 | 
			
		||||
    @mentions.each do |mention|
 | 
			
		||||
      Redis.current.publish("timeline:direct:#{mention.account.id}", @payload) if mention.account.local?
 | 
			
		||||
    end
 | 
			
		||||
    Redis.current.publish("timeline:direct:#{@account.id}", @payload) if @account.local?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def redis
 | 
			
		||||
    Redis.current
 | 
			
		||||
  end
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user